File size: 44,286 Bytes
3730180
 
 
 
 
 
 
 
 
 
 
d329b93
693fffe
 
 
 
 
d329b93
93f0d3e
50d6c49
d329b93
 
4248ee7
d329b93
ab39182
5019758
 
 
 
6be7ae6
5019758
 
3730180
 
 
 
 
ab39182
 
693fffe
 
 
cd10e9e
693fffe
50d6c49
3730180
 
ab39182
 
3730180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d329b93
3730180
 
 
 
ab50a1a
50d6c49
 
 
 
ab50a1a
6be7ae6
 
 
d329b93
ab39182
3730180
 
693fffe
3730180
 
50d6c49
 
3730180
 
 
50d6c49
3730180
ab39182
 
3730180
ab50a1a
ab39182
 
 
ab50a1a
3730180
 
 
50d6c49
3730180
 
 
50d6c49
ab39182
693fffe
 
 
 
3730180
693fffe
ab39182
 
 
 
d329b93
ab39182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693fffe
 
 
 
 
 
4248ee7
693fffe
d329b93
693fffe
d329b93
8209c0b
 
693fffe
50d6c49
693fffe
 
 
 
 
 
 
 
d329b93
693fffe
 
 
 
d329b93
693fffe
50d6c49
 
 
 
 
 
693fffe
 
 
 
 
 
 
 
 
d329b93
693fffe
 
50d6c49
d329b93
50d6c49
 
 
693fffe
 
d329b93
4248ee7
693fffe
 
 
 
 
 
d329b93
693fffe
 
 
 
50d6c49
 
 
 
 
693fffe
 
 
4248ee7
693fffe
 
547be3f
3730180
 
 
ab39182
 
 
3730180
ab39182
3730180
 
 
 
 
547be3f
3730180
547be3f
3730180
d329b93
ab39182
d329b93
ab39182
d329b93
 
 
 
 
 
 
 
 
ab39182
50d6c49
cd10e9e
87eea9d
 
 
cd10e9e
 
 
8209c0b
cd10e9e
d329b93
 
 
cd10e9e
8209c0b
 
 
 
 
 
547be3f
693fffe
 
d329b93
 
 
 
 
 
 
 
5e29983
693fffe
d329b93
5e29983
50d6c49
 
d329b93
 
 
50d6c49
d329b93
 
 
 
 
 
 
 
 
4248ee7
d329b93
 
 
 
4248ee7
d329b93
 
 
 
 
 
 
 
93f0d3e
4248ee7
93f0d3e
4248ee7
d329b93
 
 
 
 
50d6c49
d329b93
5e29983
d329b93
 
 
 
 
 
 
5e29983
d329b93
 
5e29983
 
 
 
 
 
d329b93
 
5e29983
d329b93
 
5e29983
 
693fffe
ab39182
50d6c49
d329b93
 
 
 
5e29983
d329b93
 
 
5e29983
d329b93
 
 
 
 
5e29983
d329b93
547be3f
 
 
 
 
 
 
5e29983
547be3f
 
 
 
87eea9d
 
 
 
 
547be3f
87eea9d
547be3f
 
5e29983
 
 
6eaef4e
547be3f
87eea9d
547be3f
 
5e29983
6eaef4e
 
 
 
5e29983
547be3f
 
87eea9d
 
547be3f
5e29983
 
 
547be3f
 
5e29983
bcf99ae
 
 
d329b93
 
 
 
 
5e29983
d329b93
ab39182
d329b93
 
ab39182
5e29983
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d329b93
6eaef4e
 
 
 
 
 
 
d329b93
5e29983
d329b93
5e29983
 
d329b93
 
 
 
 
 
5e29983
d329b93
 
 
93f0d3e
d329b93
93f0d3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2760434
93f0d3e
d329b93
 
93f0d3e
 
d329b93
93f0d3e
bcf99ae
93f0d3e
 
 
 
dd28f79
5e29983
93f0d3e
 
bcf99ae
93f0d3e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd28f79
 
93f0d3e
f79e124
dd28f79
93f0d3e
 
 
dd28f79
d329b93
bcf99ae
dd28f79
 
 
93f0d3e
dd28f79
 
93f0d3e
 
 
dd28f79
93f0d3e
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from imblearn.over_sampling import SMOTE
from typing import Dict, Any, List, Tuple, Union

# Importações para métricas de avaliação e plots
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report, roc_curve, auc
import matplotlib.pyplot as plt
import seaborn as sns
import os
import tempfile # Para criar arquivos temporários para os plots
import re # Para auxiliar na limpeza de markdown para LaTeX
import shutil # Para copiar arquivos

# Importações para LaTeX (pylatex)
from pylatex import Document, Section, Command, LongTable, Tabular, Figure, NoEscape, Math, LineBreak
from pylatex.utils import italic
from pylatex.base_classes import Environment

# --- DEFINIÇÃO DAS FEATURES PREDITIVAS E COLUNA ALVO PARA SEU data.csv ---
ALL_PREDICTOR_FEATURES = [
    'CreditScore', 'Geography', 'Gender', 'Age', 'Tenure',
    'Balance', 'NumOfProducts', 'HasCrCard', 'IsActiveMember', 'EstimatedSalary'
]
TARGET_COLUMN = 'Exited' # Esta é a coluna que queremos prever, não um input.

