hugohonda commited on
Commit
de02cff
·
1 Parent(s): 2c43837
.gitignore ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Jupyter Notebook
24
+ .ipynb_checkpoints/
25
+ *.ipynb_checkpoints
26
+
27
+ # Virtual environments
28
+ venv/
29
+ env/
30
+ ENV/
31
+ .venv
32
+
33
+ # IDE
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ *.swo
38
+ *~
39
+
40
+ # OS
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # Streamlit
45
+ .streamlit/secrets.toml
46
+
47
+ # Cache
48
+ .cache/
49
+ *.cache
50
+
DESCRICAO.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Prova Final de Análise Estatística de Dados e Informações
2
+
3
+ Novembro - 2025
4
+
5
+ Questão 1 – (2,5 pontos)
6
+
7
+ Esta questão aborda a aplicação prática de um problema de Ciência de Dados utilizando Regressão Linear. O objetivo é prever preços de imóveis com base em dados reais da região de King County, nos Estados Unidos. A base de dados utilizada é a Previsão de Vendas de Imóveis em King County (EUA). Siga os passos abaixo para desenvolver sua solução:
8
+
9
+ Instruções:
10
+
11
+ Análise Descritiva dos Dados (20%)
12
+
13
+ Realize uma análise inicial da base de dados.
14
+
15
+ Inclua estatísticas descritivas (média, mediana, desvio padrão, etc.) e gráficos relevantes (distribuições, correlações, etc.).
16
+
17
+ Construção do Modelo de Regressão Linear (30%)
18
+
19
+ Construa um modelo de Regressão Linear para prever os preços dos imóveis.
20
+
21
+ Apresente os coeficientes do modelo, R2 e outras métricas de avaliação.
22
+
23
+ Interpretação dos Resultados (10%)
24
+
25
+ Explique os resultados obtidos pelo modelo, destacando o impacto de cada variável nas previsões e explicações do fenômeno.
26
+
27
+ Verifique se os pressupostos da Regressão Linear (linearidade, homocedasticidade, normalidade dos resíduos, etc.) foram atendidos.
28
+
29
+ Ajustes no Modelo (30%)
30
+
31
+ Identifique possíveis problemas nos pressupostos do modelo.
32
+
33
+ Apresente soluções para corrigir esses problemas, como transformações de variáveis ou ajustes no modelo.
34
+
35
+ Reavalie o desempenho do modelo ajustado.
36
+
37
+ Tomada de Decisão (10%)
38
+
39
+ Com base no modelo final, explique como os resultados podem ser aplicados em um contexto de negócios.
40
+
41
+ Forneça exemplos de decisões estratégicas que poderiam ser tomadas com base nas previsões.
42
+
43
+ Questão 2 – (2,5 pontos)
44
+
45
+ Esta questão aborda a aplicação prática de um problema de Ciência de Dados utilizando Machine Learning. O objetivo é prever se os indivíduos irão cancelar suas reservas em uma rede de hotéis, utilizando o conjunto de dados Hotel Booking Demand. Siga os passos abaixo para desenvolver sua solução:
46
+
47
+ Instruções:
48
+
49
+ a) Análise Descritiva dos Dados (10%)
50
+
51
+ Realize uma análise descritiva da base de dados.
52
+
53
+ Inclua gráficos e tabelas para explorar as características dos dados. b) Modelo de Regressão Logística (60%)
54
+
55
+ Construa um modelo de Regressão Logística para prever o cancelamento das reservas.
56
+
57
+ Apresente as métricas de desempenho do modelo, como acurácia, precisão, recall e F1-score. c) Análise das Features (20%)
58
+
59
+ Identifique as features mais importantes para o cancelamento das reservas.
60
+
61
+ Interprete os resultados, destacando quais variáveis têm maior impacto na previsão. d) Justificativa do Método (10%)
62
+
63
+ Explique por que a Regressão Logística é mais apropriada para este problema em comparação à Regressão Linear.
64
+
65
+ Questão 3 – (2,0 pontos)
66
+
67
+ Esta questão aborda a aplicação prática de um problema de ANOVA (Análise de Variância) utilizando dados reais empregados em contextos empresariais. O objetivo é analisar as médias de quantidades e preços de produtos agrupados por países, utilizando o conjunto de dados Vendas de Varejo Online. Siga os passos abaixo para desenvolver sua solução:
68
+
69
+ Instruções:
70
+
71
+ a) Análise Descritiva dos Dados (10%)
72
+
73
+ Realize uma análise inicial da base de dados.
74
+
75
+ Inclua gráficos e tabelas que explorem as variáveis de interesse. b) Comparação entre Países (ANOVA) (40%)
76
+
77
+ Realize uma análise de variância (ANOVA) para comparar as médias de quantidade e preço dos produtos, agrupados por países.
78
+
79
+ Apresente os resultados estatísticos, incluindo valores de F, p-valor e a interpretação dos mesmos. c) Ajustes no Modelo de ANOVA (40%)
80
+
81
+ Verifique os pressupostos da ANOVA (normalidade, homocedasticidade, etc.).
82
+
83
+ Corrija possíveis problemas identificados e apresente um modelo ajustado. d) Interpretação e Tomada de Decisão (10%)
84
+
85
+ Interprete os resultados finais da análise.
86
+
87
+ Destaque possíveis decisões estratégicas baseadas nos resultados encontrados.
88
+
89
+ Questão 4 – (3,0 pontos)
90
+
91
+ Você é analista de dados de uma instituição financeira. Sua missão é desenvolver um modelo preditivo para identificar clientes com maior probabilidade de se tornarem maus pagadores (inadimplentes). O banco quer usar essas informações para reduzir riscos, melhorar sua carteira de crédito e apoiar decisões estratégicas de concessão de empréstimos.
92
+
93
+ Será utilizada a base de dados Risco de Crédito (Kaggle), que contém informações sociodemográficas, comportamentais e financeiras dos clientes.
94
+
95
+ Variável-alvo: Class – good se o cliente é considerado bom pagador; bad se for mau pagador.
96
+
97
+ Instruções:
98
+
99
+ a) Discussão sobre o problema (10%)
100
+
101
+ Contextualize o problema de risco de crédito no setor bancário e sua importância para a economia.
102
+
103
+ Explique por que prever inadimplência é essencial para reduzir perdas e melhorar a gestão de crédito. b) Análise Descritiva dos Dados (15%)
104
+
105
+ Realize uma análise exploratória da base de dados.
106
+
107
+ Apresente estatísticas descritivas e gráficos para compreender o comportamento das variáveis e sua relação com a variável Class.
108
+
109
+ Faça tratamento de valores ausentes, padronização e codificação de variáveis, se necessário. c) Definição e Seleção dos Modelos (30%)
110
+
111
+ Escolha modelos de previsão adequados para o problema (ex: Regressão Logística, Árvore de Decisão, Random Forest, XGBoost, SVM).
112
+
113
+ Justifique sua escolha com base nas características dos dados e no objetivo da análise.
114
+
115
+ Compare os modelos utilizando métricas como acurácia, precisão, recall, F1-score e AUC. d) Explicabilidade das Variáveis – SHAP value (25%)
116
+
117
+ Utilize SHAP values no modelo final para identificar as variáveis mais relevantes para a previsão de inadimplência.
118
+
119
+ Apresente gráficos interpretativos (ex: summary plot, force plot) e discuta o significado das variáveis mais influentes no contexto bancário e econômico. e) Análise Não Supervisionada com K-Means e DBSCAN (15%)
120
+
121
+ Aplique K-Means para segmentar os clientes com base em características como renda, idade, histórico de crédito, tempo de emprego etc.
122
+
123
+ Justifique o número de clusters e interprete os perfis obtidos.
124
+
125
+ Aplique DBSCAN para detectar perfis atípicos (outliers) que possam indicar risco elevado de inadimplência.
126
+
127
+ Compare os resultados e discuta como os agrupamentos podem complementar a análise supervisionada. f) Tomada de Decisão Estratégica (10%)
128
+
129
+ Com base nos resultados obtidos, sugira ações que o banco poderia adotar para reduzir riscos futuros (ex: políticas de concessão, segmentação de clientes, ações de prevenção).
130
+
131
+ Aponte como a análise de dados pode orientar estratégias de retenção, concessão responsável de crédito e prevenção de inadimplência.
Dockerfile CHANGED
@@ -31,7 +31,10 @@ RUN apt-get purge -y build-essential gcc g++ \
31
  RUN groupadd -r appuser && useradd -r -g appuser appuser
32
 
33
  COPY src/ ./src/
34
- COPY marketing_campaign.csv ./
 
 
 
35
  COPY .streamlit/ ./.streamlit/
36
 
37
  # Set proper permissions
 
31
  RUN groupadd -r appuser && useradd -r -g appuser appuser
32
 
33
  COPY src/ ./src/
34
+ COPY questao-1/ ./questao-1/
35
+ COPY questao-2/ ./questao-2/
36
+ COPY questao-3/ ./questao-3/
37
+ COPY questao-4/ ./questao-4/
38
  COPY .streamlit/ ./.streamlit/
39
 
40
  # Set proper permissions
README.md CHANGED
@@ -1,14 +1,126 @@
1
- ---
2
- title: "Análise de Personalidade Consumidor"
3
- emoji: "🏦"
4
- colorFrom: "blue"
5
- colorTo: "red"
6
- sdk: "docker"
7
- app_file: "Dockerfile"
8
- app_port: 8501
9
- tags:
10
- - streamlit
11
- pinned: false
12
- short_description: "Personalidade Consumidor"
13
- license: mit
14
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prova Final de Análise Estatística de Dados e Informações
2
+
3
+ **Novembro - 2025**
4
+
5
+ - **Autor:** Hugo Honda
6
+ - **Disciplina:** AEDI - PPCA/UnB
7
+
8
+ ## Estrutura do Projeto
9
+
10
+ O projeto está organizado por questões, cada uma em sua própria pasta:
11
+
12
+ ```
13
+ prova-final/
14
+ ├── questao-1/ # Regressão Linear - Preços de Imóveis
15
+ │ ├── questao-1.ipynb
16
+ │ ├── kc_house_data.csv
17
+ │ └── src/
18
+ │ └── streamlit_app.py
19
+ ├── questao-2/ # Regressão Logística - Cancelamento de Reservas
20
+ │ ├── questao-2.ipynb
21
+ │ ├── hotel_bookings.csv
22
+ │ └── src/
23
+ │ └── streamlit_app.py
24
+ ├── questao-3/ # ANOVA - Vendas de Varejo Online
25
+ │ ├── questao-3.ipynb
26
+ │ ├── online_retail_II.xlsx
27
+ │ ├── Year 2009-2010.csv
28
+ │ ├── Year 2010-2011.csv
29
+ │ └── src/
30
+ │ └── streamlit_app.py
31
+ ├── questao-4/ # Risco de Crédito - ML e Clustering
32
+ │ ├── questao-4.ipynb
33
+ │ ├── credit_customers.csv
34
+ │ └── src/
35
+ │ └── streamlit_app.py
36
+ ├── src/
37
+ │ └── streamlit_app.py # App principal que integra todas as questões
38
+ ├── requirements.txt
39
+ ├── Dockerfile
40
+ └── README.md
41
+ ```
42
+
43
+ ## Questões
44
+
45
+ ### Questão 1 - Regressão Linear (2,5 pontos)
46
+ - **Dataset:** King County House Sales (`kc_house_data.csv`)
47
+ - **Objetivo:** Prever preços de imóveis
48
+ - **Técnica:** Regressão Linear
49
+
50
+ ### Questão 2 - Regressão Logística (2,5 pontos)
51
+ - **Dataset:** Hotel Booking Demand (`hotel_bookings.csv`)
52
+ - **Objetivo:** Prever cancelamento de reservas
53
+ - **Técnica:** Regressão Logística
54
+
55
+ ### Questão 3 - ANOVA (2,0 pontos)
56
+ - **Dataset:** Online Retail (`online_retail_II.xlsx` ou CSVs)
57
+ - **Objetivo:** Comparar médias de quantidade e preço por país
58
+ - **Técnica:** ANOVA
59
+
60
+ ### Questão 4 - Risco de Crédito (3,0 pontos)
61
+ - **Dataset:** Credit Risk (`credit_customers.csv`)
62
+ - **Objetivo:** Prever inadimplência
63
+ - **Técnicas:** ML (Logistic Regression, Random Forest, XGBoost), SHAP, K-Means, DBSCAN
64
+
65
+ ## Como Executar
66
+
67
+ ### Localmente
68
+
69
+ #### App Principal (todas as questões)
70
+ ```bash
71
+ pip install -r requirements.txt
72
+ streamlit run src/streamlit_app.py
73
+ ```
74
+
75
+ #### App Individual por Questão
76
+ ```bash
77
+ # Questão 1
78
+ cd questao-1
79
+ streamlit run src/streamlit_app.py
80
+
81
+ # Questão 2
82
+ cd questao-2
83
+ streamlit run src/streamlit_app.py
84
+
85
+ # Questão 3
86
+ cd questao-3
87
+ streamlit run src/streamlit_app.py
88
+
89
+ # Questão 4
90
+ cd questao-4
91
+ streamlit run src/streamlit_app.py
92
+ ```
93
+
94
+ ### Docker
95
+
96
+ ```bash
97
+ docker build -t prova-final .
98
+ docker run -p 8501:8501 prova-final
99
+ ```
100
+
101
+ ### Hugging Face Spaces
102
+
103
+ O projeto está configurado para ser deployado no Hugging Face Spaces usando Docker.
104
+
105
+ ## Notebooks
106
+
107
+ Cada questão possui seu próprio notebook Jupyter:
108
+ - `questao-1/questao-1.ipynb`
109
+ - `questao-2/questao-2.ipynb`
110
+ - `questao-3/questao-3.ipynb`
111
+ - `questao-4/questao-4.ipynb`
112
+
113
+ Os notebooks contêm a análise completa de cada questão e podem ser executados independentemente.
114
+
115
+ ## Dependências
116
+
117
+ Ver `requirements.txt` para lista completa de dependências.
118
+
119
+ Principais bibliotecas:
120
+ - pandas, numpy
121
+ - scikit-learn
122
+ - streamlit, plotly
123
+ - statsmodels, scipy
124
+ - xgboost, shap
125
+ - imbalanced-learn
126
+ - openpyxl (para ler Excel)
Tarefa_6.pdf DELETED
Binary file (53.2 kB)
 
