Spaces:
Sleeping
Sleeping
update
Browse files- .gitignore +50 -0
- DESCRICAO.md +131 -0
- Dockerfile +4 -1
- README.md +126 -14
- Tarefa_6.pdf +0 -0
- main.ipynb +0 -0
- questao-1/kc_house_data.csv +3 -0
- questao-1/questao-1.ipynb +0 -0
- questao-1/src/streamlit_app.py +614 -0
- questao-2/hotel_bookings.csv +3 -0
- questao-2/questao-2.ipynb +0 -0
- questao-2/src/streamlit_app.py +627 -0
- questao-3/online_retail_II.xlsx +3 -0
- questao-3/questao-3.ipynb +0 -0
- questao-3/src/streamlit_app.py +642 -0
- marketing_campaign.csv → questao-4/credit_customers.csv +2 -2
- questao-4/questao-4.ipynb +0 -0
- questao-4/src/streamlit_app.py +912 -0
- regressao_logistica_churn_bancario.ipynb +0 -0
- requirements.txt +3 -0
- src/__pycache__/streamlit_app.cpython-313.pyc +0 -0
- src/streamlit_app.py +220 -565
.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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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
|
| 13 |
-
|
| 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="
|
| 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 |
-
**
|
| 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 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 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 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
"cat",
|
| 271 |
-
OneHotEncoder(handle_unknown="ignore", sparse_output=False),
|
| 272 |
-
categorical_features,
|
| 273 |
-
),
|
| 274 |
-
],
|
| 275 |
-
remainder="drop",
|
| 276 |
)
|
| 277 |
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
"
|
| 284 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
},
|
| 286 |
-
|
| 287 |
-
"
|
| 288 |
-
"
|
| 289 |
-
"
|
| 290 |
-
"
|
| 291 |
-
"
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
| 295 |
},
|
| 296 |
-
|
| 297 |
-
"
|
| 298 |
-
"
|
| 299 |
-
"
|
| 300 |
-
"
|
| 301 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
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 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 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 |
|
|
|
|
| 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")
|