class ChurnModelPipeline:
    def __init__(self):
        self.model = None
        self.preprocessor = None
        self.feature_names_out = None # Nomes das features após o pré-processamento
        self.coefficients_df = pd.DataFrame() # Para armazenar coeficientes e Odds Ratios
        self.metrics_dict = {} # Para armazenar métricas de avaliação
        self.plot_paths = {} # Para armazenar caminhos para os plots gerados
        self.X_test_processed = None
        self.y_test = None
        self.X_test = None # Armazenar X_test para samplear para simulação
        self.df_raw_for_plots = None # Armazenar df_raw para gerar o heatmap de correlação
        self.training_details = {} # Para armazenar detalhes do treinamento

    def _build_preprocessor(self, X: pd.DataFrame) -> ColumnTransformer:
        numeric_features = X.select_dtypes(include=np.number).columns.tolist()
        categorical_features = X.select_dtypes(include='object').columns.tolist()

        numeric_transformer = Pipeline(steps=[
            ('imputer', SimpleImputer(strategy='median')),
            ('scaler', StandardScaler())
        ])

        categorical_transformer = Pipeline(steps=[
            ('imputer', SimpleImputer(strategy='most_frequent')),
            ('onehot', OneHotEncoder(handle_unknown='ignore'))
        ])

        preprocessor = ColumnTransformer(
            transformers=[
                ('num', numeric_transformer, numeric_features),
                ('cat', categorical_transformer, categorical_features)
            ],
            remainder='drop'
        )
        return preprocessor

    def train(self, df: pd.DataFrame) -> None:
        print(f"Iniciando treinamento com {len(df)} linhas e features: {ALL_PREDICTOR_FEATURES}")
        self.training_details['dataset_rows'] = len(df)
        self.training_details['predictor_features'] = ALL_PREDICTOR_FEATURES
        self.training_details['target_column'] = TARGET_COLUMN

        missing_cols = [col for col in ALL_PREDICTOR_FEATURES + [TARGET_COLUMN] if col not in df.columns]
        if missing_cols:
            raise ValueError(f"Colunas ausentes no DataFrame: {missing_cols}. Verifique seu 'data.csv'.")

        self.df_raw_for_plots = df.copy()
        X = df[ALL_PREDICTOR_FEATURES]
        y = df[TARGET_COLUMN]

        X_train, self.X_test, y_train, self.y_test = train_test_split(
            X, y, test_size=0.2, random_state=42, stratify=y
        )
        self.training_details['X_train_shape'] = X_train.shape
        self.training_details['y_train_value_counts_before_smote'] = y_train.value_counts().to_dict()

        self.preprocessor = self._build_preprocessor(X_train)
        X_train_processed = self.preprocessor.fit_transform(X_train)
        self.training_details['X_train_processed_shape'] = X_train_processed.shape
        
        numeric_f = X_train.select_dtypes(include=np.number).columns.tolist()
        categorical_f = X_train.select_dtypes(include='object').columns.tolist()
        
        ohe_feature_names = []
        if 'cat' in self.preprocessor.named_transformers_ and isinstance(self.preprocessor.named_transformers_['cat'], Pipeline):
            if 'onehot' in self.preprocessor.named_transformers_['cat'].named_steps:
                ohe_feature_names = list(self.preprocessor.named_transformers_['cat'].named_steps['onehot'].get_feature_names_out(categorical_f))
        self.feature_names_out = numeric_f + ohe_feature_names

        smote = SMOTE(random_state=42)
        X_train_resampled, y_train_resampled = smote.fit_resample(X_train_processed, y_train)
        self.training_details['y_train_resampled_value_counts_after_smote'] = y_train_resampled.value_counts().to_dict()

        self.model = LogisticRegression(random_state=42, solver='liblinear', C=0.1, max_iter=500)
        self.model.fit(X_train_resampled, y_train_resampled)
        self.training_details['model_trained_successfully'] = True

        self.X_test_processed = self.preprocessor.transform(self.X_test)
        self._evaluate_and_store_results(self.y_test, self.X_test_processed)
        
        self._generate_plots()
    
    def _evaluate_and_store_results(self, y_test: pd.Series, X_test_processed: np.ndarray) -> None:
        if self.model is None or self.preprocessor is None:
            raise RuntimeError("Modelo ou pré-processador não treinados para avaliação.")

        y_pred = self.model.predict(X_test_processed)
        y_proba = self.model.predict_proba(X_test_processed)[:, 1]

        self.metrics_dict = {
            "Acurácia": accuracy_score(y_test, y_pred),
            "AUC ROC": roc_auc_score(y_test, y_proba),
            "Precisão": precision_score(y_test, y_pred),
            "Recall (Sensibilidade)": recall_score(y_test, y_pred),
            "F1-Score": f1_score(y_test, y_pred)
        }
        
        if hasattr(self.model, 'coef_') and self.feature_names_out:
            coefs = self.model.coef_[0] if self.model.coef_.ndim > 1 else self.model.coef_
            self.coefficients_df = pd.DataFrame({'Feature': self.feature_names_out, 'Coeficiente': coefs})
            self.coefficients_df['Odds_Ratio'] = np.exp(self.coefficients_df['Coeficiente'])
            self.coefficients_df = self.coefficients_df.sort_values(by='Odds_Ratio', ascending=False).reset_index(drop=True)

    def _generate_plots(self) -> None:
        if self.model is None or self.preprocessor is None or self.X_test_processed is None or self.y_test is None:
            return

        plot_dir = tempfile.mkdtemp()
        self.plot_paths = {}
        dpi = 150 # Aumentado DPI para melhor qualidade em relatórios

        # --- 1. Correlation Heatmap ---
        if self.df_raw_for_plots is not None and not self.df_raw_for_plots.empty:
            plt.figure(figsize=(12, 10), dpi=dpi)
            numeric_cols_for_corr = self.df_raw_for_plots[ALL_PREDICTOR_FEATURES + [TARGET_COLUMN]].select_dtypes(include=np.number)
            corr_matrix = numeric_cols_for_corr.corr()
            sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt=".2f")
            plt.title('Mapa de Calor da Correlação entre as Variáveis Numéricas', fontsize=16)
            heatmap_path = os.path.join(plot_dir, 'correlation_heatmap.png')
            plt.tight_layout()
            plt.savefig(heatmap_path)
            plt.close()
            self.plot_paths['heatmap'] = heatmap_path

        # --- 2. Odds Ratios Chart ---
        if not self.coefficients_df.empty:
            plt.figure(figsize=(14, 8), dpi=dpi)
            plot_df = self.coefficients_df.copy()
            plot_df['Abs_Coef'] = np.abs(plot_df['Coeficiente'])
            plot_df = plot_df.sort_values(by='Abs_Coef', ascending=False).head(20)

            sns.barplot(x='Odds_Ratio', y='Feature', data=plot_df, palette='viridis', hue='Feature', legend=False)
            plt.axvline(1, color='red', linestyle='--', linewidth=0.8, label='Sem Efeito (Odds Ratio = 1)')
            plt.title('Odds Ratios das Features (Top 20 por impacto absoluto)', fontsize=16)
            plt.xlabel('Odds Ratio (escala logarítmica)', fontsize=12)
            plt.ylabel('Feature', fontsize=12)
            plt.xscale('log')
            plt.xticks(fontsize=10)
            plt.yticks(fontsize=10)
            plt.tight_layout()
            odds_ratio_path = os.path.join(plot_dir, 'odds_ratios.png')
            plt.savefig(odds_ratio_path)
            plt.close()
            self.plot_paths['odds_ratios'] = odds_ratio_path

        # --- 3. Confusion Matrix ---
        y_pred = self.model.predict(self.X_test_processed)
        cm = confusion_matrix(self.y_test, y_pred)
        plt.figure(figsize=(8, 6), dpi=dpi)
        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                    xticklabels=['Previsto Não Churn', 'Previsto Churn'],
                    yticklabels=['Real Não Churn', 'Real Churn'],
                    annot_kws={"size": 14})
        plt.title('Matriz de Confusão do Modelo (Conjunto de Teste)', fontsize=16)
        plt.xlabel('Rótulo Previsto', fontsize=12)
        plt.ylabel('Rótulo Verdadeiro', fontsize=12)
        plt.tight_layout()
        cm_path = os.path.join(plot_dir, 'confusion_matrix.png')
        plt.savefig(cm_path)
        plt.close() # Fechar a figura para liberar memória
        self.plot_paths['confusion_matrix'] = cm_path

        # --- 4. ROC Curve ---
        y_proba = self.model.predict_proba(self.X_test_processed)[:, 1]
        fpr, tpr, _ = roc_curve(self.y_test, y_proba)
        roc_auc = auc(fpr, tpr)
        plt.figure(figsize=(8, 6), dpi=dpi)
        plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'Curva ROC (área = {roc_auc:.2f})')
        plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
        plt.xlim([0.0, 1.0])
        plt.ylim([0.0, 1.05])
        plt.xlabel('Taxa de Falso Positivo', fontsize=12)
        plt.ylabel('Taxa de Verdadeiro Positivo', fontsize=12)
        plt.title('Curva ROC do Modelo de Churn (Conjunto de Teste)', fontsize=16)
        plt.legend(loc="lower right", fontsize=10)
        plt.grid(True, linestyle='--', alpha=0.6)
        plt.tight_layout()
        roc_path = os.path.join(plot_dir, 'roc_curve.png')
        plt.savefig(roc_path)
        plt.close() # Fechar a figura para liberar memória
        self.plot_paths['roc_curve'] = roc_path
        
    def predict_churn(self, input_data: pd.DataFrame) -> Tuple[int, float, float]: # Adicionado float para logit_value
        if self.model is None or self.preprocessor is None:
            raise RuntimeError("Modelo ou pré-processador não treinados. Chame .train() primeiro.")
        
        if not all(col in input_data.columns for col in ALL_PREDICTOR_FEATURES):
            missing_cols = [col for col in ALL_PREDICTOR_FEATURES if col not in input_data.columns]
            raise ValueError(f"Dados de entrada brutos não contêm todas as features esperadas: {missing_cols}. Features esperadas: {ALL_PREDICTOR_FEATURES}")

        input_data_ordered = input_data[ALL_PREDICTOR_FEATURES]

        X_processed = self.preprocessor.transform(input_data_ordered)
        
        prediction = self.model.predict(X_processed)[0]
        probability_churn = self.model.predict_proba(X_processed)[0][1]
        logit_value = self.model.decision_function(X_processed)[0] # Obtém o valor do logit (L)
        
        return int(prediction), float(probability_churn), float(logit_value) # Retorna os três valores

    def _get_summary_latex_and_markdown_parts(self, last_interactive_input_df: pd.DataFrame = None) -> Tuple[str, List[Any], Dict[str, str]]:
        if self.model is None or self.preprocessor is None:
            return "Modelo ainda não treinado. Execute o treinamento primeiro.", [], {}

        markdown_story = []
        latex_story = []

        # --- Título Principal ---
        markdown_story.append("# Resumo Detalhado do Modelo de Churn")
        latex_story.append(Section(NoEscape(r'Resumo Detalhado do Modelo de Churn')))
        
        markdown_story.append("Este relatório consolida as informações sobre o modelo de Regressão Logística treinado para prever o Churn de clientes bancários, conforme as demandas da Tarefa 5 de AEDI.\n")
        latex_story.append(NoEscape(r'Este relatório consolida as informações sobre o modelo de Regressão Logística treinado para prever o Churn de clientes bancários, conforme as demandas da Tarefa 5 de AEDI.\n\n'))

        # --- 1. Simulação Rápida (Última Previsão Interativa ou Exemplo) ---
        sample_customer_df = None
        logit_sample = 0.0 # Inicializar com valores padrão
        prob_sample = 0.0

        if last_interactive_input_df is not None and not last_interactive_input_df.empty:
            sample_customer_df = last_interactive_input_df
        elif self.X_test is not None and not self.X_test.empty:
            sample_customer_df = self.X_test.sample(1, random_state=42)
        
        markdown_story.append("## 1. Simulação Rápida (Última Previsão Interativa ou Exemplo)")
        latex_story.append(Section(NoEscape(r'Simulação Rápida (Última Previsão Interativa ou Exemplo)'), False)) # False para não numerar subseção
        
        if sample_customer_df is not None:
            sample_display_df = sample_customer_df.copy()
            if 'HasCrCard' in sample_display_df.columns:
                sample_display_df['HasCrCard'] = sample_display_df['HasCrCard'].apply(lambda x: 'Sim' if x == 1 else 'Não')
            if 'IsActiveMember' in sample_display_df.columns:
                sample_display_df['IsActiveMember'] = sample_display_df['IsActiveMember'].apply(lambda x: 'Sim' if x == 1 else 'Não')

            pred_sample, prob_sample, logit_sample = self.predict_churn(sample_customer_df)
            churn_status_sample = "PROVAVELMENTE dará CHURN." if pred_sample == 1 else "PROVAVELMENTE NÃO dará CHURN."
            
            markdown_story.append("Esta seção mostra a previsão para o último conjunto de dados inserido na aba 'Previsão Interativa', ou um exemplo de cliente do conjunto de teste se nenhuma previsão interativa foi feita.\n")
            latex_story.append(NoEscape(r'Esta seção mostra a previsão para o último conjunto de dados inserido na aba `Previsão Interativa`, ou um exemplo de cliente do conjunto de teste se nenhuma previsão interativa foi feita.\n\n'))
            
            markdown_story.append("**Características do Cliente Simulado:**\n" + sample_display_df.to_markdown(index=False) + "\n")
            latex_story.append(NoEscape(r'\textbf{Características do Cliente Simulado:}\n'))
            latex_story.append(NoEscape(sample_display_df.to_latex(index=False, caption='Características do Cliente Simulado', label='tab:sim_customer', longtable=False)))
            
            markdown_story.append(f"**Resultado da Simulação:** O cliente **{churn_status_sample}** (Probabilidade de Churn: **{prob_sample:.2%}**)\n")
            latex_story.append(NoEscape(fr'\textbf{{Resultado da Simulação:}} O cliente \textbf{{{churn_status_sample}}} (Probabilidade de Churn: \textbf{{{prob_sample:.2f}\%}})\n\n'))
        else:
            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")
            latex_story.append(NoEscape(r'Não foi possível realizar uma simulação pois o DataFrame de teste ou dados interativos não estão disponíveis.\n\n'))
        
        # --- 2. Detalhes do Processo de Treinamento ---
        markdown_story.append("## 2. Detalhes do Processo de Treinamento")
        latex_story.append(Section(NoEscape(r'Detalhes do Processo de Treinamento'), False))
        
        if self.training_details:
            training_details_markdown = ""
            training_details_latex = ""

            training_details_markdown += f"- **Dataset Carregado:** `{self.training_details.get('dataset_rows', 'N/A')} linhas`\n"
            training_details_markdown += f"- **Features Preditivas:** `{', '.join(self.training_details.get('predictor_features', ['N/A']))}`\n"
            training_details_markdown += f"- **Coluna Alvo:** `{self.training_details.get('target_column', 'N/A')}`\n"
            training_details_markdown += f"- **Shape X_train (antes pré-processamento):** `{self.training_details.get('X_train_shape', 'N/A')}`\n"
            
            y_train_before_smote = self.training_details.get('y_train_value_counts_before_smote', {})
            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"
            
            training_details_markdown += f"- **Shape X_train (após pré-processamento):** `{self.training_details.get('X_train_processed_shape', 'N/A')}`\n"
            
            y_train_after_smote = self.training_details.get('y_train_resampled_value_counts_after_smote', {})
            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"
            
            training_details_markdown += f"- **Modelo Treinado:** `{'Sim' if self.training_details.get('model_trained_successfully', False) else 'Não'}`\n"
            
            # Formato LaTeX
            training_details_latex += r'\begin{itemize}' + '\n'
            training_details_latex += fr'\item \textbf{{Dataset Carregado:}} {self.training_details.get("dataset_rows", "N/A")} linhas' + '\n'
            training_details_latex += fr'\item \textbf{{Features Preditivas:}} \texttt{{{", ".join(self.training_details.get("predictor_features", ["N/A"]))}}}.' + '\n'
            training_details_latex += fr'\item \textbf{{Coluna Alvo:}} \texttt{{{self.training_details.get("target_column", "N/A")}}}.' + '\n'
            training_details_latex += fr'\item \textbf{{Shape $X_{{\text{{train}}}}$ (antes pré-processamento):}} {self.training_details.get("X_train_shape", "N/A")}.' + '\n'
            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'
            training_details_latex += fr'\item \textbf{{Shape $X_{{\text{{train}}}}$ (após pré-processamento):}} {self.training_details.get("X_train_processed_shape", "N/A")}.' + '\n'
            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'
            training_details_latex += fr'\item \textbf{{Modelo Treinado:}} {"Sim" if self.training_details.get("model_trained_successfully", False) else "Não"}.' + '\n'
            training_details_latex += r'\end{itemize}' + '\n\n'
            
            markdown_story.append(training_details_markdown)
            latex_story.append(NoEscape(training_details_latex))
        else:
            markdown_story.append("Nenhum detalhe de treinamento disponível.\n")
            latex_story.append(NoEscape(r'Nenhum detalhe de treinamento disponível.\n\n'))


        # --- 3. Descrição do Modelo e Metodologia ---
        markdown_story.append("## 3. Descrição do Modelo e Metodologia")
        latex_story.append(Section(NoEscape(r'Descrição do Modelo e Metodologia'), False))
        
        markdown_story.append("O modelo utiliza **Regressão Logística** para classificar a probabilidade de um cliente sair (churn). Foram aplicadas as seguintes etapas para garantir robustez e tratar as características dos dados:\n")
        latex_story.append(NoEscape(r'O modelo utiliza \textbf{Regressão Logística} para classificar a probabilidade de um cliente sair (churn). Foram aplicadas as seguintes etapas para garantir robustez e tratar as características dos dados:\n\n'))
        
        markdown_story.append("- **Pré-processamento de Dados:**\n    - **Features Numéricas:** Imputação de valores ausentes (mediana) e escalonamento (`StandardScaler`) para padronização.\n    - **Features Categóricas:** Imputação de valores ausentes (moda) e codificação One-Hot (`OneHotEncoder`) para transformar categorias em formato numérico.\n")
        latex_story.append(NoEscape(r'\begin{itemize}' + '\n'))
        latex_story.append(NoEscape(r'\item \textbf{Pré-processamento de Dados:}' + '\n'))
        latex_story.append(NoEscape(r'\begin{itemize}' + '\n'))
        latex_story.append(NoEscape(r'\item \textbf{Features Numéricas:} Imputação de valores ausentes (mediana) e escalonamento (\texttt{StandardScaler}) para padronização.' + '\n'))
        latex_story.append(NoEscape(r'\item \textbf{Features Categóricas:} Imputação de valores ausentes (moda) e codificação One-Hot (\texttt{OneHotEncoder}) para transformar categorias em formato numérico.' + '\n'))
        latex_story.append(NoEscape(r'\end{itemize}' + '\n'))
        
        markdown_story.append("- **Balanceamento de Classes (SMOTE):** O conjunto de dados original apresentava desbalanceamento significativo na variável alvo (`Exited`). O algoritmo SMOTE (Synthetic Minority Over-sampling Technique) foi aplicado para gerar amostras sintéticas da classe minoritária (clientes que saem), garantindo que o modelo não seja viesado para a classe majoritária (clientes que permanecem).\n")
        latex_story.append(NoEscape(r'\item \textbf{Balanceamento de Classes (SMOTE):} O conjunto de dados original apresentava desbalanceamento significativo na variável alvo (\texttt{Exited}). O algoritmo SMOTE (Synthetic Minority Over-sampling Technique) foi aplicado para gerar amostras sintéticas da classe minoritária (clientes que saem), garantindo que o modelo não seja viesado para a classe majoritária (clientes que permanecem).' + '\n'))
        
        markdown_story.append("- **Regularização (L2):** A Regressão Logística foi configurada com um parâmetro `C=0.1` (inverso da força de regularização), que aplica regularização L2. Isso ajuda a prevenir o overfitting, penalizando coeficientes grandes e promovendo um modelo mais generalizável.\n")
        latex_story.append(NoEscape(r'\item \textbf{Regularização (L2):} A Regressão Logística foi configurada com um parâmetro \texttt{C=0.1} (inverso da força de regularização), que aplica regularização L2. Isso ajuda a prevenir o overfitting, penalizando coeficientes grandes e promovendo um modelo mais generalizável.' + '\n'))
        latex_story.append(NoEscape(r'\end{itemize}' + '\n\n'))


        # --- 4. Como a Probabilidade de Churn é Calculada ---
        markdown_story.append("## 4. Como a Probabilidade de Churn é Calculada")
        latex_story.append(Section(NoEscape(r'Como a Probabilidade de Churn é Calculada'), False))
        
        markdown_story.append("A Regressão Logística é um modelo de classificação que estima a probabilidade de um evento (neste caso, o churn do cliente) ocorrer. Ao contrário da regressão linear, que prevê um valor contínuo, a regressão logística utiliza a **função sigmoide** para mapear qualquer valor real para um valor entre 0 e 1, que pode ser interpretado como probabilidade.\n")
        latex_story.append(NoEscape(r'A Regressão Logística é um modelo de classificação que estima a probabilidade de um evento (neste caso, o churn do cliente) ocorrer. Ao contrário da regressão linear, que prevê um valor contínuo, a regressão logística utiliza a \textbf{função sigmoide} para mapear qualquer valor real para um valor entre 0 e 1, que pode ser interpretado como probabilidade.\n\n'))
        
        markdown_story.append("A equação básica de um modelo linear (`L`) é:\n`L = β₀ + β₁X₁ + β₂X₂ + ... + βₙXₙ`\nOnde `β` são os coeficientes (pesos) das features (`X`).\n")
        latex_story.append(Math(data=[NoEscape(r'L = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \dots + \beta_n X_n')]))
        latex_story.append(NoEscape(r'\nOnde $\beta$ são os coeficientes (pesos) das features ($X$).\n\n'))
        
        markdown_story.append("A probabilidade (`P`) de churn é então calculada aplicando-se a função sigmoide (σ) a `L`:\n`P(Churn) = σ(L) = 1 / (1 + e⁻ᴸ)`\n")
        latex_story.append(Math(data=[NoEscape(r'P(\text{Churn}) = \sigma(L) = \frac{1}{1 + e^{-L}}')]))
        
        markdown_story.append("Esta função garante que a saída esteja sempre entre 0 e 1, representando a probabilidade de o cliente pertencer à classe 'Churn' (ou seja, `Exited = 1`). Se `P(Churn)` for maior que um determinado limiar (geralmente 0.5), o cliente é classificado como provável churn.\n")
        latex_story.append(NoEscape(r'\nEsta função garante que a saída esteja sempre entre 0 e 1, representando a probabilidade de o cliente pertencer à classe `Churn` (ou seja, \texttt{Exited = 1}). Se $P(\text{Churn})$ for maior que um determinado limiar (geralmente 0.5), o cliente é classificado como provável churn.\n\n'))

        # --- Subseção: Exemplo de Simulação Numérica (AGORA COM DADOS REAIS DA SIMULAÇÃO) ---
        markdown_story.append("### Exemplo de Simulação Numérica com Cliente Simulado")
        latex_story.append(Section(NoEscape(r'Exemplo de Simulação Numérica com Cliente Simulado'), False)) # Sub-subseção não numerada

        if sample_customer_df is not None:
            # Reutilizamos os valores de logit_sample e prob_sample calculados anteriormente para o cliente simulado
            markdown_story.append("Para ilustrar o cálculo, vamos usar as características do cliente simulado acima (ou o último cliente da Previsão Interativa) e os coeficientes do modelo treinado. Note que as características numéricas são **escalonadas** e as categóricas **one-hot encoded** antes de serem multiplicadas pelos coeficientes. \n")
            latex_story.append(NoEscape(r'Para ilustrar o cálculo, vamos usar as características do cliente simulado acima (ou o último cliente da Previsão Interativa) e os coeficientes do modelo treinado. Note que as características numéricas são \textbf{escalonadas} e as categóricas \textbf{one-hot encoded} antes de serem multiplicadas pelos coeficientes. \n\n'))

            markdown_story.append(f"**Características do Cliente 'Simulado':**\n" + sample_display_df.to_markdown(index=False) + "\n")
            latex_story.append(NoEscape(sample_display_df.to_latex(index=False, caption='Características do Cliente Simulado', label='tab:sim_customer_example', longtable=False)))

            # Pré-formatar os valores antes de inseri-los nas f-strings LaTeX
            logit_sample_formatted = f"{logit_sample:.4f}"
            prob_sample_formatted = f"{prob_sample:.4f}"
            prob_sample_percent_formatted = f"{prob_sample:.2%}" # Para exibição em percentual
            
            markdown_story.append("**Passos do Cálculo para o Cliente 'Simulado':**\n")
            markdown_story.append(f"1.  **Calcular o Logit (L):** O Logit é a soma ponderada de todas as características do cliente (já processadas pelo pré-processador do modelo) multiplicadas por seus respectivos coeficientes, mais o intercepto do modelo. Para o cliente simulado, o modelo calculou um Logit de:\n`L = {logit_sample_formatted}`\n")
            
            # LaTeX for Logit calculation
            latex_story.append(NoEscape(r'\textbf{Passos do Cálculo para o Cliente "Simulado":}\n'))
            latex_story.append(NoEscape(r'\begin{enumerate}'))
            latex_story.append(NoEscape(fr'\item \textbf{{Calcular o Logit (L):}} O Logit é a soma ponderada de todas as características do cliente (já processadas pelo pré-processador do modelo) multiplicadas por seus respectivos coeficientes, mais o intercepto do modelo. Para o cliente simulado, o modelo calculou um Logit de:'))
            latex_story.append(Math(data=[NoEscape(r'L = ' + logit_sample_formatted)])) # Alteração aqui!
            
            markdown_story.append(f"2.  **Calcular a Probabilidade de Churn (P) usando a função Sigmoide:** A probabilidade é obtida aplicando-se a função sigmoide ao valor de `L`:\n`P(Churn) = 1 / (1 + e^(-L))`\n`P(Churn) = 1 / (1 + e^(-({logit_sample_formatted})))`\n`P(Churn) = 1 / (1 + e^{{-{logit_sample_formatted}}})`\n`P(Churn) ≈ {prob_sample_formatted}`\n")
            
            # LaTeX for Probability calculation
            latex_story.append(NoEscape(r'\item \textbf{Calcular a Probabilidade de Churn (P) usando a função Sigmoide:} A probabilidade é obtida aplicando-se a função sigmoide ao valor de $L$:'))
            latex_story.append(Math(data=[NoEscape(r'P(\text{Churn}) = \frac{1}{1 + e^{-L}}')])) # Alteração aqui!
            latex_story.append(Math(data=[NoEscape(r'P(\text{Churn}) = \frac{1}{1 + e^{-(' + logit_sample_formatted + r')}}')])) # Alteração aqui!
            latex_story.append(Math(data=[NoEscape(r'P(\text{Churn}) = \frac{1}{1 + e^{-' + logit_sample_formatted + r'}}')])) # Alteração aqui!
            latex_story.append(Math(data=[NoEscape(r'P(\text{Churn}) \approx ' + prob_sample_formatted)])) # Alteração aqui!
            latex_story.append(NoEscape(r'\end{enumerate}\n'))

            markdown_story.append(f"**Resultado da Simulação para o Cliente 'Simulado':**\n")
            markdown_story.append(f"A probabilidade de Churn para este cliente específico é de **{prob_sample_formatted}**, ou seja, **{prob_sample_percent_formatted}**.\n")
            markdown_story.append(f"Este resultado indica que o cliente possui uma probabilidade de churn de {prob_sample_percent_formatted}, guiando a interpretação do risco.\n")
            
            latex_story.append(NoEscape(r'\textbf{Resultado da Simulação para o Cliente "Simulado":}\n'))
            latex_story.append(NoEscape(fr'A probabilidade de Churn para este cliente específico é de \textbf{{{prob_sample_formatted}}}, ou seja, \textbf{{{prob_sample_percent_formatted}}}. '))
            latex_story.append(NoEscape(fr'Este resultado indica que o cliente possui uma probabilidade de churn de {prob_sample_percent_formatted}, guiando a interpretação do risco.\n\n'))
        else:
            markdown_story.append("Não foi possível gerar o exemplo de simulação numérica, pois nenhum cliente simulado foi fornecido.\n")
            latex_story.append(NoEscape(r'Não foi possível gerar o exemplo de simulação numérica, pois nenhum cliente simulado foi fornecido.\n\n'))

        # --- Fim da Subseção de Exemplo ---

        # --- 5. Importância das Variáveis (Coeficientes e Odds Ratio) ---
        markdown_story.append("## 5. Importância das Variáveis (Coeficientes e Odds Ratio)")
        latex_story.append(Section(NoEscape(r'Importância das Variáveis (Coeficientes e Odds Ratio)'), False))
        
        markdown_story.append("A análise dos coeficientes do modelo de Regressão Logística, transformados em Odds Ratios, nos permite entender a influência de cada característica na probabilidade de Churn. Um Odds Ratio maior que 1 indica que o aumento daquela feature (ou pertencer àquela categoria) aumenta as chances de Churn, enquanto um valor menor que 1 diminui.\n")
        latex_story.append(NoEscape(r'A análise dos coeficientes do modelo de Regressão Logística, transformados em Odds Ratios, nos permite entender a influência de cada característica na probabilidade de Churn. Um Odds Ratio maior que 1 indica que o aumento daquela feature (ou pertencer àquela categoria) aumenta as chances de Churn, enquanto um valor menor que 1 diminui.\n\n'))
        
        if not self.coefficients_df.empty:
            markdown_story.append(self.coefficients_df.to_markdown(index=False) + "\n")
            latex_story.append(NoEscape(self.coefficients_df.to_latex(index=False, caption='Coeficientes e Odds Ratios das Variáveis', label='tab:coefficients', longtable=False)))

            # Início das correções para `SyntaxError: f-string: single '}' is not allowed`
            # Linha original de Precisão (equivalente a ~458)
            precision_value = self.metrics_dict.get('Precisão', 0)
            precision_text_latex = (
                f'\item \textbf{{Precisão ({precision_value:.2f}\%):}} ' # Usando f-string para formatar valor e \%
                + r'Das previsões de churn (\texttt{1}), quantos realmente foram churn. '
                r'É importante para o banco não abordar clientes que não iriam dar churn (reduzir falsos positivos). '
                f'Um valor de {precision_value:.2f}\% significa que das vezes que o modelo previu churn, essa porcentagem estava correta.'
            )
            latex_story.append(NoEscape(precision_text_latex + '\n'))

            # Linha original de Recall (equivalente a ~464)
            recall_value = self.metrics_dict.get('Recall (Sensibilidade)', 0)
            recall_text_latex = (
                f'\item \textbf{{Recall (Sensibilidade) ({recall_value:.2f}\%):}} ' # Usando f-string para formatar valor e \%
                + r'Dos clientes que realmente deram churn (\texttt{1}), quantos o modelo identificou. '
                r'É crucial para o banco identificar o máximo de clientes em risco (reduzir falsos negativos). '
                f'Um valor de {recall_value:.2f}\% significa que essa porcentagem de clientes que de fato deram churn foi corretamente identificada pelo modelo.'
            )
            latex_story.append(NoEscape(recall_text_latex + '\n'))
            
            # F1-Score - Ajustando para o novo padrão de concatenação se necessário, ou mantendo f-string se for simples
            f1_value = self.metrics_dict.get('F1-Score', 0)
            f1_text_latex = (
                f'\item \textbf{{F1-Score ({f1_value:.4f})}}: '
                r'É a média harmônica entre Precisão e Recall, útil quando há um desequilíbrio de classes e você precisa de um balanço entre identificar corretamente e não levantar falsos alarmes.'
            )
            latex_story.append(NoEscape(f1_text_latex + '\n'))
            
            latex_story.append(NoEscape(r'\end{itemize}' + '\n\n'))
        else:
            markdown_story.append("Nenhum coeficiente disponível. O modelo pode não ter sido treinado ou não possui coeficientes acessíveis.\n")
            latex_story.append(NoEscape(r'Nenhum dado de avaliação disponível. O modelo pode não ter sido treinado ou avaliado.\n\n'))

        # --- 7. Conclusão e Próximos Passos ---
        markdown_story.append("## 7. Conclusão e Próximos Passos")
        latex_story.append(Section(NoEscape(r'Conclusão e Próximos Passos'), False))
        
        markdown_story.append("O modelo de Regressão Logística provê uma base sólida para a previsão de churn. As variáveis identificadas como mais influentes (pelos Odds Ratios) devem ser o foco para o planejamento estratégico de retenção. Por exemplo, campanhas de marketing direcionadas a grupos de maior risco ou ofertas personalizadas podem ser desenvolvidas com base nas características que aumentam a probabilidade de churn.\nPara aprimoramento contínuo, sugere-se a exploração de outros modelos, engenharia de novas features, e reavaliação periódica do modelo com dados mais recentes.")
        latex_story.append(NoEscape(r'O modelo de Regressão Logística provê uma base sólida para a previsão de churn. As variáveis identificadas como mais influentes ( pelos Odds Ratios) devem ser o foco para o planejamento estratégico de retenção. Por exemplo, campanhas de marketing direcionadas a grupos de maior risco ou ofertas personalizadas podem ser desenvolvidas com base nas características que aumentam a probabilidade de churn.\n\nPara aprimoramento contínuo, sugere-se a exploração de outros modelos, engenharia de novas features, e reavaliação periódica do modelo com dados mais recentes.'))

        return "\n".join(markdown_story), latex_story, self.plot_paths

    def generate_latex_report(self, latex_content_parts: List[Any], header_info: Dict[str, str], plot_paths: Dict[str, str]) -> Union[str, None]:
        """Gera um arquivo LaTeX a partir do conteúdo e paths das imagens."""
        try:
            doc = Document(documentclass='article', document_options=['12pt', 'a4paper'])

            # Pacotes LaTeX
            doc.packages.append(Command('usepackage', 'amsmath'))    # Para equações avançadas
            doc.packages.append(Command('usepackage', 'graphicx'))   # Para incluir imagens
            doc.packages.append(Command('usepackage', 'booktabs'))   # Para tabelas com linhas mais bonitas
            doc.packages.append(Command('usepackage', 'geometry'))   # Para configurar margens
            doc.packages.append(Command('usepackage', 'hyperref'))   # Para links (útil para referências)
            doc.append(Command('geometry', 'margin=1in')) # Margens de 1 polegada
            doc.append(Command('graphicspath', NoEscape(r'{./}'))) # Para imagens no mesmo diretório que o .tex

            # --- Crie um diretório temporário ÚNICO para o .tex e as CÓPIAS das imagens ---
            latex_output_dir = tempfile.mkdtemp()
            output_filename_full_path = os.path.join(latex_output_dir, 'relatorio_churn.tex')

            # --- COPIE os arquivos de plot para este diretório temporário ---
            for key, original_path in plot_paths.items():
                if os.path.exists(original_path):
                    basename = os.path.basename(original_path)
                    new_path_in_latex_dir = os.path.join(latex_output_dir, basename)
                    shutil.copy2(original_path, new_path_in_latex_dir) # Copia o arquivo
                else:
                    print(f"WARNING: Plot file not found at {original_path} for key {key}. It will not be included in LaTeX.")


            doc.append(NoEscape(r'\begin{titlepage}'))
            doc.append(Command('centering'))

            # Logo da UnB
            logo_filename = 'MARCADOR.png' # Nome do arquivo com case-sensitive
            logo_target_path = os.path.join(latex_output_dir, logo_filename)
            if os.path.exists(logo_filename): # Verifica se o original existe
                shutil.copy2(logo_filename, logo_target_path) # Copia o logo para o diretório temporário do LaTeX
                with doc.create(Figure(position='h!')) as logo_fig:
                    # Referencia pelo nome do arquivo, pois está no mesmo diretório do .tex
                    logo_fig.add_image(os.path.basename(logo_target_path), width='0.25\\textwidth')
                    logo_fig.add_caption(NoEscape(r'\vspace{-0.5cm}'))
            else:
                doc.append(Command('textbf', 'AVISO: Logo da UnB não encontrado! Certifique-se de que "MARCADOR.png" esteja na raiz do seu Hugging Face Space.'))

            doc.append(Command('vspace', '0.5cm'))

            # Informações da Universidade
            doc.append(Command('large'))
            doc.append(Command('textbf', header_info["universidade"]))
            doc.append(LineBreak())
            doc.append(header_info["departamento"])
            doc.append(LineBreak())
            doc.append(header_info["programa"])
            doc.append(LineBreak())
            doc.append(header_info["mestrado"])
            doc.append(LineBreak())

            doc.append(Command('vspace', '1.0cm'))

            # Título do Trabalho (do usuário, ajustado para LaTeX)
            latex_title_lines = []
            current_line_parts = []
            words = header_info["titulo_trabalho"].split()

            for word in words:
                if word == 'UTILIZANDO':
                    if current_line_parts:
                        latex_title_lines.append(" ".join(current_line_parts))
                        current_line_parts = []
                    latex_title_lines.append(r'\ \large UTILIZANDO') # LaTeX line break and large font size
                else:
                    current_line_parts.append(word)
            if current_line_parts:
                latex_title_lines.append(" ".join(current_line_parts))

            doc.append(Command('Huge'))
            if latex_title_lines:
                doc.append(Command('textbf', NoEscape(latex_title_lines[0])))
                for line_idx in range(1, len(latex_title_lines)):
                    doc.append(LineBreak())
                    if r'\large' in latex_title_lines[line_idx]: # Check for the large font command
                        doc.append(NoEscape(latex_title_lines[line_idx]))
                    else:
                        doc.append(Command('textbf', NoEscape(latex_title_lines[line_idx])))

            doc.append(Command('vspace', '1.0cm'))

            # Identificação AEDI
            doc.append(Command('large'))
            doc.append(header_info["identificacao_aedi"])
            doc.append(LineBreak())

            doc.append(Command('vspace', '0.5cm'))

            # Informações do Aluno e Professor
            doc.append(header_info["nome_aluno"])
            doc.append(LineBreak())
            doc.append(header_info["matricula_aluno"])
            doc.append(LineBreak())
            doc.append(header_info["nome_professor"])
            doc.append(LineBreak())

            doc.append(Command('normalsize'))
            doc.append(Command('vfill')) # Empurra o conteúdo para cima
            doc.append(Command('end{titlepage}'))
            
            doc.append(Command('clearpage'))
            doc.append(Command('tableofcontents')) # Sumário
            doc.append(Command('clearpage'))
            
            # Conteúdo do Resumo
            for item in latex_content_parts:
                doc.append(item) # pylatex objects (Section, Math, NoEscape) are directly appended
            
            # Adicionar imagens ao final do documento LaTeX
            doc.append(NoEscape(r'\clearpage'))
            doc.append(Section(NoEscape(r'Visualizações Gráficas do Modelo')))
            
            for key, original_plot_path in plot_paths.items(): # Use original_plot_path para referenciar o caminho
                if os.path.exists(original_plot_path):
                    with doc.create(Figure(position='htbp')) as plot_fig:
                        # Referencia pelo nome do arquivo, pois está no mesmo diretório do .tex
                        plot_fig.add_image(os.path.basename(original_plot_path), width='0.8\textwidth')
                        plot_fig.add_caption(NoEscape(f'{key.replace("_", " ").title()}'))
                    doc.append(Command('clearpage')) # Cada imagem em uma nova página
                else:
                    print(f"WARNING: Plot file not found at {original_plot_path} for key {key} when adding to LaTeX. It may have been deleted prematurely.")

            # --- Usar doc.dumps() e salvar manualmente para maior robustez ---
            latex_content_str = doc.dumps()
            with open(output_filename_full_path, 'w', encoding='utf-8') as f:
                f.write(latex_content_str)
            
            # Verificar se o arquivo foi realmente criado e não está vazio
            if os.path.exists(output_filename_full_path) and os.path.getsize(output_filename_full_path) > 0:
                print(f"DEBUG: LaTeX file successfully created at {output_filename_full_path}")
                return output_filename_full_path
            else:
                print(f"CRITICAL ERROR: LaTeX file was NOT created or is empty by pylatex at {output_filename_full_path}.")
                return None
        except Exception as e:
            print(f"ERROR during LaTeX report generation: {e}")
            return None # Retorna None em caso de qualquer erro