main.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
questao-1/kc_house_data.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0875baa0251b21d4bdc9d2ae940a4fe0bb6009824f23dd0e2a5b2bf04557b7e
3
+ size 2515206
questao-1/questao-1.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
questao-1/src/streamlit_app.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import warnings
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import statsmodels.api as sm
11
+ import streamlit as st
12
+ from scipy import stats
13
+ from sklearn.linear_model import LinearRegression
14
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
15
+ from sklearn.model_selection import train_test_split
16
+ from statsmodels.stats.diagnostic import het_breuschpagan
17
+ from statsmodels.stats.stattools import durbin_watson
18
+
19
+ warnings.filterwarnings("ignore")
20
+
21
+ st.set_page_config(
22
+ page_title="Questão 1 - Regressão Linear",
23
+ page_icon="🏠",
24
+ layout="wide",
25
+ )
26
+
27
+ st.markdown("""
28
+ # Questão 1 – Regressão Linear para Previsão de Preços de Imóveis
29
+
30
+ **King County House Sales Dataset**
31
+
32
+ - **Autor:** Hugo Honda
33
+ - **Disciplina:** AEDI - PPCA/UnB
34
+ - **Data:** Novembro 2025
35
+
36
+ ---
37
+
38
+ ## Objetivos da Análise
39
+
40
+ 1. **Análise Descritiva dos Dados**: Explorar características e distribuições
41
+ 2. **Construção do Modelo**: Desenvolver modelo de regressão linear robusto
42
+ 3. **Interpretação dos Resultados**: Avaliar performance e significância das variáveis
43
+ 4. **Ajustes no Modelo**: Validar pressupostos e otimizar predições
44
+ 5. **Tomada de Decisão**: Fornecer ferramenta de previsão interativa
45
+ """)
46
+
47
+
48
+ @st.cache_data
49
+ def load_data():
50
+ """Carrega e prepara os dados do King County House Sales Dataset"""
51
+ try:
52
+ df = pd.read_csv("kc_house_data.csv")
53
+ df_clean = df.copy()
54
+
55
+ # Remover duplicatas
56
+ df_clean = df_clean.drop_duplicates(subset=["id"])
57
+
58
+ # Remover outliers extremos (1% e 99%)
59
+ Q1 = df_clean["price"].quantile(0.01)
60
+ Q99 = df_clean["price"].quantile(0.99)
61
+ df_clean = df_clean[(df_clean["price"] >= Q1) & (df_clean["price"] <= Q99)]
62
+
63
+ features = [
64
+ "bedrooms",
65
+ "bathrooms",
66
+ "sqft_living",
67
+ "sqft_lot",
68
+ "floors",
69
+ "waterfront",
70
+ "view",
71
+ "condition",
72
+ "grade",
73
+ "sqft_above",
74
+ "sqft_basement",
75
+ "yr_built",
76
+ "yr_renovated",
77
+ "lat",
78
+ "long",
79
+ "sqft_living15",
80
+ "sqft_lot15",
81
+ ]
82
+
83
+ return df_clean, features
84
+ except FileNotFoundError as e:
85
+ st.error(
86
+ f"Erro ao carregar dados: {str(e)}\n\nVerifique se o arquivo kc_house_data.csv está presente."
87
+ )
88
+ return None, None
89
+ except Exception as e:
90
+ st.error(f"Erro inesperado ao carregar dados: {str(e)}")
91
+ return None, None
92
+
93
+
94
+ df_clean, features = load_data()
95
+
96
+ if df_clean is not None:
97
+ # =============================
98
+ # Sidebar - Controles
99
+ # =============================
100
+
101
+ st.sidebar.header("🎛️ Controles da Análise")
102
+
103
+ st.sidebar.subheader("Parâmetros do Modelo")
104
+ test_size = st.sidebar.slider("Tamanho do conjunto de teste:", 0.1, 0.4, 0.2, 0.05)
105
+ random_state = st.sidebar.number_input("Random State:", 1, 1000, 42)
106
+ use_log_transform = st.sidebar.checkbox("Usar transformação logarítmica", True)
107
+
108
+ st.sidebar.subheader("Opções de Visualização")
109
+ show_eda = st.sidebar.checkbox("Mostrar análise exploratória", True)
110
+ show_assumptions = st.sidebar.checkbox("Mostrar validação de pressupostos", True)
111
+ show_predictions = st.sidebar.checkbox("Mostrar gráficos de predição", True)
112
+
113
+ # =============================
114
+ # Análise Exploratória
115
+ # =============================
116
+
117
+ if show_eda:
118
+ st.header("📊 Análise Exploratória dos Dados")
119
+
120
+ col1, col2, col3, col4 = st.columns(4)
121
+ with col1:
122
+ st.metric("Total de Observações", f"{len(df_clean):,}")
123
+ with col2:
124
+ st.metric("Preço Médio", f"${df_clean['price'].mean():,.0f}")
125
+ with col3:
126
+ st.metric("Preço Mínimo", f"${df_clean['price'].min():,.0f}")
127
+ with col4:
128
+ st.metric("Preço Máximo", f"${df_clean['price'].max():,.0f}")
129
+
130
+ # Distribuição de preços
131
+ fig_hist = px.histogram(
132
+ df_clean,
133
+ x="price",
134
+ nbins=50,
135
+ title="Distribuição de Preços dos Imóveis",
136
+ marginal="box",
137
+ labels={"price": "Preço ($)", "count": "Frequência"},
138
+ )
139
+ fig_hist.update_layout(showlegend=False, bargap=0.02)
140
+ st.plotly_chart(fig_hist, use_container_width=True)
141
+
142
+ # Correlações com o preço
143
+ corr_data = (
144
+ df_clean[features + ["price"]]
145
+ .corr()["price"]
146
+ .sort_values(ascending=False)[1:11]
147
+ )
148
+
149
+ fig_corr = px.bar(
150
+ x=corr_data.values,
151
+ y=corr_data.index,
152
+ orientation="h",
153
+ title="Top 10 Variáveis Mais Correlacionadas com o Preço",
154
+ labels={"x": "Correlação", "y": "Variável"},
155
+ color=corr_data.values,
156
+ color_continuous_scale="RdYlGn",
157
+ )
158
+ fig_corr.update_layout(showlegend=False)
159
+ st.plotly_chart(fig_corr, use_container_width=True)
160
+
161
+ # =============================
162
+ # Modelagem
163
+ # =============================
164
+
165
+ st.header("🔬 Modelagem de Regressão Linear")
166
+
167
+ X = df_clean[features].copy()
168
+ y = df_clean["price"].copy()
169
+
170
+ # Divisão treino-teste
171
+ X_train, X_test, y_train, y_test = train_test_split(
172
+ X, y, test_size=test_size, random_state=random_state
173
+ )
174
+
175
+ # Transformação logarítmica se selecionada
176
+ y_train_model = np.log1p(y_train) if use_log_transform else y_train.copy()
177
+ y_test_model = np.log1p(y_test) if use_log_transform else y_test.copy()
178
+ transform_label = " (com transformação logarítmica)" if use_log_transform else ""
179
+
180
+ # Ajustar modelo
181
+ model = LinearRegression()
182
+ model.fit(X_train, y_train_model)
183
+
184
+ # Predições
185
+ y_pred_train = model.predict(X_train)
186
+ y_pred_test = model.predict(X_test)
187
+
188
+ # Reverter transformação se necessário
189
+ y_pred_train_price = np.expm1(y_pred_train) if use_log_transform else y_pred_train.copy()
190
+ y_pred_test_price = np.expm1(y_pred_test) if use_log_transform else y_pred_test.copy()
191
+
192
+ # =============================
193
+ # Resultados do Modelo
194
+ # =============================
195
+
196
+ st.subheader(f"📈 Resultados do Modelo{transform_label}")
197
+
198
+ col1, col2, col3 = st.columns(3)
199
+
200
+ with col1:
201
+ r2_train = r2_score(y_train, y_pred_train_price)
202
+ r2_test = r2_score(y_test, y_pred_test_price)
203
+ st.metric("R² Treino", f"{r2_train:.4f}")
204
+ st.metric("R² Teste", f"{r2_test:.4f}")
205
+
206
+ with col2:
207
+ rmse_train = np.sqrt(mean_squared_error(y_train, y_pred_train_price))
208
+ rmse_test = np.sqrt(mean_squared_error(y_test, y_pred_test_price))
209
+ st.metric("RMSE Treino", f"${rmse_train:,.0f}")
210
+ st.metric("RMSE Teste", f"${rmse_test:,.0f}")
211
+
212
+ with col3:
213
+ mae_train = mean_absolute_error(y_train, y_pred_train_price)
214
+ mae_test = mean_absolute_error(y_test, y_pred_test_price)
215
+ st.metric("MAE Treino", f"${mae_train:,.0f}")
216
+ st.metric("MAE Teste", f"${mae_test:,.0f}")
217
+
218
+ st.info(f"""
219
+ **Interpretação das Métricas:**
220
+ - **R² = {r2_test:.4f}**: O modelo explica {r2_test * 100:.1f}% da variância nos preços
221
+ - **RMSE = ${rmse_test:,.0f}**: Erro médio de previsão de ${rmse_test:,.0f}
222
+ - **MAE = ${mae_test:,.0f}**: Erro absoluto médio de ${mae_test:,.0f}
223
+ - **Transformação**: {"Logarítmica aplicada para normalizar distribuição" if use_log_transform else "Valores originais sem transformação"}
224
+ """)
225
+
226
+ # Coeficientes
227
+ st.subheader("🎯 Importância das Variáveis")
228
+
229
+ coef_df = pd.DataFrame({"Variável": features, "Coeficiente": model.coef_})
230
+ coef_df["Abs_Coefficient"] = np.abs(coef_df["Coeficiente"])
231
+ coef_df = coef_df.sort_values("Abs_Coefficient", ascending=False)
232
+
233
+ top_vars = coef_df.head(15)
234
+
235
+ fig_coef = px.bar(
236
+ top_vars,
237
+ x="Coeficiente",
238
+ y="Variável",
239
+ orientation="h",
240
+ title="Coeficientes das 15 Variáveis Mais Importantes",
241
+ color="Abs_Coefficient",
242
+ color_continuous_scale="Viridis",
243
+ )
244
+ fig_coef.update_layout(height=500)
245
+ st.plotly_chart(fig_coef, use_container_width=True)
246
+
247
+ # Tabela de coeficientes
248
+ with st.expander("📋 Ver todos os coeficientes"):
249
+ st.dataframe(
250
+ coef_df[["Variável", "Coeficiente"]].reset_index(drop=True),
251
+ use_container_width=True,
252
+ )
253
+
254
+ # =============================
255
+ # Validação de Pressupostos
256
+ # =============================
257
+
258
+ if show_assumptions:
259
+ st.header("🔍 Validação dos Pressupostos da Regressão Linear")
260
+
261
+ # Calcular resíduos
262
+ residuals = y_train - y_pred_train_price
263
+
264
+ # Testes estatísticos
265
+ col1, col2 = st.columns(2)
266
+
267
+ with col1:
268
+ st.subheader("📊 Testes Estatísticos")
269
+
270
+ # Homocedasticidade (Breusch-Pagan)
271
+ X_train_sm = sm.add_constant(X_train)
272
+ bp_stat, bp_p, _, _ = het_breuschpagan(residuals, X_train_sm)
273
+ bp_ok = bp_p > 0.05
274
+ st.metric(
275
+ "Homocedasticidade (Breusch-Pagan)",
276
+ "✅ OK" if bp_ok else "❌ Violado",
277
+ f"p-value = {bp_p:.4f}",
278
+ )
279
+
280
+ # Independência (Durbin-Watson)
281
+ dw_stat = durbin_watson(residuals)
282
+ dw_ok = 1.5 < dw_stat < 2.5
283
+ st.metric(
284
+ "Independência (Durbin-Watson)",
285
+ "✅ OK" if dw_ok else "⚠️ Atenção",
286
+ f"DW = {dw_stat:.4f}",
287
+ )
288
+
289
+ # Normalidade (Shapiro-Wilk em amostra)
290
+ if len(residuals) <= 5000:
291
+ sample_residuals = residuals
292
+ else:
293
+ sample_residuals = residuals.sample(5000, random_state=42)
294
+
295
+ shapiro_stat, shapiro_p = stats.shapiro(sample_residuals)
296
+ shapiro_ok = shapiro_p > 0.05
297
+ st.metric(
298
+ "Normalidade dos Resíduos (Shapiro-Wilk)",
299
+ "✅ OK" if shapiro_ok else "❌ Violado",
300
+ f"p-value = {shapiro_p:.4f}",
301
+ )
302
+
303
+ with col2:
304
+ st.subheader("📈 Distribuição dos Resíduos")
305
+
306
+ fig_resid_hist = px.histogram(
307
+ x=residuals,
308
+ nbins=50,
309
+ title="Histograma dos Resíduos",
310
+ labels={"x": "Resíduos", "y": "Frequência"},
311
+ )
312
+ fig_resid_hist.update_layout(showlegend=False, height=300)
313
+ st.plotly_chart(fig_resid_hist, use_container_width=True)
314
+
315
+ # Gráfico de resíduos vs valores ajustados
316
+ fig_resid = px.scatter(
317
+ x=y_pred_train_price,
318
+ y=residuals,
319
+ title="Resíduos vs Valores Preditos (Conjunto de Treino)",
320
+ labels={"x": "Valores Preditos ($)", "y": "Resíduos ($)"},
321
+ opacity=0.5,
322
+ )
323
+ fig_resid.add_hline(
324
+ y=0, line_dash="dash", line_color="red", annotation_text="y=0"
325
+ )
326
+ st.plotly_chart(fig_resid, use_container_width=True)
327
+
328
+ # Q-Q Plot
329
+ fig_qq = go.Figure()
330
+
331
+ (osm, osr), (slope, intercept, r) = stats.probplot(residuals, dist="norm")
332
+
333
+ fig_qq.add_trace(
334
+ go.Scatter(
335
+ x=osm,
336
+ y=osr,
337
+ mode="markers",
338
+ name="Resíduos",
339
+ marker=dict(color="blue", opacity=0.5),
340
+ )
341
+ )
342
+ fig_qq.add_trace(
343
+ go.Scatter(
344
+ x=osm,
345
+ y=slope * osm + intercept,
346
+ mode="lines",
347
+ name="Linha teórica",
348
+ line=dict(color="red", dash="dash"),
349
+ )
350
+ )
351
+ fig_qq.update_layout(
352
+ title="Q-Q Plot - Normalidade dos Resíduos",
353
+ xaxis_title="Quantis Teóricos",
354
+ yaxis_title="Quantis da Amostra",
355
+ )
356
+ st.plotly_chart(fig_qq, use_container_width=True)
357
+
358
+ # Resumo dos pressupostos
359
+ st.subheader("✅ Resumo da Validação")
360
+
361
+ assumptions_status = pd.DataFrame(
362
+ {
363
+ "Pressuposto": [
364
+ "Homocedasticidade",
365
+ "Independência dos Resíduos",
366
+ "Normalidade dos Resíduos",
367
+ ],
368
+ "Status": [
369
+ "✅ Atendido" if bp_ok else "❌ Violado",
370
+ "✅ Atendido" if dw_ok else "⚠️ Atenção",
371
+ "✅ Atendido" if shapiro_ok else "❌ Violado",
372
+ ],
373
+ "Teste": [
374
+ f"Breusch-Pagan (p={bp_p:.4f})",
375
+ f"Durbin-Watson (DW={dw_stat:.4f})",
376
+ f"Shapiro-Wilk (p={shapiro_p:.4f})",
377
+ ],
378
+ }
379
+ )
380
+ st.dataframe(assumptions_status, use_container_width=True)
381
+
382
+ if use_log_transform and (not shapiro_ok or not bp_ok):
383
+ st.success(
384
+ "💡 A transformação logarítmica geralmente melhora a normalidade e homocedasticidade."
385
+ )
386
+ elif not use_log_transform and (not shapiro_ok or not bp_ok):
387
+ st.warning(
388
+ "⚠️ Considere ativar a transformação logarítmica para melhorar os pressupostos."
389
+ )
390
+
391
+ # =============================
392
+ # Gráficos de Predição
393
+ # =============================
394
+
395
+ if show_predictions:
396
+ st.header("🔮 Análise das Predições")
397
+
398
+ # Valores Reais vs Preditos
399
+ fig_pred = px.scatter(
400
+ x=y_test,
401
+ y=y_pred_test_price,
402
+ title="Valores Reais vs Preditos (Conjunto de Teste)",
403
+ labels={"x": "Valores Reais ($)", "y": "Valores Preditos ($)"},
404
+ opacity=0.5,
405
+ )
406
+
407
+ # Linha de referência (y = x)
408
+ min_val = min(y_test.min(), y_pred_test_price.min())
409
+ max_val = max(y_test.max(), y_pred_test_price.max())
410
+ fig_pred.add_trace(
411
+ go.Scatter(
412
+ x=[min_val, max_val],
413
+ y=[min_val, max_val],
414
+ mode="lines",
415
+ name="Predição Perfeita (y=x)",
416
+ line=dict(dash="dash", color="red"),
417
+ )
418
+ )
419
+ st.plotly_chart(fig_pred, use_container_width=True)
420
+
421
+ # Resíduos do teste
422
+ residuals_test = y_test - y_pred_test_price
423
+
424
+ col1, col2 = st.columns(2)
425
+
426
+ with col1:
427
+ fig_resid_test = px.scatter(
428
+ x=y_pred_test_price,
429
+ y=residuals_test,
430
+ title="Resíduos vs Valores Preditos (Teste)",
431
+ labels={"x": "Valores Preditos ($)", "y": "Resíduos ($)"},
432
+ opacity=0.5,
433
+ )
434
+ fig_resid_test.add_hline(y=0, line_dash="dash", line_color="red")
435
+ st.plotly_chart(fig_resid_test, use_container_width=True)
436
+
437
+ with col2:
438
+ fig_resid_hist_test = px.histogram(
439
+ x=residuals_test,
440
+ nbins=50,
441
+ title="Distribuição dos Resíduos (Teste)",
442
+ labels={"x": "Resíduos ($)", "y": "Frequência"},
443
+ )
444
+ fig_resid_hist_test.update_layout(showlegend=False)
445
+ st.plotly_chart(fig_resid_hist_test, use_container_width=True)
446
+
447
+ # =============================
448
+ # Ferramenta de Previsão
449
+ # =============================
450
+
451
+ st.header("🏠 Ferramenta de Previsão de Preço")
452
+
453
+ st.markdown("""
454
+ Use os controles abaixo para inserir as características de um imóvel e obter uma previsão de preço.
455
+ """)
456
+
457
+ col1, col2, col3 = st.columns(3)
458
+
459
+ with col1:
460
+ bedrooms = st.number_input("Quartos", min_value=0, max_value=10, value=3)
461
+ bathrooms = st.number_input(
462
+ "Banheiros", min_value=0.0, max_value=10.0, value=2.5, step=0.5
463
+ )
464
+ sqft_living = st.number_input(
465
+ "Área de Vivência (sqft)", min_value=0, value=2000
466
+ )
467
+ sqft_lot = st.number_input("Área do Lote (sqft)", min_value=0, value=8000)
468
+ floors = st.number_input(
469
+ "Andares", min_value=0.0, max_value=5.0, value=2.0, step=0.5
470
+ )
471
+ waterfront = st.selectbox(
472
+ "Beira-mar", [0, 1], format_func=lambda x: "Sim" if x == 1 else "Não"
473
+ )
474
+
475
+ with col2:
476
+ view = st.number_input("Vista (0-4)", min_value=0, max_value=4, value=2)
477
+ condition = st.number_input("Condição (1-5)", min_value=1, max_value=5, value=3)
478
+ grade = st.number_input("Grau (1-13)", min_value=1, max_value=13, value=7)
479
+ sqft_above = st.number_input(
480
+ "Área Acima do Solo (sqft)", min_value=0, value=1800
481
+ )
482
+ sqft_basement = st.number_input("Área do Porão (sqft)", min_value=0, value=200)
483
+
484
+ with col3:
485
+ yr_built = st.number_input(
486
+ "Ano de Construção", min_value=1900, max_value=2025, value=2000
487
+ )
488
+ yr_renovated = st.number_input(
489
+ "Ano de Renovação", min_value=0, max_value=2025, value=2010
490
+ )
491
+ lat = st.number_input(
492
+ "Latitude", min_value=47.0, max_value=48.0, value=47.5, step=0.01
493
+ )
494
+ long = st.number_input(
495
+ "Longitude", min_value=-123.0, max_value=-121.0, value=-122.3, step=0.01
496
+ )
497
+ sqft_living15 = st.number_input(
498
+ "Área Vivência Vizinhos (sqft)", min_value=0, value=1900
499
+ )
500
+ sqft_lot15 = st.number_input(
501
+ "Área Lote Vizinhos (sqft)", min_value=0, value=7500
502
+ )
503
+
504
+ if st.button("🔮 Prever Preço", type="primary"):
505
+ novo_imovel = pd.DataFrame(
506
+ {
507
+ "bedrooms": [bedrooms],
508
+ "bathrooms": [bathrooms],
509
+ "sqft_living": [sqft_living],
510
+ "sqft_lot": [sqft_lot],
511
+ "floors": [floors],
512
+ "waterfront": [waterfront],
513
+ "view": [view],
514
+ "condition": [condition],
515
+ "grade": [grade],
516
+ "sqft_above": [sqft_above],
517
+ "sqft_basement": [sqft_basement],
518
+ "yr_built": [yr_built],
519
+ "yr_renovated": [yr_renovated],
520
+ "lat": [lat],
521
+ "long": [long],
522
+ "sqft_living15": [sqft_living15],
523
+ "sqft_lot15": [sqft_lot15],
524
+ }
525
+ )
526
+
527
+ previsao = model.predict(novo_imovel[features])
528
+ previsao_price = np.expm1(previsao[0]) if use_log_transform else previsao[0]
529
+
530
+ # Calcular intervalo de confiança aproximado
531
+ margin = rmse_test * 1.96 # 95% de confiança
532
+
533
+ st.success(f"### 🎯 Previsão de Preço: ${previsao_price:,.2f}")
534
+
535
+ col1, col2, col3 = st.columns(3)
536
+ with col1:
537
+ st.metric("Preço Estimado", f"${previsao_price:,.2f}")
538
+ with col2:
539
+ st.metric(
540
+ "Limite Inferior (95%)", f"${max(0, previsao_price - margin):,.2f}"
541
+ )
542
+ with col3:
543
+ st.metric("Limite Superior (95%)", f"${previsao_price + margin:,.2f}")
544
+
545
+ st.info(f"""
546
+ **Interpretação:**
547
+ - O preço estimado é de **${previsao_price:,.2f}**
548
+ - Com 95% de confiança, o preço real está entre **${max(0, previsao_price - margin):,.2f}** e **${previsao_price + margin:,.2f}**
549
+ - O erro médio do modelo (RMSE) é de **${rmse_test:,.0f}**
550
+ """)
551
+
552
+ # =============================
553
+ # Resumo Final
554
+ # =============================
555
+
556
+ st.header("📋 Resumo da Análise")
557
+
558
+ col1, col2 = st.columns(2)
559
+
560
+ with col1:
561
+ st.subheader("🎯 Performance do Modelo")
562
+ performance_df = pd.DataFrame(
563
+ {
564
+ "Métrica": ["R² Score", "RMSE", "MAE"],
565
+ "Treino": [
566
+ f"{r2_train:.4f}",
567
+ f"${rmse_train:,.0f}",
568
+ f"${mae_train:,.0f}",
569
+ ],
570
+ "Teste": [f"{r2_test:.4f}", f"${rmse_test:,.0f}", f"${mae_test:,.0f}"],
571
+ }
572
+ )
573
+ st.dataframe(performance_df, use_container_width=True)
574
+
575
+ with col2:
576
+ st.subheader("✅ Status dos Pressupostos")
577
+ if show_assumptions:
578
+ st.dataframe(assumptions_status, use_container_width=True)
579
+ else:
580
+ st.info(
581
+ "Ative 'Mostrar validação de pressupostos' para ver o status detalhado."
582
+ )
583
+
584
+ st.subheader("🔍 Principais Conclusões")
585
+
586
+ overfit = (r2_train - r2_test) > 0.1
587
+
588
+ conclusions = f"""
589
+ **Modelo de Regressão Linear para Preços de Imóveis:**
590
+
591
+ 1. **Performance**: O modelo explica {r2_test * 100:.1f}% da variância nos preços (R² = {r2_test:.4f})
592
+ 2. **Erro de Previsão**: RMSE de ${rmse_test:,.0f} e MAE de ${mae_test:,.0f}
593
+ 3. **Generalização**: {"⚠️ Possível overfitting detectado" if overfit else "✅ Boa generalização entre treino e teste"}
594
+ 4. **Transformação**: {"Transformação logarítmica aplicada melhorou a distribuição" if use_log_transform else "Modelo em escala original"}
595
+ 5. **Variáveis Importantes**: {", ".join(top_vars.head(3)["Variável"].tolist())}
596
+ """
597
+
598
+ if show_assumptions:
599
+ all_ok = bp_ok and dw_ok and shapiro_ok
600
+ conclusions += f"\n6. **Pressupostos**: {'✅ Todos os pressupostos foram atendidos' if all_ok else '⚠️ Alguns pressupostos foram violados'}"
601
+
602
+ st.info(conclusions)
603
+
604
+ else:
605
+ st.error(
606
+ "❌ Erro ao carregar os dados. Verifique se o arquivo kc_house_data.csv está presente no diretório."
607
+ )
608
+
609
+ # =============================
610
+ # Footer
611
+ # =============================
612
+
613
+ st.markdown("---")
614
+ st.caption("PPCA/UnB | Novembro 2025")
questao-2/hotel_bookings.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c2ae42a7353905ea136e5c2287f17c92c5435826598bfbb8491c6f0c7b1fc06
3
+ size 16855599
questao-2/questao-2.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
questao-2/src/streamlit_app.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import warnings
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import streamlit as st
11
+ from imblearn.over_sampling import SMOTE
12
+ from sklearn.linear_model import LogisticRegression
13
+ from sklearn.metrics import (
14
+ accuracy_score,
15
+ classification_report,
16
+ confusion_matrix,
17
+ f1_score,
18
+ precision_score,
19
+ recall_score,
20
+ roc_auc_score,
21
+ roc_curve,
22
+ )
23
+ from sklearn.model_selection import train_test_split
24
+ from sklearn.preprocessing import LabelEncoder, StandardScaler
25
+
26
+ warnings.filterwarnings("ignore")
27
+
28
+ st.set_page_config(
29
+ page_title="Questão 2 - Regressão Logística",
30
+ page_icon="🏨",
31
+ layout="wide",
32
+ )
33
+
34
+ st.markdown("""
35
+ # Questão 2 – Regressão Logística para Previsão de Cancelamento de Reservas
36
+
37
+ **Hotel Booking Demand Dataset**
38
+
39
+ - **Autor:** Hugo Honda
40
+ - **Disciplina:** AEDI - PPCA/UnB
41
+ - **Data:** Novembro 2025
42
+
43
+ ---
44
+
45
+ ## Objetivos da Análise
46
+
47
+ 1. **Análise Descritiva dos Dados**: Explorar padrões de cancelamento
48
+ 2. **Modelo de Regressão Logística**: Construir modelo preditivo robusto
49
+ 3. **Análise das Features**: Identificar principais fatores de cancelamento
50
+ 4. **Justificativa do Método**: Avaliar adequação da regressão logística
51
+ """)
52
+
53
+
54
+ @st.cache_data
55
+ def load_data():
56
+ """Carrega e prepara os dados do Hotel Booking Demand Dataset"""
57
+ try:
58
+ df = pd.read_csv("hotel_bookings.csv")
59
+
60
+ # Tratar valores ausentes
61
+ numeric_cols = df.select_dtypes(include=[np.number]).columns
62
+ for col in numeric_cols:
63
+ if df[col].isnull().sum() > 0:
64
+ df[col].fillna(df[col].median(), inplace=True)
65
+
66
+ categorical_cols = df.select_dtypes(include=["object"]).columns
67
+ for col in categorical_cols:
68
+ if df[col].isnull().sum() > 0:
69
+ df[col].fillna(df[col].mode()[0], inplace=True)
70
+
71
+ # Features do dataset
72
+ numeric_features = [
73
+ "lead_time",
74
+ "arrival_date_week_number",
75
+ "arrival_date_day_of_month",
76
+ "stays_in_weekend_nights",
77
+ "stays_in_week_nights",
78
+ "adults",
79
+ "children",
80
+ "babies",
81
+ "previous_cancellations",
82
+ "previous_bookings_not_canceled",
83
+ "booking_changes",
84
+ "days_in_waiting_list",
85
+ "adr",
86
+ "required_car_parking_spaces",
87
+ "total_of_special_requests",
88
+ ]
89
+
90
+ categorical_features = [
91
+ "hotel",
92
+ "arrival_date_month",
93
+ "meal",
94
+ "country",
95
+ "market_segment",
96
+ "distribution_channel",
97
+ "reserved_room_type",
98
+ "assigned_room_type",
99
+ "deposit_type",
100
+ "customer_type",
101
+ ]
102
+
103
+ return df, numeric_features, categorical_features
104
+ except FileNotFoundError as e:
105
+ st.error(
106
+ f"Erro ao carregar dados: {str(e)}\n\nVerifique se o arquivo hotel_bookings.csv está presente."
107
+ )
108
+ return None, None, None
109
+ except Exception as e:
110
+ st.error(f"Erro inesperado ao carregar dados: {str(e)}")
111
+ return None, None, None
112
+
113
+
114
+ df, numeric_features, categorical_features = load_data()
115
+ target_col = "is_canceled"
116
+
117
+ if df is not None:
118
+ # =============================
119
+ # Sidebar - Controles
120
+ # =============================
121
+
122
+ st.sidebar.header("🎛️ Controles da Análise")
123
+
124
+ st.sidebar.subheader("Parâmetros do Modelo")
125
+ test_size = st.sidebar.slider("Tamanho do conjunto de teste:", 0.1, 0.4, 0.2, 0.05)
126
+ random_state = st.sidebar.number_input("Random State:", 1, 1000, 42)
127
+ use_smote = st.sidebar.checkbox("Usar SMOTE para balanceamento", True)
128
+ max_iter = st.sidebar.number_input("Máximo de iterações:", 100, 2000, 1000, 100)
129
+
130
+ st.sidebar.subheader("Opções de Visualização")
131
+ show_eda = st.sidebar.checkbox("Mostrar análise exploratória", True)
132
+ show_confusion = st.sidebar.checkbox("Mostrar matriz de confusão", True)
133
+ show_roc = st.sidebar.checkbox("Mostrar curva ROC", True)
134
+ show_feature_importance = st.sidebar.checkbox(
135
+ "Mostrar importância das features", True
136
+ )
137
+
138
+ # =============================
139
+ # Análise Exploratória
140
+ # =============================
141
+
142
+ if show_eda:
143
+ st.header("📊 Análise Descritiva dos Dados")
144
+
145
+ col1, col2, col3, col4 = st.columns(4)
146
+ with col1:
147
+ st.metric("Total de Observações", f"{len(df):,}")
148
+ with col2:
149
+ cancel_rate = df[target_col].mean()
150
+ st.metric("Taxa de Cancelamento", f"{cancel_rate:.2%}")
151
+ with col3:
152
+ st.metric("Features Numéricas", len(numeric_features))
153
+ with col4:
154
+ st.metric("Features Categóricas", len(categorical_features))
155
+
156
+ # Distribuição da variável target
157
+ target_counts = df[target_col].value_counts()
158
+
159
+ fig_target = go.Figure(
160
+ data=[
161
+ go.Pie(
162
+ labels=["Não Cancelado", "Cancelado"],
163
+ values=target_counts.values,
164
+ hole=0.4,
165
+ marker_colors=["#00CC96", "#EF553B"],
166
+ )
167
+ ]
168
+ )
169
+ fig_target.update_layout(title="Distribuição de Cancelamentos")
170
+ st.plotly_chart(fig_target, use_container_width=True)
171
+
172
+ # Análise de cancelamento por características
173
+ col1, col2 = st.columns(2)
174
+
175
+ with col1:
176
+ cancel_by_hotel = (
177
+ df.groupby("hotel")[target_col].mean().sort_values(ascending=False)
178
+ )
179
+ fig_hotel = px.bar(
180
+ x=cancel_by_hotel.index,
181
+ y=cancel_by_hotel.values,
182
+ title="Taxa de Cancelamento por Tipo de Hotel",
183
+ labels={"x": "Tipo de Hotel", "y": "Taxa de Cancelamento"},
184
+ color=cancel_by_hotel.values,
185
+ color_continuous_scale="RdYlGn_r",
186
+ )
187
+ st.plotly_chart(fig_hotel, use_container_width=True)
188
+
189
+ with col2:
190
+ cancel_by_deposit = (
191
+ df.groupby("deposit_type")[target_col]
192
+ .mean()
193
+ .sort_values(ascending=False)
194
+ )
195
+ fig_deposit = px.bar(
196
+ x=cancel_by_deposit.index,
197
+ y=cancel_by_deposit.values,
198
+ title="Taxa de Cancelamento por Tipo de Depósito",
199
+ labels={"x": "Tipo de Depósito", "y": "Taxa de Cancelamento"},
200
+ color=cancel_by_deposit.values,
201
+ color_continuous_scale="RdYlGn_r",
202
+ )
203
+ st.plotly_chart(fig_deposit, use_container_width=True)
204
+
205
+ # Distribuição de lead_time
206
+ fig_lead = px.box(
207
+ df,
208
+ x=target_col,
209
+ y="lead_time",
210
+ title="Distribuição de Lead Time por Status de Cancelamento",
211
+ labels={target_col: "Cancelado", "lead_time": "Lead Time (dias)"},
212
+ color=target_col,
213
+ color_discrete_map={0: "#00CC96", 1: "#EF553B"},
214
+ )
215
+ st.plotly_chart(fig_lead, use_container_width=True)
216
+
217
+ # =============================
218
+ # Modelagem
219
+ # =============================
220
+
221
+ st.header("🔬 Modelo de Regressão Logística")
222
+
223
+ # Preparar dados
224
+ df_model = df.copy()
225
+ le_dict = {}
226
+
227
+ for col in categorical_features:
228
+ le = LabelEncoder()
229
+ df_model[col] = le.fit_transform(df_model[col].astype(str))
230
+ le_dict[col] = le
231
+
232
+ all_features = numeric_features + categorical_features
233
+ X = df_model[all_features].copy()
234
+ y = df_model[target_col].copy()
235
+
236
+ # Tratar valores infinitos
237
+ X = X.replace([np.inf, -np.inf], np.nan).fillna(X.median())
238
+
239
+ # Divisão treino-teste
240
+ X_train, X_test, y_train, y_test = train_test_split(
241
+ X, y, test_size=test_size, random_state=random_state, stratify=y
242
+ )
243
+
244
+ # Normalização
245
+ scaler = StandardScaler()
246
+ X_train_scaled = scaler.fit_transform(X_train)
247
+ X_test_scaled = scaler.transform(X_test)
248
+
249
+ # Balanceamento com SMOTE
250
+ if use_smote:
251
+ smote = SMOTE(random_state=random_state)
252
+ X_train_balanced, y_train_balanced = smote.fit_resample(X_train_scaled, y_train)
253
+ st.info(f"✅ SMOTE aplicado: {len(X_train_scaled)} → {len(X_train_balanced)} amostras de treino")
254
+ else:
255
+ X_train_balanced, y_train_balanced = X_train_scaled, y_train
256
+
257
+ # Treinar modelo
258
+ model = LogisticRegression(
259
+ random_state=random_state,
260
+ max_iter=max_iter,
261
+ class_weight="balanced",
262
+ solver="lbfgs",
263
+ )
264
+ model.fit(X_train_balanced, y_train_balanced)
265
+
266
+ # Predições
267
+ y_pred_train = model.predict(X_train_scaled)
268
+ y_pred_test = model.predict(X_test_scaled)
269
+ y_pred_proba_train = model.predict_proba(X_train_scaled)[:, 1]
270
+ y_pred_proba_test = model.predict_proba(X_test_scaled)[:, 1]
271
+
272
+ # =============================
273
+ # Métricas de Desempenho
274
+ # =============================
275
+
276
+ st.subheader("📈 Métricas de Desempenho")
277
+
278
+ col1, col2, col3, col4 = st.columns(4)
279
+
280
+ with col1:
281
+ accuracy_train = accuracy_score(y_train, y_pred_train)
282
+ accuracy_test = accuracy_score(y_test, y_pred_test)
283
+ st.metric("Acurácia Treino", f"{accuracy_train:.4f}")
284
+ st.metric("Acurácia Teste", f"{accuracy_test:.4f}")
285
+
286
+ with col2:
287
+ precision_train = precision_score(y_train, y_pred_train, zero_division=0)
288
+ precision_test = precision_score(y_test, y_pred_test, zero_division=0)
289
+ st.metric("Precisão Treino", f"{precision_train:.4f}")
290
+ st.metric("Precisão Teste", f"{precision_test:.4f}")
291
+
292
+ with col3:
293
+ recall_train = recall_score(y_train, y_pred_train, zero_division=0)
294
+ recall_test = recall_score(y_test, y_pred_test, zero_division=0)
295
+ st.metric("Recall Treino", f"{recall_train:.4f}")
296
+ st.metric("Recall Teste", f"{recall_test:.4f}")
297
+
298
+ with col4:
299
+ f1_train = f1_score(y_train, y_pred_train, zero_division=0)
300
+ f1_test = f1_score(y_test, y_pred_test, zero_division=0)
301
+ roc_auc_train = roc_auc_score(y_train, y_pred_proba_train)
302
+ roc_auc_test = roc_auc_score(y_test, y_pred_proba_test)
303
+ st.metric("F1-Score Treino", f"{f1_train:.4f}")
304
+ st.metric("F1-Score Teste", f"{f1_test:.4f}")
305
+ st.metric("AUC-ROC Teste", f"{roc_auc_test:.4f}")
306
+
307
+ st.info(f"""
308
+ **Interpretação das Métricas:**
309
+ - **Acurácia = {accuracy_test:.2%}**: Percentual de predições corretas
310
+ - **Precisão = {precision_test:.2%}**: {precision_test * 100:.1f}% dos cancelamentos previstos são reais
311
+ - **Recall = {recall_test:.2%}**: {recall_test * 100:.1f}% dos cancelamentos reais foram identificados
312
+ - **F1-Score = {f1_test:.4f}**: Média harmônica entre precisão e recall
313
+ - **AUC-ROC = {roc_auc_test:.4f}**: Capacidade do modelo de discriminar entre classes
314
+ """)
315
+
316
+ # =============================
317
+ # Matriz de Confusão
318
+ # =============================
319
+
320
+ if show_confusion:
321
+ st.subheader("📊 Matriz de Confusão")
322
+
323
+ col1, col2 = st.columns(2)
324
+
325
+ with col1:
326
+ cm_train = confusion_matrix(y_train, y_pred_train)
327
+ fig_cm_train = px.imshow(
328
+ cm_train,
329
+ labels=dict(x="Predito", y="Real", color="Quantidade"),
330
+ x=["Não Cancelado", "Cancelado"],
331
+ y=["Não Cancelado", "Cancelado"],
332
+ text_auto=True,
333
+ title="Matriz de Confusão - Treino",
334
+ color_continuous_scale="Blues",
335
+ )
336
+ st.plotly_chart(fig_cm_train, use_container_width=True)
337
+
338
+ with col2:
339
+ cm_test = confusion_matrix(y_test, y_pred_test)
340
+ fig_cm_test = px.imshow(
341
+ cm_test,
342
+ labels=dict(x="Predito", y="Real", color="Quantidade"),
343
+ x=["Não Cancelado", "Cancelado"],
344
+ y=["Não Cancelado", "Cancelado"],
345
+ text_auto=True,
346
+ title="Matriz de Confusão - Teste",
347
+ color_continuous_scale="Blues",
348
+ )
349
+ st.plotly_chart(fig_cm_test, use_container_width=True)
350
+
351
+ # Métricas da matriz de confusão
352
+ tn, fp, fn, tp = cm_test.ravel()
353
+
354
+ col1, col2, col3, col4 = st.columns(4)
355
+ with col1:
356
+ st.metric("Verdadeiros Positivos", tp)
357
+ with col2:
358
+ st.metric("Verdadeiros Negativos", tn)
359
+ with col3:
360
+ st.metric("Falsos Positivos", fp)
361
+ with col4:
362
+ st.metric("Falsos Negativos", fn)
363
+
364
+ # =============================
365
+ # Curva ROC
366
+ # =============================
367
+
368
+ if show_roc:
369
+ st.subheader("📈 Curva ROC")
370
+
371
+ fpr_train, tpr_train, _ = roc_curve(y_train, y_pred_proba_train)
372
+ fpr_test, tpr_test, _ = roc_curve(y_test, y_pred_proba_test)
373
+
374
+ fig_roc = go.Figure()
375
+
376
+ fig_roc.add_trace(
377
+ go.Scatter(
378
+ x=fpr_train,
379
+ y=tpr_train,
380
+ mode="lines",
381
+ name=f"Treino (AUC = {roc_auc_train:.4f})",
382
+ line=dict(color="blue", width=2),
383
+ )
384
+ )
385
+
386
+ fig_roc.add_trace(
387
+ go.Scatter(
388
+ x=fpr_test,
389
+ y=tpr_test,
390
+ mode="lines",
391
+ name=f"Teste (AUC = {roc_auc_test:.4f})",
392
+ line=dict(color="green", width=2),
393
+ )
394
+ )
395
+
396
+ fig_roc.add_trace(
397
+ go.Scatter(
398
+ x=[0, 1],
399
+ y=[0, 1],
400
+ mode="lines",
401
+ name="Aleatório",
402
+ line=dict(color="red", width=2, dash="dash"),
403
+ )
404
+ )
405
+
406
+ fig_roc.update_layout(
407
+ title="Curva ROC - Receiver Operating Characteristic",
408
+ xaxis_title="Taxa de Falsos Positivos (FPR)",
409
+ yaxis_title="Taxa de Verdadeiros Positivos (TPR)",
410
+ width=800,
411
+ height=600,
412
+ )
413
+
414
+ st.plotly_chart(fig_roc, use_container_width=True)
415
+
416
+ st.info(f"""
417
+ **Interpretação da Curva ROC:**
418
+ - **AUC = {roc_auc_test:.4f}**: Quanto mais próximo de 1.0, melhor o modelo
419
+ - AUC > 0.9: Excelente
420
+ - AUC > 0.8: Muito Bom
421
+ - AUC > 0.7: Bom
422
+ - AUC > 0.6: Razoável
423
+ - AUC ≈ 0.5: Modelo aleatório
424
+ """)
425
+
426
+ # =============================
427
+ # Análise de Features
428
+ # =============================
429
+
430
+ if show_feature_importance:
431
+ st.header("🎯 Análise das Features")
432
+
433
+ st.subheader("Importância das Features (Coeficientes)")
434
+
435
+ feature_importance = pd.DataFrame(
436
+ {
437
+ "Feature": all_features,
438
+ "Coefficient": model.coef_[0],
439
+ "Abs_Coefficient": np.abs(model.coef_[0]),
440
+ }
441
+ ).sort_values("Abs_Coefficient", ascending=False)
442
+
443
+ top_features = feature_importance.head(20)
444
+
445
+ fig_coef = px.bar(
446
+ top_features,
447
+ x="Coefficient",
448
+ y="Feature",
449
+ orientation="h",
450
+ title="Top 20 Features Mais Importantes",
451
+ labels={"Coefficient": "Coeficiente", "Feature": "Variável"},
452
+ color="Coefficient",
453
+ color_continuous_scale="RdBu_r",
454
+ )
455
+ fig_coef.update_layout(height=600)
456
+ st.plotly_chart(fig_coef, use_container_width=True)
457
+
458
+ st.info("""
459
+ **Interpretação dos Coeficientes:**
460
+ - **Coeficientes positivos**: Aumentam a probabilidade de cancelamento
461
+ - **Coeficientes negativos**: Diminuem a probabilidade de cancelamento
462
+ - **Magnitude**: Quanto maior o valor absoluto, maior o impacto na predição
463
+ """)
464
+
465
+ # Tabela com todas as features
466
+ with st.expander("📋 Ver todas as features e coeficientes"):
467
+ display_df = feature_importance[["Feature", "Coefficient"]].reset_index(
468
+ drop=True
469
+ )
470
+ st.dataframe(display_df, use_container_width=True)
471
+
472
+ # Top features positivas e negativas
473
+ col1, col2 = st.columns(2)
474
+
475
+ with col1:
476
+ st.subheader("🔴 Top 5 Features que Aumentam Cancelamento")
477
+ top_positive = feature_importance.nlargest(5, "Coefficient")[
478
+ ["Feature", "Coefficient"]
479
+ ]
480
+ st.dataframe(top_positive.reset_index(drop=True), use_container_width=True)
481
+
482
+ with col2:
483
+ st.subheader("🟢 Top 5 Features que Diminuem Cancelamento")
484
+ top_negative = feature_importance.nsmallest(5, "Coefficient")[
485
+ ["Feature", "Coefficient"]
486
+ ]
487
+ st.dataframe(top_negative.reset_index(drop=True), use_container_width=True)
488
+
489
+ # =============================
490
+ # Justificativa do Método
491
+ # =============================
492
+
493
+ st.header("📝 Justificativa do Método")
494
+
495
+ st.markdown("""
496
+ ### Por que Regressão Logística?
497
+
498
+ A **Regressão Logística** é adequada para este problema pelos seguintes motivos:
499
+
500
+ 1. **Problema de Classificação Binária**:
501
+ - Variável target é binária (cancelado/não cancelado)
502
+ - Regressão logística é especificamente desenhada para este tipo de problema
503
+
504
+ 2. **Interpretabilidade**:
505
+ - Coeficientes representam odds ratios
506
+ - Fácil identificar fatores de risco
507
+ - Importante para decisões de negócio
508
+
509
+ 3. **Probabilidades Calibradas**:
510
+ - Fornece probabilidades diretas de cancelamento
511
+ - Útil para ranking de risco e tomada de decisão
512
+
513
+ 4. **Performance**:
514
+ - Modelo simples e eficiente
515
+ - Baixo risco de overfitting
516
+ - Rápido para treinar e fazer predições
517
+
518
+ 5. **Requisitos Regulatórios**:
519
+ - Modelos interpretáveis são preferidos em contextos de negócio
520
+ - Facilita explicação para stakeholders
521
+
522
+ ### Limitações e Considerações
523
+
524
+ - Assume relação linear entre log-odds e features
525
+ - Pode não capturar interações complexas (considerar árvores/ensembles)
526
+ - Requer balanceamento de classes (SMOTE aplicado)
527
+ """)
528
+
529
+ # =============================
530
+ # Relatório de Classificação
531
+ # =============================
532
+
533
+ st.subheader("📋 Relatório de Classificação Detalhado")
534
+
535
+ report = classification_report(y_test, y_pred_test, output_dict=True)
536
+ report_df = pd.DataFrame(report).transpose()
537
+
538
+ st.dataframe(report_df.style.format("{:.4f}"), use_container_width=True)
539
+
540
+ # =============================
541
+ # Resumo Final
542
+ # =============================
543
+
544
+ st.header("📋 Resumo da Análise")
545
+
546
+ col1, col2 = st.columns(2)
547
+
548
+ with col1:
549
+ st.subheader("🎯 Performance do Modelo")
550
+ performance_df = pd.DataFrame(
551
+ {
552
+ "Métrica": ["Acurácia", "Precisão", "Recall", "F1-Score", "AUC-ROC"],
553
+ "Treino": [
554
+ f"{accuracy_train:.4f}",
555
+ f"{precision_train:.4f}",
556
+ f"{recall_train:.4f}",
557
+ f"{f1_train:.4f}",
558
+ f"{roc_auc_train:.4f}",
559
+ ],
560
+ "Teste": [
561
+ f"{accuracy_test:.4f}",
562
+ f"{precision_test:.4f}",
563
+ f"{recall_test:.4f}",
564
+ f"{f1_test:.4f}",
565
+ f"{roc_auc_test:.4f}",
566
+ ],
567
+ }
568
+ )
569
+ st.dataframe(performance_df, use_container_width=True)
570
+
571
+ with col2:
572
+ st.subheader("✅ Informações do Modelo")
573
+ model_info = pd.DataFrame(
574
+ {
575
+ "Característica": [
576
+ "Total de Features",
577
+ "Amostras de Treino",
578
+ "Amostras de Teste",
579
+ "Balanceamento",
580
+ "Normalização",
581
+ ],
582
+ "Valor": [
583
+ len(all_features),
584
+ len(X_train),
585
+ len(X_test),
586
+ "SMOTE Aplicado" if use_smote else "Não Aplicado",
587
+ "StandardScaler",
588
+ ],
589
+ }
590
+ )
591
+ st.dataframe(model_info, use_container_width=True)
592
+
593
+ st.subheader("🔍 Principais Conclusões")
594
+
595
+ overfit = (accuracy_train - accuracy_test) > 0.1
596
+ good_performance = roc_auc_test >= 0.7
597
+
598
+ conclusions = f"""
599
+ **Modelo de Regressão Logística para Cancelamento de Reservas:**
600
+
601
+ 1. **Performance Geral**: {"✅ Bom" if good_performance else "⚠️ Requer melhorias"} (AUC-ROC = {roc_auc_test:.4f})
602
+ 2. **Acurácia**: {accuracy_test:.2%} das predições corretas
603
+ 3. **Precisão**: {precision_test:.2%} dos cancelamentos previstos são verdadeiros
604
+ 4. **Recall**: {recall_test:.2%} dos cancelamentos reais foram identificados
605
+ 5. **Generalização**: {"⚠️ Possível overfitting detectado" if overfit else "✅ Boa generalização entre treino e teste"}
606
+ 6. **Features Principais**: {", ".join(top_features.head(3)["Feature"].tolist())}
607
+ 7. **Balanceamento**: {"✅ SMOTE aplicado com sucesso" if use_smote else "Sem balanceamento"}
608
+
609
+ **Recomendações:**
610
+ - {"✅ Modelo pronto para produção" if good_performance and not overfit else "⚠️ Considerar ajustes adicionais"}
611
+ - Monitorar performance em dados novos
612
+ - Atualizar modelo periodicamente
613
+ """
614
+
615
+ st.success(conclusions)
616
+
617
+ else:
618
+ st.error(
619
+ "❌ Erro ao carregar os dados. Verifique se o arquivo hotel_bookings.csv está presente no diretório."
620
+ )
621
+
622
+ # =============================
623
+ # Footer
624
+ # =============================
625
+
626
+ st.markdown("---")
627
+ st.caption("PPCA/UnB | Novembro 2025")
questao-3/online_retail_II.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a000db4167982e3f929cc7c2051a8fd5969944c1a4986d14c88a7b03eb9e326
3
+ size 45628546
questao-3/questao-3.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
questao-3/src/streamlit_app.py ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import warnings
5
+
6
+ import pandas as pd
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+ import statsmodels.formula.api as smf
10
+ import streamlit as st
11
+ from scipy import stats
12
+ from statsmodels.stats.anova import anova_lm
13
+ from statsmodels.stats.stattools import durbin_watson
14
+
15
+ warnings.filterwarnings("ignore")
16
+
17
+ st.set_page_config(
18
+ page_title="Questão 3 - ANOVA",
19
+ page_icon="📊",
20
+ layout="wide",
21
+ )
22
+
23
+ st.markdown("""
24
+ # Questão 3 – ANOVA para Análise de Vendas de Varejo Online
25
+
26
+ **Online Retail Dataset**
27
+
28
+ - **Autor:** Hugo Honda
29
+ - **Disciplina:** AEDI - PPCA/UnB
30
+ - **Data:** Novembro 2025
31
+
32
+ ---
33
+
34
+ ## Objetivos da Análise
35
+
36
+ 1. **Análise Descritiva dos Dados**: Explorar padrões de vendas por país
37
+ 2. **Comparação entre Países (ANOVA)**: Testar diferenças significativas nas médias
38
+ 3. **Ajustes no Modelo de ANOVA**: Validar pressupostos estatísticos
39
+ 4. **Interpretação e Tomada de Decisão**: Fornecer insights para estratégia de negócio
40
+ """)
41
+
42
+
43
+ @st.cache_data
44
+ def load_data():
45
+ """Carrega e prepara os dados do Online Retail Dataset"""
46
+ try:
47
+ df = pd.read_excel("online_retail_II.xlsx")
48
+
49
+ # Limpeza de dados
50
+ df_clean = df.dropna(subset=["Country", "Quantity", "Price"])
51
+ df_clean = df_clean[df_clean["Quantity"] > 0]
52
+ df_clean = df_clean[df_clean["Price"] > 0]
53
+
54
+ # Criar coluna de receita
55
+ df_clean["Revenue"] = df_clean["Quantity"] * df_clean["Price"]
56
+
57
+ # Filtrar países com número mínimo de observações
58
+ country_counts = df_clean["Country"].value_counts()
59
+ min_observations = 100
60
+ valid_countries = country_counts[country_counts >= min_observations].index
61
+ df_clean = df_clean[df_clean["Country"].isin(valid_countries)]
62
+
63
+ return df_clean
64
+ except FileNotFoundError as e:
65
+ st.error(
66
+ f"Erro ao carregar dados: {str(e)}\n\nVerifique se o arquivo online_retail_II.xlsx está presente."
67
+ )
68
+ return None
69
+ except Exception as e:
70
+ st.error(f"Erro inesperado ao carregar dados: {str(e)}")
71
+ return None
72
+
73
+
74
+ df_clean = load_data()
75
+ country_col, quantity_col, price_col = "Country", "Quantity", "Price"
76
+
77
+ if df_clean is not None:
78
+ # =============================
79
+ # Sidebar - Controles
80
+ # =============================
81
+
82
+ st.sidebar.header("🎛️ Controles da Análise")
83
+
84
+ st.sidebar.subheader("Parâmetros da Análise")
85
+ top_n_countries = st.sidebar.slider("Número de países para análise:", 3, 15, 8)
86
+ alpha = st.sidebar.slider("Nível de significância (α):", 0.01, 0.10, 0.05, 0.01)
87
+
88
+ st.sidebar.subheader("Opções de Visualização")
89
+ show_eda = st.sidebar.checkbox("Mostrar análise exploratória", True)
90
+ show_assumptions = st.sidebar.checkbox("Mostrar validação de pressupostos", True)
91
+ show_detailed_anova = st.sidebar.checkbox("Mostrar análise detalhada", True)
92
+
93
+ # =============================
94
+ # Análise Exploratória
95
+ # =============================
96
+
97
+ if show_eda:
98
+ st.header("📊 Análise Descritiva dos Dados")
99
+
100
+ col1, col2, col3, col4 = st.columns(4)
101
+ with col1:
102
+ st.metric("Total de Observações", f"{len(df_clean):,}")
103
+ with col2:
104
+ st.metric("Países Únicos", df_clean[country_col].nunique())
105
+ with col3:
106
+ st.metric("Quantidade Média", f"{df_clean[quantity_col].mean():.2f}")
107
+ with col4:
108
+ st.metric("Receita Total", f"${df_clean['Revenue'].sum():,.0f}")
109
+
110
+ # Top países por volume de transações
111
+ top_countries_by_count = df_clean[country_col].value_counts().head(15)
112
+
113
+ fig_count = px.bar(
114
+ x=top_countries_by_count.index,
115
+ y=top_countries_by_count.values,
116
+ title="Top 15 Países por Número de Transações",
117
+ labels={"x": "País", "y": "Número de Transações"},
118
+ color=top_countries_by_count.values,
119
+ color_continuous_scale="Viridis",
120
+ )
121
+ fig_count.update_layout(showlegend=False)
122
+ st.plotly_chart(fig_count, use_container_width=True)
123
+
124
+ # Estatísticas descritivas por país (top 10)
125
+ top_countries_stats = df_clean[country_col].value_counts().head(10).index
126
+ df_top_stats = df_clean[df_clean[country_col].isin(top_countries_stats)]
127
+
128
+ stats_by_country = (
129
+ df_top_stats.groupby(country_col)
130
+ .agg(
131
+ {
132
+ quantity_col: ["mean", "median", "std"],
133
+ price_col: ["mean", "median", "std"],
134
+ "Revenue": ["sum", "mean"],
135
+ }
136
+ )
137
+ .round(2)
138
+ )
139
+
140
+ with st.expander("📋 Ver estatísticas descritivas por país (Top 10)"):
141
+ st.dataframe(stats_by_country, use_container_width=True)
142
+
143
+ # Box plots
144
+ col1, col2 = st.columns(2)
145
+
146
+ with col1:
147
+ fig_box_qty = px.box(
148
+ df_top_stats,
149
+ x=country_col,
150
+ y=quantity_col,
151
+ title="Distribuição de Quantidade por País (Top 10)",
152
+ labels={country_col: "País", quantity_col: "Quantidade"},
153
+ )
154
+ fig_box_qty.update_xaxis(tickangle=45)
155
+ st.plotly_chart(fig_box_qty, use_container_width=True)
156
+
157
+ with col2:
158
+ fig_box_price = px.box(
159
+ df_top_stats,
160
+ x=country_col,
161
+ y=price_col,
162
+ title="Distribuição de Preço por País (Top 10)",
163
+ labels={country_col: "País", price_col: "Preço Unitário"},
164
+ )
165
+ fig_box_price.update_xaxis(tickangle=45)
166
+ st.plotly_chart(fig_box_price, use_container_width=True)
167
+
168
+ # =============================
169
+ # Análise ANOVA
170
+ # =============================
171
+
172
+ st.header("🔬 Análise de Variância (ANOVA)")
173
+
174
+ # Selecionar top N países para análise
175
+ top_countries_anova = (
176
+ df_clean[country_col].value_counts().head(top_n_countries).index
177
+ )
178
+ df_anova = df_clean[df_clean[country_col].isin(top_countries_anova)].copy()
179
+
180
+ st.info(f"""
181
+ **Configuração da Análise:**
182
+ - **Países selecionados**: {top_n_countries}
183
+ - **Total de observações**: {len(df_anova):,}
184
+ - **Nível de significância**: α = {alpha}
185
+ - **Hipótese nula (H₀)**: As médias entre os países são iguais
186
+ - **Hipótese alternativa (H₁)**: Pelo menos uma média é diferente
187
+ """)
188
+
189
+ # ANOVA para Quantidade
190
+ st.subheader("📦 ANOVA para Quantidade")
191
+
192
+ formula_quantity = f"{quantity_col} ~ C({country_col})"
193
+ model_quantity = smf.ols(formula=formula_quantity, data=df_anova).fit()
194
+ anova_table_quantity = anova_lm(model_quantity, typ=2)
195
+
196
+ st.dataframe(anova_table_quantity.style.format("{:.4f}"), use_container_width=True)
197
+
198
+ f_stat_q = anova_table_quantity.iloc[0]["F"]
199
+ p_value_q = anova_table_quantity.iloc[0]["PR(>F)"]
200
+
201
+ col1, col2, col3 = st.columns(3)
202
+ with col1:
203
+ st.metric("F-statistic", f"{f_stat_q:.4f}")
204
+ with col2:
205
+ st.metric("p-value", f"{p_value_q:.6f}")
206
+ with col3:
207
+ is_significant_q = p_value_q < alpha
208
+ st.metric(
209
+ "Resultado",
210
+ "✅ Significativo" if is_significant_q else "❌ Não Significativo",
211
+ f"α = {alpha}",
212
+ )
213
+
214
+ if is_significant_q:
215
+ st.success(f"""
216
+ **Conclusão**: Rejeitamos H₀ (p = {p_value_q:.6f} < {alpha})
217
+
218
+ Existe diferença estatisticamente significativa entre as médias de quantidade dos países analisados.
219
+ """)
220
+ else:
221
+ st.warning(f"""
222
+ **Conclusão**: Não rejeitamos H₀ (p = {p_value_q:.6f} ≥ {alpha})
223
+
224
+ Não há evidências suficientes para concluir que as médias de quantidade diferem entre os países.
225
+ """)
226
+
227
+ # ANOVA para Preço
228
+ st.subheader("💰 ANOVA para Preço Unitário")
229
+
230
+ formula_price = f"{price_col} ~ C({country_col})"
231
+ model_price = smf.ols(formula=formula_price, data=df_anova).fit()
232
+ anova_table_price = anova_lm(model_price, typ=2)
233
+
234
+ st.dataframe(anova_table_price.style.format("{:.4f}"), use_container_width=True)
235
+
236
+ f_stat_p = anova_table_price.iloc[0]["F"]
237
+ p_value_p = anova_table_price.iloc[0]["PR(>F)"]
238
+
239
+ col1, col2, col3 = st.columns(3)
240
+ with col1:
241
+ st.metric("F-statistic", f"{f_stat_p:.4f}")
242
+ with col2:
243
+ st.metric("p-value", f"{p_value_p:.6f}")
244
+ with col3:
245
+ is_significant_p = p_value_p < alpha
246
+ st.metric(
247
+ "Resultado",
248
+ "✅ Significativo" if is_significant_p else "❌ Não Significativo",
249
+ f"α = {alpha}",
250
+ )
251
+
252
+ if is_significant_p:
253
+ st.success(f"""
254
+ **Conclusão**: Rejeitamos H₀ (p = {p_value_p:.6f} < {alpha})
255
+
256
+ Existe diferença estatisticamente significativa entre as médias de preço dos países analisados.
257
+ """)
258
+ else:
259
+ st.warning(f"""
260
+ **Conclusão**: Não rejeitamos H₀ (p = {p_value_p:.6f} ≥ {alpha})
261
+
262
+ Não há evidências suficientes para concluir que as médias de preço diferem entre os países.
263
+ """)
264
+
265
+ # ANOVA para Receita
266
+ st.subheader("💵 ANOVA para Receita")
267
+
268
+ formula_revenue = f"Revenue ~ C({country_col})"
269
+ model_revenue = smf.ols(formula=formula_revenue, data=df_anova).fit()
270
+ anova_table_revenue = anova_lm(model_revenue, typ=2)
271
+
272
+ st.dataframe(anova_table_revenue.style.format("{:.4f}"), use_container_width=True)
273
+
274
+ f_stat_r = anova_table_revenue.iloc[0]["F"]
275
+ p_value_r = anova_table_revenue.iloc[0]["PR(>F)"]
276
+
277
+ col1, col2, col3 = st.columns(3)
278
+ with col1:
279
+ st.metric("F-statistic", f"{f_stat_r:.4f}")
280
+ with col2:
281
+ st.metric("p-value", f"{p_value_r:.6f}")
282
+ with col3:
283
+ is_significant_r = p_value_r < alpha
284
+ st.metric(
285
+ "Resultado",
286
+ "✅ Significativo" if is_significant_r else "❌ Não Significativo",
287
+ f"α = {alpha}",
288
+ )
289
+
290
+ if is_significant_r:
291
+ st.success(f"""
292
+ **Conclusão**: Rejeitamos H₀ (p = {p_value_r:.6f} < {alpha})
293
+
294
+ Existe diferença estatisticamente significativa entre as médias de receita dos países analisados.
295
+ """)
296
+ else:
297
+ st.warning(f"""
298
+ **Conclusão**: Não rejeitamos H₀ (p = {p_value_r:.6f} ≥ {alpha})
299
+
300
+ Não há evidências suficientes para concluir que as médias de receita diferem entre os países.
301
+ """)
302
+
303
+ # =============================
304
+ # Visualizações Comparativas
305
+ # =============================
306
+
307
+ st.subheader("📊 Visualizações Comparativas")
308
+
309
+ # Médias por país
310
+ means_by_country = (
311
+ df_anova.groupby(country_col)
312
+ .agg({quantity_col: "mean", price_col: "mean", "Revenue": "mean"})
313
+ .reset_index()
314
+ )
315
+
316
+ col1, col2 = st.columns(2)
317
+
318
+ with col1:
319
+ fig_means_qty = px.bar(
320
+ means_by_country,
321
+ x=country_col,
322
+ y=quantity_col,
323
+ title="Média de Quantidade por País",
324
+ labels={country_col: "País", quantity_col: "Quantidade Média"},
325
+ color=quantity_col,
326
+ color_continuous_scale="Blues",
327
+ )
328
+ fig_means_qty.update_xaxis(tickangle=45)
329
+ st.plotly_chart(fig_means_qty, use_container_width=True)
330
+
331
+ with col2:
332
+ fig_means_price = px.bar(
333
+ means_by_country,
334
+ x=country_col,
335
+ y=price_col,
336
+ title="Média de Preço por País",
337
+ labels={country_col: "País", price_col: "Preço Médio"},
338
+ color=price_col,
339
+ color_continuous_scale="Greens",
340
+ )
341
+ fig_means_price.update_xaxis(tickangle=45)
342
+ st.plotly_chart(fig_means_price, use_container_width=True)
343
+
344
+ # Receita média por país
345
+ fig_means_revenue = px.bar(
346
+ means_by_country,
347
+ x=country_col,
348
+ y="Revenue",
349
+ title="Média de Receita por País",
350
+ labels={country_col: "País", "Revenue": "Receita Média"},
351
+ color="Revenue",
352
+ color_continuous_scale="Reds",
353
+ )
354
+ fig_means_revenue.update_xaxis(tickangle=45)
355
+ st.plotly_chart(fig_means_revenue, use_container_width=True)
356
+
357
+ # =============================
358
+ # Validação de Pressupostos
359
+ # =============================
360
+
361
+ if show_assumptions:
362
+ st.header("🔍 Validação dos Pressupostos da ANOVA")
363
+
364
+ st.markdown("""
365
+ A ANOVA requer três pressupostos principais:
366
+ 1. **Normalidade**: Os resíduos devem seguir distribuição normal
367
+ 2. **Homocedasticidade**: Variâncias iguais entre grupos
368
+ 3. **Independência**: Observações independentes entre si
369
+ """)
370
+
371
+ # Pressupostos para Quantidade
372
+ st.subheader("📦 Pressupostos para Quantidade")
373
+
374
+ residuals_q = model_quantity.resid
375
+ groups_q = [g[quantity_col].values for _, g in df_anova.groupby(country_col)]
376
+
377
+ # Teste de Levene para homocedasticidade
378
+ levene_stat_q, levene_p_q = stats.levene(*groups_q, center="mean")
379
+ levene_ok_q = levene_p_q > alpha
380
+
381
+ # Teste de Durbin-Watson para independência
382
+ dw_stat_q = durbin_watson(residuals_q)
383
+ dw_ok_q = 1.5 < dw_stat_q < 2.5
384
+
385
+ # Teste de normalidade (Shapiro-Wilk em amostra)
386
+ sample_residuals_q = residuals_q if len(residuals_q) <= 5000 else residuals_q.sample(5000, random_state=42)
387
+ shapiro_stat_q, shapiro_p_q = stats.shapiro(sample_residuals_q)
388
+ shapiro_ok_q = shapiro_p_q > alpha
389
+
390
+ col1, col2, col3 = st.columns(3)
391
+ with col1:
392
+ st.metric("Homocedasticidade (Levene)", "✅ OK" if levene_ok_q else "❌ Violado", f"p = {levene_p_q:.4f}")
393
+ with col2:
394
+ st.metric("Independência (Durbin-Watson)", "✅ OK" if dw_ok_q else "⚠️ Atenção", f"DW = {dw_stat_q:.4f}")
395
+ with col3:
396
+ st.metric("Normalidade (Shapiro-Wilk)", "✅ OK" if shapiro_ok_q else "❌ Violado", f"p = {shapiro_p_q:.4f}")
397
+
398
+ # Visualizações dos resíduos - Quantidade
399
+ col1, col2 = st.columns(2)
400
+
401
+ with col1:
402
+ fig_resid_q = px.histogram(
403
+ x=residuals_q,
404
+ nbins=50,
405
+ title="Distribuição dos Resíduos - Quantidade",
406
+ labels={"x": "Resíduos", "y": "Frequência"},
407
+ )
408
+ fig_resid_q.update_layout(showlegend=False)
409
+ st.plotly_chart(fig_resid_q, use_container_width=True)
410
+
411
+ with col2:
412
+ # Q-Q Plot
413
+ (osm, osr), (slope, intercept, r) = stats.probplot(residuals_q, dist="norm")
414
+
415
+ fig_qq_q = go.Figure()
416
+ fig_qq_q.add_trace(
417
+ go.Scatter(
418
+ x=osm,
419
+ y=osr,
420
+ mode="markers",
421
+ name="Resíduos",
422
+ marker=dict(color="blue", opacity=0.5),
423
+ )
424
+ )
425
+ fig_qq_q.add_trace(
426
+ go.Scatter(
427
+ x=osm,
428
+ y=slope * osm + intercept,
429
+ mode="lines",
430
+ name="Linha teórica",
431
+ line=dict(color="red", dash="dash"),
432
+ )
433
+ )
434
+ fig_qq_q.update_layout(
435
+ title="Q-Q Plot - Quantidade",
436
+ xaxis_title="Quantis Teóricos",
437
+ yaxis_title="Quantis da Amostra",
438
+ )
439
+ st.plotly_chart(fig_qq_q, use_container_width=True)
440
+
441
+ # Pressupostos para Preço
442
+ st.subheader("💰 Pressupostos para Preço")
443
+
444
+ residuals_p = model_price.resid
445
+ groups_p = [g[price_col].values for _, g in df_anova.groupby(country_col)]
446
+
447
+ levene_stat_p, levene_p_p = stats.levene(*groups_p, center="mean")
448
+ levene_ok_p = levene_p_p > alpha
449
+
450
+ dw_stat_p = durbin_watson(residuals_p)
451
+ dw_ok_p = 1.5 < dw_stat_p < 2.5
452
+
453
+ sample_residuals_p = residuals_p if len(residuals_p) <= 5000 else residuals_p.sample(5000, random_state=42)
454
+ shapiro_stat_p, shapiro_p_p = stats.shapiro(sample_residuals_p)
455
+ shapiro_ok_p = shapiro_p_p > alpha
456
+
457
+ col1, col2, col3 = st.columns(3)
458
+ with col1:
459
+ st.metric("Homocedasticidade (Levene)", "✅ OK" if levene_ok_p else "❌ Violado", f"p = {levene_p_p:.4f}")
460
+ with col2:
461
+ st.metric("Independência (Durbin-Watson)", "✅ OK" if dw_ok_p else "⚠️ Atenção", f"DW = {dw_stat_p:.4f}")
462
+ with col3:
463
+ st.metric("Normalidade (Shapiro-Wilk)", "✅ OK" if shapiro_ok_p else "❌ Violado", f"p = {shapiro_p_p:.4f}")
464
+
465
+ # Resumo dos pressupostos
466
+ st.subheader("✅ Resumo da Validação")
467
+
468
+ assumptions_df = pd.DataFrame(
469
+ {
470
+ "Variável": [
471
+ "Quantidade",
472
+ "Quantidade",
473
+ "Quantidade",
474
+ "Preço",
475
+ "Preço",
476
+ "Preço",
477
+ ],
478
+ "Pressuposto": ["Homocedasticidade", "Independência", "Normalidade"]
479
+ * 2,
480
+ "Status": [
481
+ "✅ Atendido" if levene_ok_q else "❌ Violado",
482
+ "✅ Atendido" if dw_ok_q else "⚠️ Atenção",
483
+ "✅ Atendido" if shapiro_ok_q else "❌ Violado",
484
+ "✅ Atendido" if levene_ok_p else "❌ Violado",
485
+ "✅ Atendido" if dw_ok_p else "⚠️ Atenção",
486
+ "✅ Atendido" if shapiro_ok_p else "❌ Violado",
487
+ ],
488
+ "Teste/Valor": [
489
+ f"Levene (p={levene_p_q:.4f})",
490
+ f"DW={dw_stat_q:.4f}",
491
+ f"Shapiro (p={shapiro_p_q:.4f})",
492
+ f"Levene (p={levene_p_p:.4f})",
493
+ f"DW={dw_stat_p:.4f}",
494
+ f"Shapiro (p={shapiro_p_p:.4f})",
495
+ ],
496
+ }
497
+ )
498
+
499
+ st.dataframe(assumptions_df, use_container_width=True)
500
+
501
+ if not (levene_ok_q and shapiro_ok_q) or not (levene_ok_p and shapiro_ok_p):
502
+ st.warning("""
503
+ ⚠️ **Nota sobre violação de pressupostos:**
504
+
505
+ Quando os pressupostos são violados, considere:
506
+ - Transformação de dados (log, sqrt, Box-Cox)
507
+ - Testes não-paramétricos (Kruskal-Wallis)
508
+ - Aumentar tamanho da amostra (Teorema do Limite Central)
509
+ - ANOVA é robusta a pequenas violações com amostras grandes
510
+ """)
511
+
512
+ # =============================
513
+ # Análise Detalhada
514
+ # =============================
515
+
516
+ if show_detailed_anova:
517
+ st.header("📈 Análise Detalhada")
518
+
519
+ # Sumário dos modelos
520
+ with st.expander("📋 Ver sumário completo do modelo - Quantidade"):
521
+ st.text(model_quantity.summary())
522
+
523
+ with st.expander("📋 Ver sumário completo do modelo - Preço"):
524
+ st.text(model_price.summary())
525
+
526
+ with st.expander("📋 Ver sumário completo do modelo - Receita"):
527
+ st.text(model_revenue.summary())
528
+
529
+ # =============================
530
+ # Resumo e Tomada de Decisão
531
+ # =============================
532
+
533
+ st.header("📋 Resumo e Tomada de Decisão")
534
+
535
+ col1, col2 = st.columns(2)
536
+
537
+ with col1:
538
+ st.subheader("🎯 Resultados da ANOVA")
539
+ results_df = pd.DataFrame(
540
+ {
541
+ "Variável": ["Quantidade", "Preço", "Receita"],
542
+ "F-statistic": [
543
+ f"{f_stat_q:.4f}",
544
+ f"{f_stat_p:.4f}",
545
+ f"{f_stat_r:.4f}",
546
+ ],
547
+ "p-value": [f"{p_value_q:.6f}", f"{p_value_p:.6f}", f"{p_value_r:.6f}"],
548
+ "Significativo": [
549
+ "✅ Sim" if is_significant_q else "❌ Não",
550
+ "✅ Sim" if is_significant_p else "❌ Não",
551
+ "✅ Sim" if is_significant_r else "❌ Não",
552
+ ],
553
+ }
554
+ )
555
+ st.dataframe(results_df, use_container_width=True)
556
+
557
+ with col2:
558
+ st.subheader("✅ Status dos Pressupostos")
559
+ if show_assumptions:
560
+ pressupostos_summary = pd.DataFrame(
561
+ {
562
+ "Variável": ["Quantidade", "Preço"],
563
+ "Homocedasticidade": [
564
+ "✅" if levene_ok_q else "❌",
565
+ "✅" if levene_ok_p else "❌",
566
+ ],
567
+ "Independência": [
568
+ "✅" if dw_ok_q else "⚠️",
569
+ "✅" if dw_ok_p else "⚠️",
570
+ ],
571
+ "Normalidade": [
572
+ "✅" if shapiro_ok_q else "❌",
573
+ "✅" if shapiro_ok_p else "❌",
574
+ ],
575
+ }
576
+ )
577
+ st.dataframe(pressupostos_summary, use_container_width=True)
578
+ else:
579
+ st.info(
580
+ "Ative 'Mostrar validação de pressupostos' para ver o status detalhado."
581
+ )
582
+
583
+ st.subheader("🔍 Principais Conclusões e Recomendações")
584
+
585
+ conclusions = f"""
586
+ **Análise ANOVA - Vendas de Varejo Online:**
587
+
588
+ 1. **Quantidade**: {"✅ Diferenças significativas detectadas" if is_significant_q else "❌ Sem diferenças significativas"} (p = {p_value_q:.6f})
589
+ 2. **Preço**: {"✅ Diferenças significativas detectadas" if is_significant_p else "❌ Sem diferenças significativas"} (p = {p_value_p:.6f})
590
+ 3. **Receita**: {"✅ Diferenças significativas detectadas" if is_significant_r else "❌ Sem diferenças significativas"} (p = {p_value_r:.6f})
591
+
592
+ **Implicações para o Negócio:**
593
+ """
594
+
595
+ if is_significant_q or is_significant_p or is_significant_r:
596
+ conclusions += """
597
+ - **Estratégia Diferenciada**: Implementar estratégias específicas por país
598
+ - **Precificação Regional**: Ajustar preços baseado em comportamento local
599
+ - **Alocação de Recursos**: Priorizar países com maior potencial
600
+ - **Marketing Direcionado**: Campanhas customizadas por mercado
601
+ """
602
+ else:
603
+ conclusions += """
604
+ - **Estratégia Uniforme**: Manter estratégia global consistente
605
+ - **Precificação Padronizada**: Preços uniformes são adequados
606
+ - **Eficiência Operacional**: Processos padronizados entre países
607
+ """
608
+
609
+ conclusions += f"""
610
+
611
+ **Recomendações Técnicas:**
612
+ - Nível de confiança: {(1 - alpha) * 100:.0f}%
613
+ - Países analisados: {top_n_countries}
614
+ - Total de observações: {len(df_anova):,}
615
+ """
616
+
617
+ if show_assumptions:
618
+ all_ok_q = levene_ok_q and dw_ok_q and shapiro_ok_q
619
+ all_ok_p = levene_ok_p and dw_ok_p and shapiro_ok_p
620
+
621
+ if not (all_ok_q and all_ok_p):
622
+ conclusions += (
623
+ "\n- ⚠️ Considerar transformação de dados ou testes não-paramétricos"
624
+ )
625
+ else:
626
+ conclusions += (
627
+ "\n- ✅ Todos os pressupostos atendidos, resultados confiáveis"
628
+ )
629
+
630
+ st.success(conclusions)
631
+
632
+ else:
633
+ st.error(
634
+ "❌ Erro ao carregar os dados. Verifique se os arquivos de dados estão presentes no diretório."
635
+ )
636
+
637
+ # =============================
638
+ # Footer
639
+ # =============================
640
+
641
+ st.markdown("---")
642
+ st.caption("PPCA/UnB | Novembro 2025")
marketing_campaign.csv → questao-4/credit_customers.csv RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:cd0affa36b1b981e80ba0e27767e9b3ab723f7a3dba948f722af81abc6b990ea
3
- size 220188
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0baf9fddd41e5a6af0ba84e6037a415f11384dd40c494186563a2d3b68f5c25
3
+ size 153016
questao-4/questao-4.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
questao-4/src/streamlit_app.py ADDED
@@ -0,0 +1,912 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ import warnings
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import streamlit as st
11
+ from imblearn.over_sampling import SMOTE
12
+ from sklearn.cluster import DBSCAN, KMeans
13
+ from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
14
+ from sklearn.linear_model import LogisticRegression
15
+ from sklearn.metrics import (
16
+ accuracy_score,
17
+ calinski_harabasz_score,
18
+ confusion_matrix,
19
+ davies_bouldin_score,
20
+ f1_score,
21
+ precision_score,
22
+ recall_score,
23
+ roc_auc_score,
24
+ roc_curve,
25
+ silhouette_score,
26
+ )
27
+ from sklearn.model_selection import train_test_split
28
+ from sklearn.preprocessing import LabelEncoder, StandardScaler
29
+ from sklearn.tree import DecisionTreeClassifier
30
+ from xgboost import XGBClassifier
31
+
32
+ try:
33
+ import shap
34
+
35
+ SHAP_AVAILABLE = True
36
+ except ImportError:
37
+ SHAP_AVAILABLE = False
38
+
39
+ warnings.filterwarnings("ignore")
40
+
41
+ st.set_page_config(
42
+ page_title="Questão 4 - Risco de Crédito",
43
+ page_icon="🏦",
44
+ layout="wide",
45
+ )
46
+
47
+ st.markdown("""
48
+ # Questão 4 – Modelo Preditivo de Risco de Crédito
49
+
50
+ **Credit Risk Dataset**
51
+
52
+ - **Autor:** Hugo Honda
53
+ - **Disciplina:** AEDI - PPCA/UnB
54
+ - **Data:** Novembro 2025
55
+
56
+ ---
57
+
58
+ ## Objetivos da Análise
59
+
60
+ 1. **Discussão sobre o Problema**: Contextualizar importância da gestão de risco
61
+ 2. **Análise Descritiva dos Dados**: Explorar características dos clientes
62
+ 3. **Definição e Seleção dos Modelos**: Comparar múltiplos algoritmos de ML
63
+ 4. **Explicabilidade das Variáveis - SHAP**: Interpretar decisões do modelo
64
+ 5. **Análise Não Supervisionada**: Identificar perfis de clientes via clustering
65
+ 6. **Tomada de Decisão Estratégica**: Fornecer insights acionáveis
66
+ """)
67
+
68
+
69
+ @st.cache_data
70
+ def load_data():
71
+ """Carrega e prepara os dados do Credit Risk Dataset"""
72
+ try:
73
+ df = pd.read_csv("credit_customers.csv")
74
+
75
+ # Mapear target para binário
76
+ df["class"] = df["class"].map({"good": 0, "bad": 1})
77
+
78
+ # Tratar valores ausentes
79
+ numeric_cols = df.select_dtypes(include=[np.number]).columns
80
+ for col in numeric_cols:
81
+ if df[col].isnull().sum() > 0:
82
+ df[col].fillna(df[col].median(), inplace=True)
83
+
84
+ categorical_cols = df.select_dtypes(include=["object"]).columns
85
+ for col in categorical_cols:
86
+ if df[col].isnull().sum() > 0:
87
+ df[col].fillna(df[col].mode()[0], inplace=True)
88
+
89
+ return df
90
+ except FileNotFoundError as e:
91
+ st.error(
92
+ f"Erro ao carregar dados: {str(e)}\n\nVerifique se o arquivo credit_customers.csv está presente."
93
+ )
94
+ return None
95
+ except Exception as e:
96
+ st.error(f"Erro inesperado ao carregar dados: {str(e)}")
97
+ return None
98
+
99
+
100
+ df = load_data()
101
+ target_col = "class"
102
+
103
+ if df is not None:
104
+ # =============================
105
+ # Sidebar - Controles
106
+ # =============================
107
+
108
+ st.sidebar.header("🎛️ Controles da Análise")
109
+
110
+ st.sidebar.subheader("Parâmetros do Modelo")
111
+ test_size = st.sidebar.slider("Tamanho do conjunto de teste:", 0.1, 0.4, 0.2, 0.05)
112
+ random_state = st.sidebar.number_input("Random State:", 1, 1000, 42)
113
+ use_smote = st.sidebar.checkbox("Usar SMOTE para balanceamento", True)
114
+
115
+ st.sidebar.subheader("Seleção de Modelos")
116
+ use_logistic = st.sidebar.checkbox("Logistic Regression", True)
117
+ use_decision_tree = st.sidebar.checkbox("Decision Tree", True)
118
+ use_random_forest = st.sidebar.checkbox("Random Forest", True)
119
+ use_xgboost = st.sidebar.checkbox("XGBoost", True)
120
+ use_gradient_boosting = st.sidebar.checkbox("Gradient Boosting", False)
121
+
122
+ st.sidebar.subheader("Opções de Visualização")
123
+ show_discussion = st.sidebar.checkbox("Mostrar discussão do problema", True)
124
+ show_eda = st.sidebar.checkbox("Mostrar análise exploratória", True)
125
+ show_shap = (
126
+ st.sidebar.checkbox("Mostrar análise SHAP", True) if SHAP_AVAILABLE else False
127
+ )
128
+ show_clustering = st.sidebar.checkbox("Mostrar análise de clustering", True)
129
+
130
+ # =============================
131
+ # Discussão sobre o Problema
132
+ # =============================
133
+
134
+ if show_discussion:
135
+ st.header("💡 Discussão sobre o Problema de Risco de Crédito")
136
+
137
+ col1, col2 = st.columns(2)
138
+
139
+ with col1:
140
+ st.markdown("""
141
+ ### 📌 Importância no Setor Bancário
142
+
143
+ **1. Redução de Perdas Financeiras**
144
+ - Identificar clientes com alto risco de inadimplência
145
+ - Evitar perdas significativas para a instituição
146
+ - Proteger o capital e liquidez do banco
147
+
148
+ **2. Gestão de Carteira de Crédito**
149
+ - Melhorar qualidade da carteira de empréstimos
150
+ - Otimizar alocação de recursos
151
+ - Balancear risco e retorno
152
+
153
+ **3. Decisões Estratégicas**
154
+ - Concessão de empréstimos
155
+ - Definição de limites de crédito
156
+ - Políticas de cobrança
157
+ - Taxas de juros personalizadas
158
+ """)
159
+
160
+ with col2:
161
+ st.markdown("""
162
+ ### 🎯 Por que Prever Inadimplência é Essencial
163
+
164
+ **1. Prevenção de Perdas**
165
+ - Antecipar problemas antes que ocorram
166
+ - Tomar ações preventivas
167
+ - Reduzir taxa de inadimplência
168
+
169
+ **2. Otimização de Recursos**
170
+ - Focar esforços de cobrança onde necessário
171
+ - Alocar recursos de forma eficiente
172
+ - Automatizar decisões de baixo risco
173
+
174
+ **3. Conformidade Regulatória**
175
+ - Atender requisitos de Basileia III
176
+ - Gerenciar capital regulatório
177
+ - Demonstrar governança de risco
178
+
179
+ **4. Vantagem Competitiva**
180
+ - Oferecer produtos adequados
181
+ - Melhor experiência ao cliente
182
+ - Reduzir custos operacionais
183
+ """)
184
+
185
+ st.info("""
186
+ **Impacto Econômico:** A gestão eficaz de risco de crédito não apenas protege a instituição financeira,
187
+ mas também contribui para a estabilidade do sistema financeiro como um todo, reduzindo a inadimplência
188
+ sistêmica e promovendo crescimento econômico sustentável.
189
+ """)
190
+
191
+ # =============================
192
+ # Análise Exploratória
193
+ # =============================
194
+
195
+ if show_eda:
196
+ st.header("📊 Análise Descritiva dos Dados")
197
+
198
+ col1, col2, col3, col4 = st.columns(4)
199
+ with col1:
200
+ st.metric("Total de Observações", f"{len(df):,}")
201
+ with col2:
202
+ default_rate = df[target_col].mean()
203
+ st.metric("Taxa de Inadimplência", f"{default_rate:.2%}")
204
+ with col3:
205
+ st.metric("Total de Features", len(df.columns) - 1)
206
+ with col4:
207
+ numeric_features = df.select_dtypes(include=[np.number]).columns
208
+ st.metric("Features Numéricas", len(numeric_features) - 1)
209
+
210
+ # Distribuição da variável target
211
+ target_counts = df[target_col].value_counts()
212
+
213
+ fig_target = go.Figure(
214
+ data=[
215
+ go.Pie(
216
+ labels=["Bom Pagador", "Mau Pagador"],
217
+ values=target_counts.values,
218
+ hole=0.4,
219
+ marker_colors=["#00CC96", "#EF553B"],
220
+ )
221
+ ]
222
+ )
223
+ fig_target.update_layout(title="Distribuição de Classes")
224
+ st.plotly_chart(fig_target, use_container_width=True)
225
+
226
+ # Análise de balanceamento
227
+ balance_ratio = target_counts.min() / target_counts.max()
228
+ if balance_ratio < 0.5:
229
+ st.warning(f"""
230
+ ⚠️ **Desbalanceamento de Classes Detectado**
231
+
232
+ - Razão de balanceamento: {balance_ratio:.2%}
233
+ - Recomendação: Usar técnicas de balanceamento (SMOTE, class_weight)
234
+ """)
235
+ else:
236
+ st.success("✅ Classes relativamente balanceadas")
237
+
238
+ # Estatísticas descritivas
239
+ with st.expander("📋 Ver estatísticas descritivas"):
240
+ st.dataframe(df.describe(), use_container_width=True)
241
+
242
+ # =============================
243
+ # Preparação dos Dados
244
+ # =============================
245
+
246
+ st.header("🔬 Definição e Seleção dos Modelos")
247
+
248
+ # Preparar dados
249
+ df_clean = df.copy()
250
+ categorical_cols = df_clean.select_dtypes(include=["object"]).columns
251
+ le_dict = {}
252
+
253
+ for col in categorical_cols:
254
+ if col != target_col:
255
+ le = LabelEncoder()
256
+ df_clean[col] = le.fit_transform(df_clean[col].astype(str))
257
+ le_dict[col] = le
258
+
259
+ all_features = [col for col in df_clean.columns if col != target_col]
260
+ X = df_clean[all_features].copy()
261
+ y = df_clean[target_col].copy()
262
+
263
+ # Divisão treino-teste
264
+ X_train, X_test, y_train, y_test = train_test_split(
265
+ X, y, test_size=test_size, random_state=random_state, stratify=y
266
+ )
267
+
268
+ # Normalização
269
+ scaler = StandardScaler()
270
+ X_train_scaled = scaler.fit_transform(X_train)
271
+ X_test_scaled = scaler.transform(X_test)
272
+
273
+ # Balanceamento com SMOTE
274
+ if use_smote:
275
+ smote = SMOTE(random_state=random_state)
276
+ X_train_balanced, y_train_balanced = smote.fit_resample(X_train_scaled, y_train)
277
+ st.info(
278
+ f"✅ SMOTE aplicado: {len(X_train_scaled)} → {len(X_train_balanced)} amostras de treino"
279
+ )
280
+ else:
281
+ X_train_balanced, y_train_balanced = X_train_scaled, y_train
282
+
283
+ # =============================
284
+ # Treinamento dos Modelos
285
+ # =============================
286
+
287
+ st.subheader("🤖 Treinamento e Comparação de Modelos")
288
+
289
+ models = {}
290
+
291
+ if use_logistic:
292
+ models["Logistic Regression"] = LogisticRegression(
293
+ random_state=random_state, max_iter=1000
294
+ )
295
+ if use_decision_tree:
296
+ models["Decision Tree"] = DecisionTreeClassifier(
297
+ random_state=random_state, max_depth=10
298
+ )
299
+ if use_random_forest:
300
+ models["Random Forest"] = RandomForestClassifier(
301
+ random_state=random_state, n_estimators=100, max_depth=10
302
+ )
303
+ if use_xgboost:
304
+ models["XGBoost"] = XGBClassifier(
305
+ random_state=random_state, eval_metric="logloss", n_estimators=100
306
+ )
307
+ if use_gradient_boosting:
308
+ models["Gradient Boosting"] = GradientBoostingClassifier(
309
+ random_state=random_state, n_estimators=100
310
+ )
311
+
312
+ if not models:
313
+ st.warning("⚠️ Selecione pelo menos um modelo para treinar.")
314
+ else:
315
+ results = {}
316
+ progress_bar = st.progress(0)
317
+ status_text = st.empty()
318
+
319
+ for i, (name, model) in enumerate(models.items()):
320
+ status_text.text(f"Treinando {name}...")
321
+
322
+ model.fit(X_train_balanced, y_train_balanced)
323
+ y_pred = model.predict(X_test_scaled)
324
+ y_pred_proba = model.predict_proba(X_test_scaled)[:, 1]
325
+
326
+ results[name] = {
327
+ "accuracy": accuracy_score(y_test, y_pred),
328
+ "precision": precision_score(y_test, y_pred, zero_division=0),
329
+ "recall": recall_score(y_test, y_pred, zero_division=0),
330
+ "f1": f1_score(y_test, y_pred, zero_division=0),
331
+ "roc_auc": roc_auc_score(y_test, y_pred_proba),
332
+ "model": model,
333
+ "y_pred": y_pred,
334
+ "y_pred_proba": y_pred_proba,
335
+ }
336
+
337
+ progress_bar.progress((i + 1) / len(models))
338
+
339
+ status_text.text("✅ Treinamento concluído!")
340
+ progress_bar.empty()
341
+
342
+ # =============================
343
+ # Comparação de Modelos
344
+ # =============================
345
+
346
+ st.subheader("📈 Comparação de Performance dos Modelos")
347
+
348
+ comparison_df = pd.DataFrame(
349
+ {
350
+ name: {
351
+ "Accuracy": f"{data['accuracy']:.4f}",
352
+ "Precision": f"{data['precision']:.4f}",
353
+ "Recall": f"{data['recall']:.4f}",
354
+ "F1-Score": f"{data['f1']:.4f}",
355
+ "AUC-ROC": f"{data['roc_auc']:.4f}",
356
+ }
357
+ for name, data in results.items()
358
+ }
359
+ ).T
360
+
361
+ st.dataframe(
362
+ comparison_df.style.highlight_max(axis=0, color="lightgreen"),
363
+ use_container_width=True,
364
+ )
365
+
366
+ # Identificar melhor modelo
367
+ best_model_name = max(results, key=lambda x: results[x]["roc_auc"])
368
+ best_model = results[best_model_name]["model"]
369
+ best_auc = results[best_model_name]["roc_auc"]
370
+
371
+ st.success(f"🏆 **Melhor Modelo: {best_model_name}** (AUC-ROC: {best_auc:.4f})")
372
+
373
+ # Visualização comparativa
374
+ metrics_for_plot = pd.DataFrame(
375
+ {
376
+ name: [
377
+ data["accuracy"],
378
+ data["precision"],
379
+ data["recall"],
380
+ data["f1"],
381
+ data["roc_auc"],
382
+ ]
383
+ for name, data in results.items()
384
+ },
385
+ index=["Accuracy", "Precision", "Recall", "F1-Score", "AUC-ROC"],
386
+ ).T.reset_index()
387
+ metrics_for_plot.columns = ["Model"] + list(metrics_for_plot.columns[1:])
388
+
389
+ fig_comparison = go.Figure()
390
+ for metric in ["Accuracy", "Precision", "Recall", "F1-Score", "AUC-ROC"]:
391
+ fig_comparison.add_trace(
392
+ go.Bar(
393
+ name=metric, x=metrics_for_plot["Model"], y=metrics_for_plot[metric]
394
+ )
395
+ )
396
+
397
+ fig_comparison.update_layout(
398
+ title="Comparação de Métricas por Modelo",
399
+ xaxis_title="Modelo",
400
+ yaxis_title="Score",
401
+ barmode="group",
402
+ height=500,
403
+ )
404
+ st.plotly_chart(fig_comparison, use_container_width=True)
405
+
406
+ # =============================
407
+ # Matriz de Confusão
408
+ # =============================
409
+
410
+ st.subheader("📊 Matriz de Confusão - Melhor Modelo")
411
+
412
+ cm = confusion_matrix(y_test, results[best_model_name]["y_pred"])
413
+
414
+ fig_cm = px.imshow(
415
+ cm,
416
+ labels=dict(x="Predito", y="Real", color="Quantidade"),
417
+ x=["Bom Pagador", "Mau Pagador"],
418
+ y=["Bom Pagador", "Mau Pagador"],
419
+ text_auto=True,
420
+ title=f"Matriz de Confusão - {best_model_name}",
421
+ color_continuous_scale="Blues",
422
+ )
423
+ st.plotly_chart(fig_cm, use_container_width=True)
424
+
425
+ tn, fp, fn, tp = cm.ravel()
426
+
427
+ col1, col2, col3, col4 = st.columns(4)
428
+ with col1:
429
+ st.metric(
430
+ "Verdadeiros Positivos (TP)",
431
+ tp,
432
+ help="Maus pagadores corretamente identificados",
433
+ )
434
+ with col2:
435
+ st.metric(
436
+ "Verdadeiros Negativos (TN)",
437
+ tn,
438
+ help="Bons pagadores corretamente identificados",
439
+ )
440
+ with col3:
441
+ st.metric(
442
+ "Falsos Positivos (FP)",
443
+ fp,
444
+ help="Bons pagadores classificados como maus",
445
+ )
446
+ with col4:
447
+ st.metric(
448
+ "Falsos Negativos (FN)",
449
+ fn,
450
+ help="Maus pagadores não identificados (CRÍTICO)",
451
+ )
452
+
453
+ # Custo de erro
454
+ st.info(f"""
455
+ **Análise de Custo:**
456
+ - **Falsos Negativos ({fn})**: Maior risco! Maus pagadores não identificados podem causar perdas significativas
457
+ - **Falsos Positivos ({fp})**: Oportunidade perdida! Bons clientes podem ser rejeitados
458
+ - **Taxa de Identificação de Maus Pagadores**: {results[best_model_name]["recall"]:.2%} (Recall)
459
+ """)
460
+
461
+ # =============================
462
+ # Curva ROC
463
+ # =============================
464
+
465
+ st.subheader("📈 Curva ROC - Comparação de Modelos")
466
+
467
+ fig_roc = go.Figure()
468
+
469
+ for name, data in results.items():
470
+ fpr, tpr, _ = roc_curve(y_test, data["y_pred_proba"])
471
+ fig_roc.add_trace(
472
+ go.Scatter(
473
+ x=fpr,
474
+ y=tpr,
475
+ mode="lines",
476
+ name=f"{name} (AUC={data['roc_auc']:.4f})",
477
+ line=dict(width=2),
478
+ )
479
+ )
480
+
481
+ fig_roc.add_trace(
482
+ go.Scatter(
483
+ x=[0, 1],
484
+ y=[0, 1],
485
+ mode="lines",
486
+ name="Aleatório",
487
+ line=dict(color="red", width=2, dash="dash"),
488
+ )
489
+ )
490
+
491
+ fig_roc.update_layout(
492
+ title="Curva ROC - Receiver Operating Characteristic",
493
+ xaxis_title="Taxa de Falsos Positivos (FPR)",
494
+ yaxis_title="Taxa de Verdadeiros Positivos (TPR)",
495
+ height=600,
496
+ )
497
+
498
+ st.plotly_chart(fig_roc, use_container_width=True)
499
+
500
+ # =============================
501
+ # Análise SHAP
502
+ # =============================
503
+
504
+ if show_shap and SHAP_AVAILABLE:
505
+ st.header("🔍 Explicabilidade das Variáveis (SHAP)")
506
+
507
+ st.markdown("""
508
+ **SHAP (SHapley Additive exPlanations)** é uma abordagem de teoria dos jogos para explicar
509
+ as predições de qualquer modelo de machine learning. Ele conecta teoria dos jogos com
510
+ explicabilidade local, atribuindo um valor de importância para cada feature.
511
+ """)
512
+
513
+ try:
514
+ # Verificar tipo de modelo para escolher explainer apropriado
515
+ is_tree_model = isinstance(
516
+ best_model,
517
+ (
518
+ XGBClassifier,
519
+ RandomForestClassifier,
520
+ DecisionTreeClassifier,
521
+ GradientBoostingClassifier,
522
+ ),
523
+ )
524
+
525
+ with st.spinner("Calculando SHAP values..."):
526
+ if is_tree_model:
527
+ explainer = shap.TreeExplainer(best_model)
528
+ shap_values = explainer.shap_values(X_test_scaled[:1000])
529
+ elif isinstance(best_model, LogisticRegression):
530
+ explainer = shap.LinearExplainer(
531
+ best_model, X_train_scaled[:100]
532
+ )
533
+ shap_values = explainer.shap_values(X_test_scaled[:1000])
534
+ else:
535
+ explainer = shap.KernelExplainer(
536
+ best_model.predict_proba, X_train_scaled[:100]
537
+ )
538
+ shap_values = explainer.shap_values(X_test_scaled[:100])
539
+
540
+ # Se shap_values é uma lista (para classificação binária), pegar o segundo elemento
541
+ if isinstance(shap_values, list):
542
+ shap_values_to_plot = shap_values[1]
543
+ else:
544
+ shap_values_to_plot = shap_values
545
+
546
+ st.subheader("📊 SHAP Summary Plot")
547
+
548
+ # Criar figura SHAP
549
+ import matplotlib.pyplot as plt
550
+
551
+ fig, ax = plt.subplots(figsize=(10, 8))
552
+ shap.summary_plot(
553
+ shap_values_to_plot,
554
+ X_test_scaled[:1000],
555
+ feature_names=all_features,
556
+ show=False,
557
+ max_display=20,
558
+ )
559
+ st.pyplot(fig)
560
+ plt.close()
561
+
562
+ st.info("""
563
+ **Interpretação do SHAP Summary Plot:**
564
+ - **Eixo Y**: Features ordenadas por importância (mais importante no topo)
565
+ - **Eixo X**: Impacto no modelo (valores positivos aumentam probabilidade de inadimplência)
566
+ - **Cor**: Valor da feature (vermelho = alto, azul = baixo)
567
+ - **Dispersão**: Mostra o efeito de diferentes valores da feature
568
+ """)
569
+
570
+ # SHAP Feature Importance
571
+ st.subheader("🎯 Importância das Features (SHAP)")
572
+
573
+ shap_importance = np.abs(shap_values_to_plot).mean(axis=0)
574
+ importance_df = (
575
+ pd.DataFrame(
576
+ {"Feature": all_features, "Importance": shap_importance}
577
+ )
578
+ .sort_values("Importance", ascending=False)
579
+ .head(20)
580
+ )
581
+
582
+ fig_shap_imp = px.bar(
583
+ importance_df,
584
+ x="Importance",
585
+ y="Feature",
586
+ orientation="h",
587
+ title="Top 20 Features por Importância SHAP",
588
+ color="Importance",
589
+ color_continuous_scale="Viridis",
590
+ )
591
+ fig_shap_imp.update_layout(height=600)
592
+ st.plotly_chart(fig_shap_imp, use_container_width=True)
593
+
594
+ except Exception as e:
595
+ st.warning(
596
+ f"⚠️ Erro ao calcular SHAP values: {str(e)}\n\nIsso pode ocorrer com alguns tipos de modelos."
597
+ )
598
+ elif show_shap and not SHAP_AVAILABLE:
599
+ st.warning("📦 SHAP não está instalado. Execute: `pip install shap`")
600
+
601
+ # Feature Importance alternativa (para modelos baseados em árvore)
602
+ if hasattr(best_model, "feature_importances_"):
603
+ st.subheader("🎯 Importância das Features (Modelo)")
604
+
605
+ feature_importance = (
606
+ pd.DataFrame(
607
+ {
608
+ "Feature": all_features,
609
+ "Importance": best_model.feature_importances_,
610
+ }
611
+ )
612
+ .sort_values("Importance", ascending=False)
613
+ .head(20)
614
+ )
615
+
616
+ fig_feat_imp = px.bar(
617
+ feature_importance,
618
+ x="Importance",
619
+ y="Feature",
620
+ orientation="h",
621
+ title=f"Top 20 Features Mais Importantes - {best_model_name}",
622
+ color="Importance",
623
+ color_continuous_scale="Viridis",
624
+ )
625
+ fig_feat_imp.update_layout(height=600)
626
+ st.plotly_chart(fig_feat_imp, use_container_width=True)
627
+
628
+ # =============================
629
+ # Análise de Clustering
630
+ # =============================
631
+
632
+ if show_clustering:
633
+ st.header("📊 Análise Não Supervisionada (Clustering)")
634
+
635
+ st.markdown("""
636
+ Clustering permite identificar grupos naturais de clientes com características similares,
637
+ independentemente do rótulo de inadimplência. Isso pode revelar perfis de risco não capturados
638
+ pela análise supervisionada.
639
+ """)
640
+
641
+ # Selecionar features para clustering (usar todas numéricas originais)
642
+ numeric_features_for_cluster = df.select_dtypes(
643
+ include=[np.number]
644
+ ).columns.tolist()
645
+ if target_col in numeric_features_for_cluster:
646
+ numeric_features_for_cluster.remove(target_col)
647
+
648
+ if len(numeric_features_for_cluster) >= 2:
649
+ X_cluster = df[numeric_features_for_cluster].copy()
650
+ X_cluster = X_cluster.fillna(X_cluster.median())
651
+ X_cluster_scaled = StandardScaler().fit_transform(X_cluster)
652
+
653
+ col1, col2 = st.columns(2)
654
+
655
+ with col1:
656
+ st.subheader("🔵 K-Means Clustering")
657
+
658
+ n_clusters = st.slider("Número de clusters (K-Means):", 2, 10, 4)
659
+
660
+ kmeans = KMeans(
661
+ n_clusters=n_clusters, random_state=random_state, n_init=10
662
+ )
663
+ clusters_kmeans = kmeans.fit_predict(X_cluster_scaled)
664
+
665
+ # Métricas de clustering
666
+ silhouette_km = silhouette_score(X_cluster_scaled, clusters_kmeans)
667
+ davies_bouldin_km = davies_bouldin_score(
668
+ X_cluster_scaled, clusters_kmeans
669
+ )
670
+ calinski_km = calinski_harabasz_score(
671
+ X_cluster_scaled, clusters_kmeans
672
+ )
673
+
674
+ st.metric(
675
+ "Silhouette Score",
676
+ f"{silhouette_km:.4f}",
677
+ help="Maior é melhor (range: -1 a 1)",
678
+ )
679
+ st.metric(
680
+ "Davies-Bouldin Index",
681
+ f"{davies_bouldin_km:.4f}",
682
+ help="Menor é melhor",
683
+ )
684
+ st.metric(
685
+ "Calinski-Harabasz Score",
686
+ f"{calinski_km:.2f}",
687
+ help="Maior é melhor",
688
+ )
689
+
690
+ # Distribuição de clusters por classe
691
+ df_kmeans = df.copy()
692
+ df_kmeans["Cluster"] = clusters_kmeans
693
+
694
+ cluster_class_dist = (
695
+ pd.crosstab(
696
+ df_kmeans["Cluster"],
697
+ df_kmeans[target_col],
698
+ normalize="index",
699
+ )
700
+ * 100
701
+ )
702
+
703
+ st.write("**Distribuição de Classes por Cluster (%):**")
704
+ st.dataframe(cluster_class_dist.round(2), use_container_width=True)
705
+
706
+ with col2:
707
+ st.subheader("🔴 DBSCAN Clustering")
708
+
709
+ eps = st.slider("Epsilon (raio de vizinhança):", 0.1, 2.0, 0.5, 0.1)
710
+ min_samples = st.slider("Mínimo de amostras:", 2, 20, 5)
711
+
712
+ dbscan = DBSCAN(eps=eps, min_samples=min_samples)
713
+ clusters_dbscan = dbscan.fit_predict(X_cluster_scaled)
714
+
715
+ n_clusters_db = len(set(clusters_dbscan)) - (
716
+ 1 if -1 in clusters_dbscan else 0
717
+ )
718
+ n_outliers = (clusters_dbscan == -1).sum()
719
+
720
+ st.metric("Número de Clusters", n_clusters_db)
721
+ st.metric(
722
+ "Outliers Detectados",
723
+ n_outliers,
724
+ help="Pontos classificados como -1",
725
+ )
726
+ st.metric(
727
+ "% de Outliers",
728
+ f"{(n_outliers / len(clusters_dbscan)) * 100:.2f}%",
729
+ )
730
+
731
+ if n_clusters_db > 1:
732
+ # Calcular métricas apenas para pontos não-outliers
733
+ mask_not_outliers = clusters_dbscan != -1
734
+ if mask_not_outliers.sum() > n_clusters_db:
735
+ silhouette_db = silhouette_score(
736
+ X_cluster_scaled[mask_not_outliers],
737
+ clusters_dbscan[mask_not_outliers],
738
+ )
739
+ st.metric("Silhouette Score", f"{silhouette_db:.4f}")
740
+
741
+ # Análise de outliers
742
+ if n_outliers > 0:
743
+ df_dbscan = df.copy()
744
+ df_dbscan["Cluster"] = clusters_dbscan
745
+ outliers_default_rate = df_dbscan[df_dbscan["Cluster"] == -1][
746
+ target_col
747
+ ].mean()
748
+ normal_default_rate = df_dbscan[df_dbscan["Cluster"] != -1][
749
+ target_col
750
+ ].mean()
751
+
752
+ st.write(
753
+ f"**Taxa de Inadimplência em Outliers:** {outliers_default_rate:.2%}"
754
+ )
755
+ st.write(
756
+ f"**Taxa de Inadimplência em Clusters:** {normal_default_rate:.2%}"
757
+ )
758
+
759
+ # Visualização 2D usando PCA
760
+ from sklearn.decomposition import PCA
761
+
762
+ pca = PCA(n_components=2)
763
+ X_pca = pca.fit_transform(X_cluster_scaled)
764
+
765
+ col1, col2 = st.columns(2)
766
+
767
+ with col1:
768
+ fig_kmeans_viz = px.scatter(
769
+ x=X_pca[:, 0],
770
+ y=X_pca[:, 1],
771
+ color=clusters_kmeans.astype(str),
772
+ title="Visualização K-Means (PCA 2D)",
773
+ labels={
774
+ "x": f"PC1 ({pca.explained_variance_ratio_[0]:.1%})",
775
+ "y": f"PC2 ({pca.explained_variance_ratio_[1]:.1%})",
776
+ },
777
+ opacity=0.6,
778
+ )
779
+ fig_kmeans_viz.update_layout(height=500)
780
+ st.plotly_chart(fig_kmeans_viz, use_container_width=True)
781
+
782
+ with col2:
783
+ fig_dbscan_viz = px.scatter(
784
+ x=X_pca[:, 0],
785
+ y=X_pca[:, 1],
786
+ color=clusters_dbscan.astype(str),
787
+ title="Visualização DBSCAN (PCA 2D)",
788
+ labels={
789
+ "x": f"PC1 ({pca.explained_variance_ratio_[0]:.1%})",
790
+ "y": f"PC2 ({pca.explained_variance_ratio_[1]:.1%})",
791
+ },
792
+ opacity=0.6,
793
+ )
794
+ fig_dbscan_viz.update_layout(height=500)
795
+ st.plotly_chart(fig_dbscan_viz, use_container_width=True)
796
+
797
+ st.info(f"""
798
+ **Interpretação do Clustering:**
799
+ - **K-Means**: Identifica {n_clusters} grupos com características similares
800
+ - **DBSCAN**: Detecta {n_clusters_db} clusters densos e {n_outliers} outliers
801
+ - **Variância Explicada (PCA)**: {(pca.explained_variance_ratio_[0] + pca.explained_variance_ratio_[1]):.1%} com 2 componentes
802
+ - Os clusters podem representar perfis de risco distintos
803
+ """)
804
+ else:
805
+ st.warning("Número insuficiente de features numéricas para clustering.")
806
+
807
+ # =============================
808
+ # Tomada de Decisão Estratégica
809
+ # =============================
810
+
811
+ st.header("💼 Tomada de Decisão Estratégica")
812
+
813
+ st.markdown("""
814
+ ### 🎯 Recomendações Baseadas na Análise
815
+ """)
816
+
817
+ col1, col2 = st.columns(2)
818
+
819
+ with col1:
820
+ st.markdown(f"""
821
+ #### 📊 Insights do Modelo Preditivo
822
+
823
+ **Modelo Recomendado:** {best_model_name}
824
+ - **AUC-ROC:** {best_auc:.4f}
825
+ - **Recall (Detecção de Maus Pagadores):** {results[best_model_name]["recall"]:.2%}
826
+ - **Precisão:** {results[best_model_name]["precision"]:.2%}
827
+
828
+ **Aplicações Práticas:**
829
+ 1. **Aprovação Automática**: Clientes de baixo risco
830
+ 2. **Análise Manual**: Casos intermediários
831
+ 3. **Rejeição ou Condições Especiais**: Alto risco
832
+ 4. **Pricing Dinâmico**: Taxas baseadas em risco
833
+ """)
834
+
835
+ with col2:
836
+ st.markdown("""
837
+ #### 🎯 Estratégias de Mitigação de Risco
838
+
839
+ **Para Reduzir Falsos Negativos:**
840
+ - Ajustar threshold de decisão
841
+ - Implementar sistema de alertas
842
+ - Análise adicional de casos limítrofes
843
+
844
+ **Para Otimizar Receita:**
845
+ - Oferecer taxas ajustadas ao risco
846
+ - Programas de fidelidade para bons pagadores
847
+ - Produtos personalizados por perfil
848
+
849
+ **Monitoramento Contínuo:**
850
+ - Recalibrar modelo periodicamente
851
+ - Monitorar drift de dados
852
+ - Atualizar com novos dados
853
+ """)
854
+
855
+ # Resumo executivo
856
+ st.subheader("📋 Resumo Executivo")
857
+
858
+ summary_metrics = pd.DataFrame(
859
+ {
860
+ "Métrica": [
861
+ "Total de Clientes Analisados",
862
+ "Taxa de Inadimplência",
863
+ "Melhor Modelo",
864
+ "Performance (AUC-ROC)",
865
+ "Taxa de Detecção",
866
+ "Falsos Negativos (Risco)",
867
+ "Clusters Identificados",
868
+ ],
869
+ "Valor": [
870
+ f"{len(df):,}",
871
+ f"{default_rate:.2%}",
872
+ best_model_name,
873
+ f"{best_auc:.4f}",
874
+ f"{results[best_model_name]['recall']:.2%}",
875
+ f"{fn} ({(fn / (fn + tp)) * 100:.1f}% dos maus pagadores)",
876
+ f"{n_clusters if show_clustering and len(numeric_features_for_cluster) >= 2 else 'N/A'}",
877
+ ],
878
+ }
879
+ )
880
+
881
+ st.dataframe(summary_metrics, use_container_width=True)
882
+
883
+ st.success(f"""
884
+ **Conclusão Final:**
885
+
886
+ O modelo {best_model_name} demonstrou a melhor performance com AUC-ROC de {best_auc:.4f},
887
+ sendo capaz de identificar {results[best_model_name]["recall"]:.2%} dos maus pagadores.
888
+
889
+ **Impacto Estimado:**
890
+ - Redução potencial de inadimplência: {results[best_model_name]["recall"] * default_rate * 100:.1f}%
891
+ - Melhoria na qualidade da carteira de crédito
892
+ - Otimização da alocação de recursos de cobrança
893
+
894
+ **Próximos Passos:**
895
+ 1. Implementar modelo em ambiente de produção
896
+ 2. Estabelecer pipeline de monitoramento
897
+ 3. Criar dashboards executivos
898
+ 4. Treinar equipes para uso do sistema
899
+ 5. Avaliar impacto financeiro em 6-12 meses
900
+ """)
901
+
902
+ else:
903
+ st.error(
904
+ "❌ Erro ao carregar os dados. Verifique se o arquivo credit_customers.csv está presente no diretório."
905
+ )
906
+
907
+ # =============================
908
+ # Footer
909
+ # =============================
910
+
911
+ st.markdown("---")
912
+ st.caption("PPCA/UnB | Novembro 2025")
regressao_logistica_churn_bancario.ipynb DELETED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -8,3 +8,6 @@ plotly
8
  statsmodels
9
  scipy
10
  imbalanced-learn
 
 
 
 
8
  statsmodels
9
  scipy
10
  imbalanced-learn
11
+ xgboost
12
+ shap
13
+ openpyxl
src/__pycache__/streamlit_app.cpython-313.pyc DELETED
Binary file (23.1 kB)
 
src/streamlit_app.py CHANGED
@@ -1,592 +1,247 @@
1
  #!/usr/bin/env python
2
  # coding: utf-8
3
 
4
- import warnings
5
- from collections import Counter
6
-
7
- import numpy as np
8
- import pandas as pd
9
- import plotly.express as px
10
- import plotly.graph_objects as go
11
  import streamlit as st
12
- from imblearn.over_sampling import SMOTE
13
- from imblearn.pipeline import Pipeline as ImbPipeline
14
- from sklearn.compose import ColumnTransformer
15
- from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
16
- from sklearn.linear_model import LogisticRegression
17
- from sklearn.metrics import (
18
- accuracy_score,
19
- auc,
20
- average_precision_score,
21
- balanced_accuracy_score,
22
- confusion_matrix,
23
- f1_score,
24
- precision_recall_curve,
25
- precision_score,
26
- recall_score,
27
- roc_auc_score,
28
- roc_curve,
29
- )
30
- from sklearn.model_selection import (
31
- GridSearchCV,
32
- StratifiedKFold,
33
- cross_val_score,
34
- train_test_split,
35
- )
36
- from sklearn.pipeline import Pipeline
37
- from sklearn.preprocessing import OneHotEncoder, StandardScaler
38
-
39
- warnings.filterwarnings("ignore")
40
 
41
  st.set_page_config(
42
- page_title="Análise de Personalidade de Consumidores - Reclamações",
43
- page_icon="🧠",
44
  layout="wide",
 
45
  )
46
 
47
- st.markdown(
48
- """
49
- # Tarefa 6 de AEDI - Análise de Personalidade de Consumidores
50
 
51
- **Customer Personality Analysis** - Kaggle
52
 
53
  - **Autor:** Hugo Honda
54
  - **Disciplina:** AEDI - PPCA/UnB
55
- - **Data:** Outubro de 2025
56
 
57
- """
58
- )
59
 
60
- # =============================
61
- # Carregamento e preparação dos dados
62
- # =============================
63
-
64
-
65
- @st.cache_data
66
- def load_data():
67
- """Carrega e prepara os dados de marketing_campaign.csv"""
68
- # Auto-detecta separador
69
- df = pd.read_csv("marketing_campaign.csv", sep=None, engine="python")
70
-
71
- # Normalizar nomes de colunas
72
- df.columns = [c.strip().replace(" ", "_") for c in df.columns]
73
-
74
- # FIX 1: Correct age calculation using actual data collection year
75
- if "Year_Birth" in df.columns and "Dt_Customer" in df.columns:
76
- # Extract year from Dt_Customer to get actual data collection year
77
- df["Dt_Customer"] = pd.to_datetime(df["Dt_Customer"], format="%d-%m-%Y")
78
- df["Data_Year"] = df["Dt_Customer"].dt.year
79
- # Use median year as reference for age calculation
80
- reference_year = df["Data_Year"].median()
81
- df["Age"] = reference_year - df["Year_Birth"]
82
- print(f"Idade calculada usando ano de referência: {reference_year}")
83
- elif "Year_Birth" in df.columns:
84
- # Fallback: use 2014 as reference (middle of dataset range)
85
- df["Age"] = 2014 - df["Year_Birth"]
86
- print("Idade calculada usando ano de referência: 2014")
87
-
88
- df["Total_Dependents"] = df["Kidhome"] + df["Teenhome"]
89
-
90
- # FIX 2: Remove flawed conversion rate calculation
91
- purchase_cols = [c for c in df.columns if "Purchases" in c and c.startswith("Num")]
92
- if purchase_cols:
93
- df["Total_Purchases"] = df[purchase_cols].sum(axis=1)
94
- # Remove the problematic conversion rate that creates artificial correlation
95
- print("Feature 'Conversion_Rate' removida - causava correlação artificial")
96
-
97
- # FIX 3: Better missing value handling
98
- df["Income"].fillna(df["Income"].median(), inplace=True)
99
-
100
- # Age filtering with reasonable bounds
101
- df = df[(df["Age"] >= 18) & (df["Age"] <= 100)]
102
- df = df.dropna(subset=["Complain"])
103
- df["Complain"] = df["Complain"].astype(int)
104
-
105
- # Feature selection
106
- feature_candidates = [
107
- "Age",
108
- "Income",
109
- "Recency",
110
- "MntWines",
111
- "MntFruits",
112
- "MntMeatProducts",
113
- "MntFishProducts",
114
- "MntSweetProducts",
115
- "MntGoldProds",
116
- "NumDealsPurchases",
117
- "NumWebPurchases",
118
- "NumCatalogPurchases",
119
- "NumStorePurchases",
120
- "NumWebVisitsMonth",
121
- "Kidhome",
122
- "Teenhome",
123
- "Total_Dependents",
124
- "Total_Purchases", # Added total purchases instead of conversion rate
125
- "Education",
126
- "Marital_Status",
127
- ]
128
-
129
- features = [f for f in feature_candidates if f in df.columns]
130
- numeric_features = [f for f in features if df[f].dtype in ["int64", "float64"]]
131
- categorical_features = [f for f in features if df[f].dtype == "object"]
132
-
133
- X = df[features].copy()
134
- y = df["Complain"].copy()
135
-
136
- # Preencher NaNs
137
- for col in numeric_features:
138
- if X[col].isnull().any():
139
- X[col].fillna(X[col].median(), inplace=True)
140
-
141
- for col in categorical_features:
142
- if X[col].isnull().any():
143
- X[col].fillna("Unknown", inplace=True)
144
-
145
- return df, X, y, features, categorical_features, numeric_features
146
-
147
-
148
- # Carregar dados
149
- try:
150
- df_raw, X, y, features, categorical_features, numeric_features = load_data()
151
- except Exception as e:
152
- st.error(f"Erro ao carregar dados: {e}")
153
- st.stop()
154
-
155
- # =============================
156
- # Sidebar - Controles
157
- # =============================
158
-
159
- st.sidebar.header("Controles da Análise")
160
-
161
- st.sidebar.subheader("Parâmetros do Modelo")
162
- show_eda = st.sidebar.checkbox("Mostrar EDA", True)
163
- test_size = st.sidebar.slider("Tamanho do conjunto de teste:", 0.1, 0.4, 0.15, 0.05)
164
- random_state = st.sidebar.number_input("Random State:", 1, 1000, 42)
165
-
166
- # =============================
167
- # EDA
168
- # =============================
169
-
170
- if show_eda:
171
- st.header("Análise Exploratória")
172
-
173
- col1, col2, col3 = st.columns(3)
174
- with col1:
175
- st.metric("Observações", len(df_raw))
176
- with col2:
177
- st.metric("Taxa de Queixas", f"{y.mean():.1%}")
178
- with col3:
179
- st.metric("Features", len(features))
180
-
181
- # Enhanced pie chart with better styling
182
- fig_target = px.pie(
183
- values=y.value_counts().values,
184
- names=["Sem Queixa", "Com Queixa"],
185
- title="Distribuição de Queixas - Dataset Desbalanceado",
186
- color_discrete_sequence=["#2E8B57", "#DC143C"],
187
- hole=0.3,
188
- )
189
- fig_target.update_traces(textposition="inside", textinfo="percent+label")
190
- fig_target.update_layout(
191
- font_size=12,
192
- showlegend=True,
193
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
194
- )
195
- st.plotly_chart(fig_target, use_container_width=True)
196
-
197
- # Enhanced correlation analysis
198
- numeric_cols = df_raw.select_dtypes(include=[np.number]).columns.tolist()
199
- if "Complain" in numeric_cols:
200
- corr_with_target = (
201
- df_raw[numeric_cols].corr()["Complain"].sort_values(ascending=False)
202
- )
203
-
204
- st.subheader("Top 10 Correlações com Complain")
205
- st.write(corr_with_target.head(11)[1:])
206
-
207
- # Enhanced correlation visualization
208
- top_corr = corr_with_target.head(11)[1:].abs().sort_values(ascending=True)
209
-
210
- fig_corr = px.bar(
211
- x=top_corr.values,
212
- y=top_corr.index,
213
- orientation="h",
214
- title="Top 10 Correlações com Complain (Valor Absoluto)",
215
- color=top_corr.values,
216
- color_continuous_scale="RdBu_r",
217
- )
218
- fig_corr.update_layout(
219
- xaxis_title="Correlação Absoluta",
220
- yaxis_title="Features",
221
- height=500,
222
- coloraxis_showscale=False,
223
- )
224
- st.plotly_chart(fig_corr, use_container_width=True)
225
-
226
- # Class distribution analysis
227
- class_counts = Counter(y)
228
- st.subheader("Distribuição de Classes")
229
- st.write(
230
- f"**Classe 0 (Sem queixa):** {class_counts[0]} ({class_counts[0] / len(y) * 100:.1f}%)"
231
- )
232
- st.write(
233
- f"**Classe 1 (Com queixa):** {class_counts[1]} ({class_counts[1] / len(y) * 100:.1f}%)"
234
- )
235
-
236
- # =============================
237
- # Modelagem Avançada
238
- # =============================
239
-
240
- st.header("Modelagem Avançada com Múltiplos Algoritmos")
241
-
242
- # FIX 6: Proper train/val/test split to avoid data leakage
243
- # Split into train (70%) / val (15%) / test (15%) for proper threshold tuning
244
- X_temp, X_test, y_temp, y_test = train_test_split(
245
- X, y, test_size=0.15, random_state=random_state, stratify=y
246
- )
247
- X_train, X_val, y_train, y_val = train_test_split(
248
- X_temp,
249
- y_temp,
250
- test_size=0.176,
251
- random_state=random_state,
252
- stratify=y_temp, # 0.176 * 0.85 ≈ 0.15
253
- )
254
 
255
- st.write(
256
- f"**Features:** {len(features)} ({len(numeric_features)} numéricas, {len(categorical_features)} categóricas)"
257
- )
258
- st.write(
259
- f"**Treino:** {X_train.shape[0]} | **Validação:** {X_val.shape[0]} | **Teste:** {X_test.shape[0]}"
260
- )
261
- st.write(
262
- f"**Taxa de queixas - Treino:** {y_train.mean():.2%} | **Val:** {y_val.mean():.2%} | **Teste:** {y_test.mean():.2%}"
263
- )
264
 
265
- # Pipeline de pré-processamento
266
- preprocessor = ColumnTransformer(
267
- transformers=[
268
- ("num", StandardScaler(), numeric_features),
269
- (
270
- "cat",
271
- OneHotEncoder(handle_unknown="ignore", sparse_output=False),
272
- categorical_features,
273
- ),
274
- ],
275
- remainder="drop",
276
  )
277
 
278
- # FIX 4: Remove extreme class weights and use balanced approach
279
- param_grids = {
280
- "LogisticRegression": {
281
- "classifier__C": [0.01, 0.1, 1.0, 10.0],
282
- "classifier__penalty": ["l2"],
283
- "classifier__solver": ["lbfgs"],
284
- "classifier__class_weight": ["balanced"], # Only balanced weights
 
 
 
 
 
 
 
 
 
285
  },
286
- "RandomForest": {
287
- "classifier__n_estimators": [100, 200],
288
- "classifier__max_depth": [6, 8, 10],
289
- "classifier__min_samples_split": [10, 20],
290
- "classifier__min_samples_leaf": [4, 8],
291
- "classifier__class_weight": [
292
- "balanced",
293
- "balanced_subsample",
294
- ], # Only balanced weights
 
 
295
  },
296
- "GradientBoosting": {
297
- "classifier__n_estimators": [100, 150],
298
- "classifier__learning_rate": [0.05, 0.1],
299
- "classifier__max_depth": [3, 4],
300
- "classifier__min_samples_split": [10, 20],
301
- "classifier__subsample": [0.8, 1.0],
 
 
 
 
 
302
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  }
304
 
305
- base_models = {
306
- "LogisticRegression": LogisticRegression(random_state=random_state, max_iter=2000),
307
- "RandomForest": RandomForestClassifier(random_state=random_state, n_jobs=-1),
308
- "GradientBoosting": GradientBoostingClassifier(random_state=random_state),
309
- }
310
-
311
- # Testar diferentes estratégias de balanceamento
312
- sampling_strategies = {
313
- "No Sampling": None,
314
- "SMOTE": SMOTE(random_state=random_state, k_neighbors=5),
315
- }
316
-
317
- best_models = {}
318
- results = []
319
-
320
- st.subheader("Treinamento e Avaliação dos Modelos")
321
-
322
- progress_bar = st.progress(0)
323
- total_models = len(base_models) * len(sampling_strategies)
324
-
325
- for i, (model_name, base_model) in enumerate(base_models.items()):
326
- st.write(f"**{model_name}**")
327
-
328
- for j, (sampling_name, sampler) in enumerate(sampling_strategies.items()):
329
- if sampler is None:
330
- pipeline = Pipeline(
331
- [("preprocessor", preprocessor), ("classifier", base_model)]
332
- )
333
- else:
334
- pipeline = ImbPipeline(
335
- [
336
- ("preprocessor", preprocessor),
337
- ("sampler", sampler),
338
- ("classifier", base_model),
339
- ]
340
- )
341
-
342
- # FIX 5: Use nested cross-validation for proper evaluation
343
- # Inner CV for hyperparameter tuning
344
- inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=random_state)
345
-
346
- grid_search = GridSearchCV(
347
- pipeline,
348
- param_grids[model_name],
349
- cv=inner_cv,
350
- scoring="f1", # F1 balanceia precision e recall
351
- n_jobs=-1,
352
- verbose=0,
353
- error_score="raise",
354
- )
355
-
356
- grid_search.fit(X_train, y_train)
357
- best_pipeline = grid_search.best_estimator_
358
-
359
- # Outer CV for unbiased evaluation
360
- outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=random_state)
361
- cv_scores = cross_val_score(
362
- best_pipeline, X_train, y_train, cv=outer_cv, scoring="f1"
363
- )
364
-
365
- # Also evaluate on validation set
366
- y_val_pred = best_pipeline.predict(X_val)
367
- y_val_proba = best_pipeline.predict_proba(X_val)[:, 1]
368
-
369
- metrics = {
370
- "Model": f"{model_name} + {sampling_name}",
371
- "Best_Params": str(grid_search.best_params_),
372
- "CV_F1_Mean": cv_scores.mean(),
373
- "CV_F1_Std": cv_scores.std(),
374
- "Val_Accuracy": accuracy_score(y_val, y_val_pred),
375
- "Val_Balanced_Acc": balanced_accuracy_score(y_val, y_val_pred),
376
- "Val_Precision": precision_score(y_val, y_val_pred, zero_division=0),
377
- "Val_Recall": recall_score(y_val, y_val_pred, zero_division=0),
378
- "Val_F1": f1_score(y_val, y_val_pred, zero_division=0),
379
- "Val_AUC_ROC": roc_auc_score(y_val, y_val_proba),
380
- "Val_AUC_PR": average_precision_score(y_val, y_val_proba),
381
- }
382
-
383
- results.append(metrics)
384
- best_models[f"{model_name} + {sampling_name}"] = best_pipeline
385
-
386
- st.write(
387
- f" {sampling_name:20s} | CV F1: {cv_scores.mean():.4f}±{cv_scores.std():.4f} | Val Recall: {metrics['Val_Recall']:.4f} | Val F1: {metrics['Val_F1']:.4f}"
388
- )
389
-
390
- progress_bar.progress((i * len(sampling_strategies) + j + 1) / total_models)
391
-
392
- results_df = pd.DataFrame(results).round(4)
393
-
394
- # FIX 7: Select best model based on CV scores (unbiased) + validation confirmation
395
- # Use CV F1 as primary metric, with validation F1 as secondary
396
- results_df["Score"] = results_df["CV_F1_Mean"] * 0.7 + results_df["Val_F1"] * 0.3
397
- best_model_name = results_df.loc[results_df["Score"].idxmax(), "Model"]
398
-
399
- st.subheader("Melhor Modelo Selecionado")
400
- st.write(f"**Modelo:** {best_model_name}")
401
- st.write(
402
- f"**CV F1 Score:** {results_df.loc[results_df['Score'].idxmax(), 'CV_F1_Mean']:.4f} ± {results_df.loc[results_df['Score'].idxmax(), 'CV_F1_Std']:.4f}"
403
- )
404
- st.write(
405
- f"**Val F1 Score:** {results_df.loc[results_df['Score'].idxmax(), 'Val_F1']:.4f}"
406
- )
407
-
408
- st.subheader("Top 5 Modelos (ordenados por Score combinado)")
409
- top_models = results_df.nlargest(5, "Score")[
410
- ["Model", "CV_F1_Mean", "CV_F1_Std", "Val_F1", "Val_Recall", "Score"]
411
- ]
412
- st.dataframe(top_models, use_container_width=True)
413
-
414
- # FIX 8: Proper threshold tuning without data leakage
415
- best_pipeline = best_models[best_model_name]
416
- y_val_proba = best_pipeline.predict_proba(X_val)[:, 1]
417
-
418
- st.subheader("Threshold Tuning")
419
- st.write("**Diagnóstico de Probabilidades no Validation Set:**")
420
- st.write(f"- Min: {y_val_proba.min():.4f} | Max: {y_val_proba.max():.4f}")
421
- st.write(f"- Mean: {y_val_proba.mean():.4f} | Median: {np.median(y_val_proba):.4f}")
422
- st.write(
423
- f"- P95: {np.percentile(y_val_proba, 95):.4f} | P99: {np.percentile(y_val_proba, 99):.4f}"
424
- )
425
- st.write(
426
- f"- Positivos reais no val: {y_val.sum()} de {len(y_val)} ({y_val.mean():.2%})"
427
- )
428
-
429
- # Enhanced probability distribution visualization
430
- fig_prob = go.Figure()
431
-
432
- # Histogram for each class
433
- for class_label in [0, 1]:
434
- mask = y_val == class_label
435
- fig_prob.add_trace(
436
- go.Histogram(
437
- x=y_val_proba[mask], name=f"Classe {class_label}", opacity=0.7, nbinsx=30
438
- )
439
- )
440
-
441
- fig_prob.update_layout(
442
- title="Distribuição de Probabilidades por Classe",
443
- xaxis_title="Probabilidade Predita",
444
- yaxis_title="Frequência",
445
- barmode="overlay",
446
- )
447
- st.plotly_chart(fig_prob, use_container_width=True)
448
-
449
- # FIX 9: Threshold tuning using validation set only (no test set leakage)
450
- precision, recall, thresholds = precision_recall_curve(y_val, y_val_proba)
451
- f1_scores = 2 * (precision * recall) / (precision + recall + 1e-10)
452
- best_threshold_idx = np.argmax(f1_scores)
453
- best_threshold = (
454
- thresholds[best_threshold_idx] if best_threshold_idx < len(thresholds) else 0.5
455
- )
456
-
457
- st.write("**Threshold Tuning (apenas no conjunto de VALIDAÇÃO):**")
458
- st.write(f"- Threshold ótimo: {best_threshold:.4f}")
459
- st.write(f"- F1 esperado no threshold: {f1_scores[best_threshold_idx]:.4f}")
460
- st.write(f"- Precision no threshold: {precision[best_threshold_idx]:.4f}")
461
- st.write(f"- Recall no threshold: {recall[best_threshold_idx]:.4f}")
462
-
463
- # Final evaluation on test set
464
- y_test_proba = best_pipeline.predict_proba(X_test)[:, 1]
465
- y_test_pred_default = best_pipeline.predict(X_test)
466
- y_test_pred_tuned = (y_test_proba >= best_threshold).astype(int)
467
-
468
- st.subheader("Resultados Finais no Conjunto de Teste")
469
-
470
- col1, col2 = st.columns(2)
471
-
472
- with col1:
473
- st.write("**Threshold Padrão (0.5):**")
474
- st.write(f"- Recall: {recall_score(y_test, y_test_pred_default):.4f}")
475
- st.write(
476
- f"- Precision: {precision_score(y_test, y_test_pred_default, zero_division=0):.4f}"
477
- )
478
- st.write(f"- F1-Score: {f1_score(y_test, y_test_pred_default):.4f}")
479
- st.write(
480
- f"- Balanced Accuracy: {balanced_accuracy_score(y_test, y_test_pred_default):.4f}"
481
- )
482
-
483
- with col2:
484
- st.write(f"**Threshold Otimizado ({best_threshold:.4f}):**")
485
- st.write(f"- Recall: {recall_score(y_test, y_test_pred_tuned):.4f}")
486
- st.write(
487
- f"- Precision: {precision_score(y_test, y_test_pred_tuned, zero_division=0):.4f}"
488
- )
489
- st.write(f"- F1-Score: {f1_score(y_test, y_test_pred_tuned):.4f}")
490
- st.write(
491
- f"- Balanced Accuracy: {balanced_accuracy_score(y_test, y_test_pred_tuned):.4f}"
492
- )
493
- st.write(f"- AUC-ROC: {roc_auc_score(y_test, y_test_proba):.4f}")
494
- st.write(f"- AUC-PR: {average_precision_score(y_test, y_test_proba):.4f}")
495
-
496
- # Enhanced ROC Curve
497
- fpr, tpr, _ = roc_curve(y_test, y_test_proba)
498
- roc_auc = auc(fpr, tpr)
499
-
500
- fig_roc = go.Figure()
501
- fig_roc.add_trace(
502
- go.Scatter(
503
- x=fpr,
504
- y=tpr,
505
- mode="lines",
506
- name=f"ROC Curve (AUC = {roc_auc:.3f})",
507
- line=dict(color="blue", width=3),
508
- )
509
- )
510
- fig_roc.add_trace(
511
- go.Scatter(
512
- x=[0, 1],
513
- y=[0, 1],
514
- mode="lines",
515
- name="Random Classifier",
516
- line=dict(color="red", dash="dash"),
517
- )
518
- )
519
-
520
- fig_roc.update_layout(
521
- title="Curva ROC - Avaliação do Modelo",
522
- xaxis_title="Taxa de Falsos Positivos",
523
- yaxis_title="Taxa de Verdadeiros Positivos",
524
- width=600,
525
- height=500,
526
- )
527
- st.plotly_chart(fig_roc, use_container_width=True)
528
-
529
- # Enhanced confusion matrix heatmap
530
- cm = confusion_matrix(y_test, y_test_pred_tuned)
531
- st.write("**Matriz de Confusão:**")
532
- st.write(" Pred 0 Pred 1")
533
- st.write(f"Real 0 {cm[0, 0]:6d} {cm[0, 1]:6d}")
534
- st.write(f"Real 1 {cm[1, 0]:6d} {cm[1, 1]:6d}")
535
-
536
- fig_cm = px.imshow(
537
- cm,
538
- text_auto=True,
539
- aspect="auto",
540
- title="Matriz de Confusão - Threshold Otimizado",
541
- labels=dict(x="Predito", y="Real", color="Contagem"),
542
- color_continuous_scale="Blues",
543
- )
544
- fig_cm.update_layout(xaxis_title="Predito", yaxis_title="Real", width=500, height=400)
545
- st.plotly_chart(fig_cm, use_container_width=True)
546
-
547
- # Feature importance
548
- preprocessor_fitted = best_pipeline.named_steps["preprocessor"]
549
- classifier = best_pipeline.named_steps["classifier"]
550
-
551
- feature_names = numeric_features + list(
552
- preprocessor_fitted.named_transformers_["cat"].get_feature_names_out(
553
- categorical_features
554
- )
555
- )
556
-
557
- if hasattr(classifier, "feature_importances_"):
558
- importances = classifier.feature_importances_
559
- elif hasattr(classifier, "coef_"):
560
- importances = np.abs(classifier.coef_[0])
561
  else:
562
- importances = None
563
-
564
- if importances is not None:
565
- # Get top 15 features
566
- top_features = pd.DataFrame(
567
- {"feature": feature_names, "importance": importances}
568
- ).nlargest(15, "importance")
569
-
570
- st.subheader("Top 15 Features Mais Importantes")
571
- st.dataframe(top_features, use_container_width=True)
572
-
573
- # Enhanced feature importance visualization
574
- fig_importance = px.bar(
575
- top_features,
576
- x="importance",
577
- y="feature",
578
- orientation="h",
579
- title=f"Feature Importance - {best_model_name}",
580
- color="importance",
581
- color_continuous_scale="viridis",
582
- )
583
- fig_importance.update_layout(
584
- xaxis_title="Importância",
585
- yaxis_title="Features",
586
- height=600,
587
- coloraxis_showscale=False,
588
- )
589
- st.plotly_chart(fig_importance, use_container_width=True)
590
 
591
  st.markdown("---")
592
- st.caption("PPCA/UnB | Outubro 2025")
 
1
  #!/usr/bin/env python
2
  # coding: utf-8
3
 
 
 
 
 
 
 
 
4
  import streamlit as st
5
+ from pathlib import Path
6
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  st.set_page_config(
9
+ page_title="Prova Final - AEDI",
10
+ page_icon="📊",
11
  layout="wide",
12
+ initial_sidebar_state="expanded",
13
  )
14
 
15
+ st.markdown("""
16
+ # Prova Final de Análise Estatística de Dados e Informações
 
17
 
18
+ **Novembro - 2025**
19
 
20
  - **Autor:** Hugo Honda
21
  - **Disciplina:** AEDI - PPCA/UnB
 
22
 
23
+ ---
 
24
 
25
+ ## Navegação por Questões
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ Selecione a questão que deseja visualizar no menu lateral.
28
+ """)
29
+
30
+ # Sidebar para navegação
31
+ st.sidebar.title("📚 Navegação")
32
+ st.sidebar.markdown("---")
 
 
 
33
 
34
+ questao_selecionada = st.sidebar.radio(
35
+ "Selecione a Questão:",
36
+ options=[1, 2, 3, 4],
37
+ format_func=lambda x: f"Questão {x}",
38
+ index=0
 
 
 
 
 
 
39
  )
40
 
41
+ st.sidebar.markdown("---")
42
+ st.sidebar.markdown("### 📋 Descrição das Questões")
43
+
44
+ questoes_info = {
45
+ 1: {
46
+ "titulo": "Regressão Linear - Preços de Imóveis",
47
+ "pontos": "2,5 pontos",
48
+ "dataset": "King County House Sales",
49
+ "tecnica": "Regressão Linear",
50
+ "descricao": """
51
+ - Análise Descritiva dos Dados (20%)
52
+ - Construção do Modelo de Regressão Linear (30%)
53
+ - Interpretação dos Resultados (10%)
54
+ - Ajustes no Modelo (30%)
55
+ - Tomada de Decisão (10%)
56
+ """
57
  },
58
+ 2: {
59
+ "titulo": "Regressão Logística - Cancelamento de Reservas",
60
+ "pontos": "2,5 pontos",
61
+ "dataset": "Hotel Booking Demand",
62
+ "tecnica": "Regressão Logística",
63
+ "descricao": """
64
+ - Análise Descritiva dos Dados (10%)
65
+ - Modelo de Regressão Logística (60%)
66
+ - Análise das Features (20%)
67
+ - Justificativa do Método (10%)
68
+ """
69
  },
70
+ 3: {
71
+ "titulo": "ANOVA - Vendas de Varejo Online",
72
+ "pontos": "2,0 pontos",
73
+ "dataset": "Online Retail",
74
+ "tecnica": "ANOVA",
75
+ "descricao": """
76
+ - Análise Descritiva dos Dados (10%)
77
+ - Comparação entre Países (ANOVA) (40%)
78
+ - Ajustes no Modelo de ANOVA (40%)
79
+ - Interpretação e Tomada de Decisão (10%)
80
+ """
81
  },
82
+ 4: {
83
+ "titulo": "Risco de Crédito - ML e Clustering",
84
+ "pontos": "3,0 pontos",
85
+ "dataset": "Credit Risk",
86
+ "tecnica": "ML, SHAP, K-Means, DBSCAN",
87
+ "descricao": """
88
+ - Discussão sobre o Problema (10%)
89
+ - Análise Descritiva dos Dados (15%)
90
+ - Definição e Seleção dos Modelos (30%)
91
+ - Explicabilidade das Variáveis - SHAP (25%)
92
+ - Análise Não Supervisionada (15%)
93
+ - Tomada de Decisão Estratégica (10%)
94
+ """
95
+ }
96
  }
97
 
98
+ info = questoes_info[questao_selecionada]
99
+ st.sidebar.markdown(f"""
100
+ **{info['titulo']}**
101
+
102
+ - **Pontos:** {info['pontos']}
103
+ - **Dataset:** {info['dataset']}
104
+ - **Técnica:** {info['tecnica']}
105
+ """)
106
+ st.sidebar.markdown(info['descricao'])
107
+
108
+ # Carregar e executar o streamlit app da questão selecionada
109
+ base_path = Path(__file__).parent.parent
110
+ questao_dir = base_path / f"questao-{questao_selecionada}"
111
+ streamlit_file = questao_dir / "src" / "streamlit_app.py"
112
+
113
+ if streamlit_file.exists():
114
+ st.markdown(f"## Questão {questao_selecionada} - {info['titulo']}")
115
+ st.markdown("---")
116
+
117
+ # Mudar para o diretório da questão e executar
118
+ original_cwd = os.getcwd()
119
+ os.chdir(str(questao_dir))
120
+
121
+ try:
122
+ # Ler o código do streamlit app
123
+ with open(streamlit_file, 'r', encoding='utf-8') as f:
124
+ code = f.read()
125
+
126
+ # Processar o código para remover configurações duplicadas
127
+ lines = code.split('\n')
128
+ exec_lines = []
129
+ skip_section = False
130
+
131
+ for line in lines:
132
+ # Pular st.set_page_config
133
+ if 'st.set_page_config' in line:
134
+ skip_section = True
135
+ continue
136
+ if skip_section and (line.strip() == '' or line.startswith('st.markdown')):
137
+ if '"""' in line or "'''" in line:
138
+ # Encontrar fim do markdown
139
+ if line.count('"""') == 2 or line.count("'''") == 2:
140
+ skip_section = False
141
+ continue
142
+ if skip_section:
143
+ continue
144
+
145
+ # Pular markdown inicial duplicado (primeiras 10 linhas)
146
+ if len(exec_lines) < 5 and ('Questão' in line or 'Regressão' in line or 'ANOVA' in line or 'Risco' in line):
147
+ if '#' in line:
148
+ continue
149
+
150
+ exec_lines.append(line)
151
+
152
+ exec_code = '\n'.join(exec_lines)
153
+
154
+ # Criar namespace com imports
155
+ exec_globals = {'st': st, '__file__': str(streamlit_file)}
156
+
157
+ # Imports comuns
158
+ import warnings
159
+ import numpy as np
160
+ import pandas as pd
161
+ import plotly.express as px
162
+ import plotly.graph_objects as go
163
+
164
+ exec_globals.update({
165
+ 'warnings': warnings,
166
+ 'np': np,
167
+ 'pd': pd,
168
+ 'px': px,
169
+ 'go': go,
170
+ })
171
+
172
+ # Imports específicos por questão
173
+ if questao_selecionada == 1:
174
+ from sklearn.linear_model import LinearRegression
175
+ from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
176
+ from sklearn.model_selection import train_test_split
177
+ from scipy import stats
178
+ from statsmodels.stats.diagnostic import het_breuschpagan
179
+ from statsmodels.stats.stattools import durbin_watson
180
+ from statsmodels.stats.outliers_influence import variance_inflation_factor
181
+ import statsmodels.api as sm
182
+ exec_globals.update(locals())
183
+ elif questao_selecionada == 2:
184
+ from sklearn.linear_model import LogisticRegression
185
+ from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score,
186
+ roc_auc_score, roc_curve, confusion_matrix, classification_report)
187
+ from sklearn.model_selection import train_test_split
188
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
189
+ from imblearn.over_sampling import SMOTE
190
+ exec_globals.update(locals())
191
+ elif questao_selecionada == 3:
192
+ from scipy import stats
193
+ import statsmodels.formula.api as smf
194
+ from statsmodels.stats.anova import anova_lm
195
+ from statsmodels.stats.stattools import durbin_watson
196
+ from statsmodels.stats.oneway import anova_oneway
197
+ exec_globals.update(locals())
198
+ elif questao_selecionada == 4:
199
+ from sklearn.linear_model import LogisticRegression
200
+ from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
201
+ from sklearn.tree import DecisionTreeClassifier
202
+ from sklearn.svm import SVC
203
+ from xgboost import XGBClassifier
204
+ from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score,
205
+ roc_auc_score, roc_curve, confusion_matrix)
206
+ from sklearn.model_selection import train_test_split
207
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
208
+ from sklearn.cluster import KMeans, DBSCAN
209
+ from imblearn.over_sampling import SMOTE
210
+ try:
211
+ import shap
212
+ exec_globals['shap'] = shap
213
+ except:
214
+ pass
215
+ exec_globals.update(locals())
216
+
217
+ # Executar o código
218
+ exec(exec_code, exec_globals)
219
+
220
+ except Exception as e:
221
+ st.error(f"Erro ao executar questão {questao_selecionada}: {str(e)}")
222
+ import traceback
223
+ with st.expander("Detalhes do erro"):
224
+ st.code(traceback.format_exc())
225
+ st.info(f"""
226
+ **Para executar esta questão diretamente:**
227
+
228
+ ```bash
229
+ cd questao-{questao_selecionada}
230
+ streamlit run src/streamlit_app.py
231
+ ```
232
+ """)
233
+ finally:
234
+ os.chdir(original_cwd)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  else:
236
+ st.error(f"Arquivo não encontrado: {streamlit_file}")
237
+ st.info(f"""
238
+ **Para executar esta questão diretamente:**
239
+
240
+ ```bash
241
+ cd questao-{questao_selecionada}
242
+ streamlit run src/streamlit_app.py
243
+ ```
244
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  st.markdown("---")
247
+ st.caption("PPCA/UnB | Novembro 2025")