|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import scipy.stats as stats |
|
|
import statsmodels.api as sm |
|
|
import matplotlib.patches as mpatches |
|
|
from statsmodels.formula.api import ols |
|
|
from sklearn.model_selection import train_test_split |
|
|
from statsmodels.api import OLS, add_constant |
|
|
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error |
|
|
from statsmodels.stats.outliers_influence import variance_inflation_factor |
|
|
from statsmodels.stats.diagnostic import het_breuschpagan |
|
|
from scipy.stats import shapiro |
|
|
from scipy.stats import anderson |
|
|
from scipy.stats import kruskal |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from plotly.subplots import make_subplots |
|
|
import seaborn as sns |
|
|
from datetime import datetime |
|
|
import base64 |
|
|
|
|
|
st.set_page_config( |
|
|
page_title="📊 Análise Estatística Avançada", |
|
|
page_icon="📊", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.main > div { |
|
|
padding-top: 2rem; |
|
|
} |
|
|
.stApp { |
|
|
background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%); |
|
|
color: #ffffff; |
|
|
} |
|
|
.block-container { |
|
|
background: rgba(30, 30, 30, 0.95); |
|
|
border-radius: 20px; |
|
|
padding: 2rem; |
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.4); |
|
|
margin: 1rem; |
|
|
backdrop-filter: blur(10px); |
|
|
color: #ffffff; |
|
|
} |
|
|
.metric-container { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 15px; |
|
|
padding: 1rem; |
|
|
margin: 0.5rem 0; |
|
|
color: white; |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.success-box { |
|
|
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); |
|
|
border-radius: 15px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
color: white; |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.warning-box { |
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
|
|
border-radius: 15px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
color: white; |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.info-box { |
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
|
|
border-radius: 15px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
color: white; |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
|
|
|
.sidebar .sidebar-content { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 15px; |
|
|
} |
|
|
|
|
|
h1, h2, h3 { |
|
|
color: white; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.stSelectbox > div > div { |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.stButton > button { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
border-radius: 25px; |
|
|
border: none; |
|
|
padding: 0.5rem 2rem; |
|
|
font-weight: 600; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.stButton > button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.2); |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<div style="text-align: center; margin-bottom: 2rem;"> |
|
|
<h1 style="font-size: 3rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
margin-bottom: 0.5rem; color: white;"> |
|
|
📊 Análise Estatística Avançada |
|
|
</h1> |
|
|
<p style="font-size: 1.2rem; color: #7f8c8d; margin-top: 0;"> |
|
|
ANOVA & Regressão Linear com Diagnósticos Completos |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
@st.cache_data |
|
|
def load_data(): |
|
|
try: |
|
|
file_path = "src/AmesHousing.csv" |
|
|
return pd.read_csv(file_path) |
|
|
except: |
|
|
st.error("📂 Arquivo não encontrado. Verifique o caminho do dataset.") |
|
|
return None |
|
|
|
|
|
df = load_data() |
|
|
|
|
|
if df is not None: |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
with col1: |
|
|
st.markdown(""" |
|
|
<div class="metric-container"> |
|
|
<h3>📏 Linhas</h3> |
|
|
<h2>{}</h2> |
|
|
</div> |
|
|
""".format(len(df)), unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
st.markdown(""" |
|
|
<div class="metric-container"> |
|
|
<h3>📊 Colunas</h3> |
|
|
<h2>{}</h2> |
|
|
</div> |
|
|
""".format(len(df.columns)), unsafe_allow_html=True) |
|
|
|
|
|
with col3: |
|
|
st.markdown(""" |
|
|
<div class="metric-container"> |
|
|
<h3>💰 Preço Médio</h3> |
|
|
<h2>${:,.0f}</h2> |
|
|
</div> |
|
|
""".format(df['SalePrice'].mean()), unsafe_allow_html=True) |
|
|
|
|
|
with col4: |
|
|
st.markdown(""" |
|
|
<div class="metric-container"> |
|
|
<h3>🏠 Preço Mediano</h3> |
|
|
<h2>${:,.0f}</h2> |
|
|
</div> |
|
|
""".format(df['SalePrice'].median()), unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.expander("🔍 Pré-visualização dos Dados", expanded=False): |
|
|
st.dataframe(df.head(10), use_container_width=True) |
|
|
|
|
|
|
|
|
fig = px.histogram(df, x='SalePrice', nbins=50, |
|
|
title='📈 Distribuição dos Preços de Venda', |
|
|
color_discrete_sequence=['#667eea']) |
|
|
fig.update_layout( |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
font=dict(color='#2c3e50') |
|
|
) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
qualitativas = df.select_dtypes(include='object').columns.tolist() |
|
|
quantitativas = df.select_dtypes(include=['float64', 'int64']).drop(columns=['SalePrice'], errors='ignore').columns.tolist() |
|
|
st.sidebar.markdown(""" |
|
|
<div style="text-align: center; padding: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 15px; margin-bottom: 1rem; color: white;"> |
|
|
<h2>⚙️ Configurações</h2> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
st.sidebar.markdown("### 📊 Variáveis para ANOVA") |
|
|
anova_vars = st.sidebar.multiselect( |
|
|
"Selecione variáveis qualitativas:", |
|
|
qualitativas, |
|
|
help="Escolha variáveis categóricas para análise ANOVA" |
|
|
) |
|
|
st.sidebar.markdown('### 🔧 Variáveis independentes para regressão') |
|
|
regressao_vars = st.sidebar.multiselect( |
|
|
"Selecione as variáveis:", |
|
|
options=df.columns.drop('SalePrice'), |
|
|
help="Escolha quatro a seis variáveis explicativas, sendo obrigatório incluir ao menos uma variável contínua e uma categórica " |
|
|
) |
|
|
|
|
|
st.sidebar.markdown("---") |
|
|
st.sidebar.markdown("### ⚙️ Configurações Avançadas") |
|
|
|
|
|
|
|
|
show_advanced_plots = st.sidebar.checkbox("📊 Gráficos Avançados", value=True) |
|
|
show_correlations = st.sidebar.checkbox("🔗 Matriz de Correlação", value=False) |
|
|
auto_refresh = st.sidebar.checkbox("🔄 Atualização Automática", value=False) |
|
|
|
|
|
|
|
|
st.sidebar.markdown("---") |
|
|
st.sidebar.markdown("### ℹ️ Informações") |
|
|
st.sidebar.info(f"Última atualização: {datetime.now().strftime('%H:%M:%S')}") |
|
|
st.sidebar.info(f"Registros carregados: {len(df):,}") |
|
|
|
|
|
|
|
|
if show_correlations and regressao_vars: |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; margin: 2rem 0;"> |
|
|
<h2 style="color: #2c3e50;">🔗 Matriz de Correlação</h2> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
numeric_vars = [var for var in regressao_vars if df[var].dtype in ['float64', 'int64']] |
|
|
if len(numeric_vars) > 1: |
|
|
corr_matrix = df[numeric_vars + ['SalePrice']].corr() |
|
|
|
|
|
|
|
|
fig = px.imshow( |
|
|
corr_matrix, |
|
|
title="🔥 Mapa de Calor - Correlações", |
|
|
color_continuous_scale="RdBu_r", |
|
|
aspect="auto", |
|
|
text_auto=True |
|
|
) |
|
|
fig.update_layout( |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
font=dict(color='#2c3e50'), |
|
|
height=600 |
|
|
) |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
else: |
|
|
st.info("🔍 Selecione pelo menos 2 variáveis numéricas para visualizar correlações.") |
|
|
|
|
|
|
|
|
if anova_vars: |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; margin: 2rem 0;"> |
|
|
<h2 style="color: white;">🔬 Análise ANOVA</h2> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
for var in anova_vars: |
|
|
with st.container(): |
|
|
st.markdown(f""" |
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 15px; padding: 1rem; margin: 1rem 0; color: white;"> |
|
|
<h3>📋 ANOVA - {var}</h3> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
df_anova = df[[var, 'SalePrice']].dropna() |
|
|
if df_anova[var].nunique() < 2: |
|
|
st.warning(f"⚠️ '{var}' não tem categorias suficientes para ANOVA.") |
|
|
continue |
|
|
|
|
|
formula = f'SalePrice ~ C(Q("{var}"))' |
|
|
modelo = ols(formula, data=df_anova).fit() |
|
|
anova_result = sm.stats.anova_lm(modelo, typ=2) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.markdown("#### 📊 Resultados ANOVA") |
|
|
st.dataframe(anova_result.round(4), use_container_width=True) |
|
|
|
|
|
with col2: |
|
|
|
|
|
shapiro_result = shapiro(modelo.resid) |
|
|
ad_result = anderson(modelo.resid) |
|
|
|
|
|
st.markdown("#### 🧪 Testes de Diagnóstico") |
|
|
|
|
|
if shapiro_result.pvalue > 0.05: |
|
|
st.markdown(f""" |
|
|
<div class="success-box"> |
|
|
<strong>✅ Shapiro-Wilk</strong><br> |
|
|
p-valor: {shapiro_result.pvalue:.4f}<br> |
|
|
Normalidade confirmada |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
else: |
|
|
st.markdown(f""" |
|
|
<div class="warning-box"> |
|
|
<strong>⚠️ Shapiro-Wilk</strong><br> |
|
|
p-valor: {shapiro_result.pvalue:.4f}<br> |
|
|
Normalidade rejeitada |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
grupos = [g["SalePrice"].values for _, g in df_anova.groupby(var)] |
|
|
levene_result = stats.levene(*grupos) |
|
|
kruskal_result = stats.kruskal(*grupos) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
if levene_result.pvalue > 0.05: |
|
|
st.markdown(f""" |
|
|
<div class="success-box"> |
|
|
<strong>✅ Teste de Levene</strong><br> |
|
|
p-valor: {levene_result.pvalue:.4f}<br> |
|
|
Homocedasticidade confirmada |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
else: |
|
|
st.markdown(f""" |
|
|
<div class="warning-box"> |
|
|
<strong>⚠️ Teste de Levene</strong><br> |
|
|
p-valor: {levene_result.pvalue:.4f}<br> |
|
|
Heterocedasticidade detectada |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
if kruskal_result.pvalue < 0.05: |
|
|
st.markdown(f""" |
|
|
<div class="warning-box"> |
|
|
<strong>📊 Kruskal-Wallis</strong><br> |
|
|
p-valor: {kruskal_result.pvalue:.4f}<br> |
|
|
Diferenças significativas |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
else: |
|
|
st.markdown(f""" |
|
|
<div class="info-box"> |
|
|
<strong>📊 Kruskal-Wallis</strong><br> |
|
|
p-valor: {kruskal_result.pvalue:.4f}<br> |
|
|
Sem diferenças significativas |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
medianas = df_anova.groupby(var)['SalePrice'].median().sort_values(ascending=False) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.markdown("#### 💰 Medianas por Categoria") |
|
|
medianas_df = pd.DataFrame({ |
|
|
'Categoria': medianas.index, |
|
|
'Mediana (US$)': [f"${x:,.0f}" for x in medianas.values] |
|
|
}) |
|
|
st.dataframe(medianas_df, use_container_width=True, hide_index=True) |
|
|
|
|
|
with col2: |
|
|
|
|
|
mediana_max = medianas.max() |
|
|
categorias_top = medianas[medianas == mediana_max].index.tolist() |
|
|
|
|
|
if len(categorias_top) == 1: |
|
|
st.markdown(f""" |
|
|
<div class="success-box"> |
|
|
<h4>🏆 Categoria Mais Vantajosa</h4> |
|
|
<strong>{categorias_top[0]}</strong><br> |
|
|
Mediana: ${mediana_max:,.0f} |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
else: |
|
|
categorias_str = ", ".join(categorias_top) |
|
|
st.markdown(f""" |
|
|
<div class="success-box"> |
|
|
<h4>🏆 Categorias Mais Vantajosas (Empate)</h4> |
|
|
<strong>{categorias_str}</strong><br> |
|
|
Mediana: ${mediana_max:,.0f} |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if show_advanced_plots: |
|
|
|
|
|
fig = make_subplots( |
|
|
rows=2, cols=2, |
|
|
subplot_titles=( |
|
|
f'🎻 Distribuição por {var}', |
|
|
f'📊 Boxplot por {var}', |
|
|
f'📈 Médias por {var}', |
|
|
f'🎯 Contagem por {var}' |
|
|
), |
|
|
specs=[[{"secondary_y": False}, {"secondary_y": False}], |
|
|
[{"secondary_y": False}, {"secondary_y": False}]] |
|
|
) |
|
|
|
|
|
|
|
|
for i, category in enumerate(df_anova[var].unique()): |
|
|
data = df_anova[df_anova[var] == category]['SalePrice'] |
|
|
fig.add_trace( |
|
|
go.Violin(y=data, name=str(category), box_visible=True), |
|
|
row=1, col=1 |
|
|
) |
|
|
|
|
|
|
|
|
for i, category in enumerate(df_anova[var].unique()): |
|
|
data = df_anova[df_anova[var] == category]['SalePrice'] |
|
|
fig.add_trace( |
|
|
go.Box(y=data, name=str(category)), |
|
|
row=1, col=2 |
|
|
) |
|
|
|
|
|
|
|
|
means = df_anova.groupby(var)['SalePrice'].mean().reset_index() |
|
|
fig.add_trace( |
|
|
go.Bar(x=means[var], y=means['SalePrice'], |
|
|
name='Médias', marker_color='lightblue'), |
|
|
row=2, col=1 |
|
|
) |
|
|
|
|
|
|
|
|
counts = df_anova[var].value_counts().reset_index() |
|
|
fig.add_trace( |
|
|
go.Bar(x=counts[var], y=counts['count'], |
|
|
name='Contagens', marker_color='lightgreen'), |
|
|
row=2, col=2 |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
height=800, |
|
|
showlegend=False, |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
font=dict(color='white') |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
else: |
|
|
|
|
|
fig = px.violin(df_anova, x=var, y='SalePrice', |
|
|
title=f'🎻 Distribuição de Preços por {var}', |
|
|
color=var, |
|
|
color_discrete_sequence=px.colors.qualitative.Set3) |
|
|
|
|
|
fig.update_layout( |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
font=dict(color='white'), |
|
|
height=500 |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("---") |
|
|
|
|
|
if regressao_vars: |
|
|
|
|
|
st.markdown(""" |
|
|
<div style='text-align: center; padding: 1rem; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 10px; margin-bottom: 2rem;'> |
|
|
<h2 style='color: white; margin: 0;'>📊 Regressão Linear Múltipla</h2> |
|
|
<p style='color: #f0f0f0; margin: 0.5rem 0 0 0;'>Análise Estatística Avançada</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
df_reg = df[['SalePrice'] + regressao_vars].dropna().copy() |
|
|
if df_reg.empty: |
|
|
st.error("⚠️ Não há dados suficientes para regressão com as variáveis selecionadas.") |
|
|
else: |
|
|
df_reg['LogSalePrice'] = np.log(df_reg['SalePrice']) |
|
|
|
|
|
|
|
|
progress_bar = st.progress(0) |
|
|
status_text = st.empty() |
|
|
|
|
|
status_text.text('🔄 Preparando dados...') |
|
|
progress_bar.progress(20) |
|
|
|
|
|
|
|
|
df_reg = pd.get_dummies(df_reg, columns=[v for v in regressao_vars if df_reg[v].dtype == 'object'], drop_first=True) |
|
|
|
|
|
X = df_reg.drop(columns=['SalePrice', 'LogSalePrice']) |
|
|
y = df_reg['LogSalePrice'] |
|
|
|
|
|
|
|
|
X = X.astype(float) |
|
|
y = y.astype(float) |
|
|
|
|
|
status_text.text('🎯 Treinando modelo...') |
|
|
progress_bar.progress(50) |
|
|
|
|
|
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) |
|
|
X_train_const = add_constant(X_train) |
|
|
X_test_const = add_constant(X_test) |
|
|
|
|
|
modelo = OLS(y_train, X_train_const).fit() |
|
|
|
|
|
status_text.text('✅ Modelo treinado!') |
|
|
progress_bar.progress(100) |
|
|
|
|
|
|
|
|
import time |
|
|
time.sleep(1) |
|
|
progress_bar.empty() |
|
|
status_text.empty() |
|
|
|
|
|
|
|
|
tab1, tab2, tab3, tab4 = st.tabs(["📈 Resumo do Modelo", "🔍 Diagnósticos", "📊 Avaliação", "💡 Interpretação"]) |
|
|
|
|
|
with tab1: |
|
|
st.markdown("### 📋 Resumo Estatístico") |
|
|
|
|
|
|
|
|
with st.expander("🔍 Ver Resumo Completo do Modelo", expanded=False): |
|
|
st.text(modelo.summary()) |
|
|
|
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
r2_adj = modelo.rsquared_adj |
|
|
f_stat = modelo.fvalue |
|
|
n_obs = int(modelo.nobs) |
|
|
|
|
|
with col1: |
|
|
st.metric( |
|
|
label="R² Ajustado", |
|
|
value=f"{r2_adj:.3f}", |
|
|
delta=f"{r2_adj*100:.1f}% da variância explicada" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.metric( |
|
|
label="Estatística F", |
|
|
value=f"{f_stat:.2f}", |
|
|
delta="Significância do modelo" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
st.metric( |
|
|
label="Observações", |
|
|
value=f"{n_obs}", |
|
|
delta="Tamanho da amostra" |
|
|
) |
|
|
|
|
|
with tab2: |
|
|
st.markdown("### 🔍 Diagnóstico dos Resíduos") |
|
|
|
|
|
residuos = modelo.resid |
|
|
fitted = modelo.fittedvalues |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
|
|
|
fig, ax = plt.subplots(figsize=(8, 6)) |
|
|
scatter = ax.scatter(fitted, residuos, alpha=0.6, c='steelblue', edgecolor='white', s=50) |
|
|
ax.axhline(0, color='red', linestyle='--', linewidth=2, alpha=0.8) |
|
|
ax.set_xlabel("Valores Ajustados", fontsize=12) |
|
|
ax.set_ylabel("Resíduos", fontsize=12) |
|
|
ax.set_title("Resíduos vs Valores Ajustados", fontsize=14, fontweight='bold') |
|
|
ax.grid(True, alpha=0.3) |
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
with col2: |
|
|
|
|
|
from scipy import stats |
|
|
fig, ax = plt.subplots(figsize=(8, 6)) |
|
|
stats.probplot(residuos, dist="norm", plot=ax) |
|
|
ax.set_title("Q-Q Plot (Normalidade)", fontsize=14, fontweight='bold') |
|
|
ax.grid(True, alpha=0.3) |
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
st.markdown("### 🧪 Testes Estatísticos") |
|
|
|
|
|
shapiro_test = shapiro(residuos) |
|
|
het_test = het_breuschpagan(residuos, X_train_const) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
normalidade_status = "✅ Normal" if shapiro_test.pvalue >= 0.05 else "❌ Não Normal" |
|
|
cor_normalidade = "success" if shapiro_test.pvalue >= 0.05 else "error" |
|
|
|
|
|
st.markdown(f""" |
|
|
<div style='padding: 1rem; border-radius: 8px; background-color: {"#d4edda" if shapiro_test.pvalue >= 0.05 else "#f8d7da"}; |
|
|
border: 1px solid {"#c3e6cb" if shapiro_test.pvalue >= 0.05 else "#f5c6cb"};'> |
|
|
<h4 style='margin: 0; color: {"#155724" if shapiro_test.pvalue >= 0.05 else "#721c24"};'> |
|
|
🎯 Teste Shapiro-Wilk |
|
|
</h4> |
|
|
<p style='margin: 0.5rem 0 0 0; color: {"#155724" if shapiro_test.pvalue >= 0.05 else "#721c24"};'> |
|
|
<strong>Status:</strong> {normalidade_status}<br> |
|
|
<strong>P-valor:</strong> {shapiro_test.pvalue:.4f} |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
hetero_status = "✅ Homocedasticidade" if het_test[1] >= 0.05 else "❌ Heterocedasticidade" |
|
|
|
|
|
st.markdown(f""" |
|
|
<div style='padding: 1rem; border-radius: 8px; background-color: {"#d4edda" if het_test[1] >= 0.05 else "#f8d7da"}; |
|
|
border: 1px solid {"#c3e6cb" if het_test[1] >= 0.05 else "#f5c6cb"};'> |
|
|
<h4 style='margin: 0; color: {"#155724" if het_test[1] >= 0.05 else "#721c24"};'> |
|
|
📊 Teste Breusch-Pagan |
|
|
</h4> |
|
|
<p style='margin: 0.5rem 0 0 0; color: {"#155724" if het_test[1] >= 0.05 else "#721c24"};'> |
|
|
<strong>Status:</strong> {hetero_status}<br> |
|
|
<strong>P-valor:</strong> {het_test[1]:.4f} |
|
|
</p> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with tab3: |
|
|
st.markdown("### 📊 Avaliação do Modelo") |
|
|
|
|
|
|
|
|
vif_data = pd.DataFrame() |
|
|
vif_data["Variável"] = X.columns |
|
|
vif_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])] |
|
|
vif_data["Status"] = vif_data["VIF"].apply(lambda x: "⚠️ Alto" if x > 10 else "✅ OK" if x < 5 else "🔶 Moderado") |
|
|
|
|
|
st.markdown("#### 🔗 Análise de Multicolinearidade (VIF)") |
|
|
|
|
|
|
|
|
def highlight_vif(val): |
|
|
if val > 10: |
|
|
return 'background-color: #ffebee; color: #c62828' |
|
|
elif val > 5: |
|
|
return 'background-color: #fff3e0; color: #ef6c00' |
|
|
else: |
|
|
return 'background-color: #e8f5e8; color: #2e7d32' |
|
|
|
|
|
st.dataframe( |
|
|
vif_data.style.applymap(highlight_vif, subset=['VIF']).format({'VIF': '{:.2f}'}), |
|
|
use_container_width=True |
|
|
) |
|
|
|
|
|
|
|
|
y_pred = modelo.predict(X_test_const) |
|
|
r2 = r2_score(y_test, y_pred) |
|
|
rmse = np.sqrt(mean_squared_error(y_test, y_pred)) |
|
|
mae = mean_absolute_error(y_test, y_pred) |
|
|
|
|
|
st.markdown("#### 🎯 Métricas de Performance") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
st.metric( |
|
|
label="R² (Teste)", |
|
|
value=f"{r2:.4f}", |
|
|
delta=f"{r2*100:.1f}% da variância explicada", |
|
|
delta_color="normal" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.metric( |
|
|
label="RMSE", |
|
|
value=f"{rmse:.3f}", |
|
|
delta="Erro médio quadrático", |
|
|
delta_color="off" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
st.metric( |
|
|
label="MAE", |
|
|
value=f"{mae:.3f}", |
|
|
delta="Erro médio absoluto", |
|
|
delta_color="off" |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown("#### 📈 Valores Reais vs Preditos") |
|
|
|
|
|
fig, ax = plt.subplots(figsize=(10, 6)) |
|
|
ax.scatter(y_test, y_pred, alpha=0.6, c='steelblue', edgecolor='white', s=50) |
|
|
|
|
|
|
|
|
min_val = min(min(y_test), min(y_pred)) |
|
|
max_val = max(max(y_test), max(y_pred)) |
|
|
ax.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2, alpha=0.8, label='Predição Perfeita') |
|
|
|
|
|
ax.set_xlabel("Valores Reais (Log)", fontsize=12) |
|
|
ax.set_ylabel("Valores Preditos (Log)", fontsize=12) |
|
|
ax.set_title("Valores Reais vs Preditos", fontsize=14, fontweight='bold') |
|
|
ax.legend() |
|
|
ax.grid(True, alpha=0.3) |
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
with tab4: |
|
|
st.markdown("### 💡 Interpretação Crítica do Modelo") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if r2 >= 0.7: |
|
|
st.success(f""" |
|
|
**🎉 Modelo Excelente!** |
|
|
|
|
|
O modelo explica aproximadamente **{r2*100:.1f}%** da variância logarítmica do preço. |
|
|
Isso indica um excelente ajuste aos dados. |
|
|
""") |
|
|
elif 0.4 <= r2 < 0.7: |
|
|
st.warning(f""" |
|
|
**⚠️ Modelo Moderado** |
|
|
|
|
|
O modelo explica cerca de **{r2*100:.1f}%** da variância. Pode haver variáveis importantes |
|
|
faltando ou relações não lineares não capturadas. |
|
|
""") |
|
|
else: |
|
|
st.error(f""" |
|
|
**❌ Modelo Fraco** |
|
|
|
|
|
R² de apenas **{r2*100:.1f}%**. O modelo tem baixo poder explicativo. |
|
|
Considere revisar variáveis ou abordagem. |
|
|
""") |
|
|
|
|
|
|
|
|
st.markdown("#### 🔍 Análise dos Pressupostos") |
|
|
|
|
|
|
|
|
if shapiro_test.pvalue < 0.05: |
|
|
st.warning(""" |
|
|
**📊 Normalidade dos Resíduos**: Os resíduos **não seguem distribuição normal** (p < 0.05). |
|
|
Isso pode afetar a validade de intervalos de confiança e testes t. |
|
|
""") |
|
|
else: |
|
|
st.info(""" |
|
|
**📊 Normalidade dos Resíduos**: Os resíduos seguem uma distribuição aproximadamente normal (p ≥ 0.05). ✅ |
|
|
""") |
|
|
|
|
|
|
|
|
if het_test[1] < 0.05: |
|
|
st.warning(""" |
|
|
**📈 Homocedasticidade**: Evidência de **heterocedasticidade** (Breusch-Pagan, p < 0.05). |
|
|
O erro padrão das estimativas pode estar distorcido. |
|
|
""") |
|
|
else: |
|
|
st.info(""" |
|
|
**📈 Homocedasticidade**: Não há evidência de heterocedasticidade (p ≥ 0.05). ✅ |
|
|
""") |
|
|
|
|
|
|
|
|
high_vif = vif_data[vif_data["VIF"] > 10] |
|
|
if not high_vif.empty: |
|
|
st.warning(f""" |
|
|
**🔗 Multicolinearidade**: As seguintes variáveis apresentam **alta multicolinearidade** (VIF > 10): |
|
|
""") |
|
|
st.dataframe(high_vif[['Variável', 'VIF']], use_container_width=True) |
|
|
else: |
|
|
st.info("**🔗 Multicolinearidade**: Nenhuma variável apresenta multicolinearidade severa (VIF > 10). ✅") |
|
|
|
|
|
|
|
|
st.markdown("#### 🎯 Conclusão Final") |
|
|
|
|
|
if r2 > 0.7 and shapiro_test.pvalue > 0.05 and het_test[1] > 0.05 and high_vif.empty: |
|
|
st.success(""" |
|
|
**🌟 MODELO RECOMENDADO** |
|
|
|
|
|
O modelo é bem ajustado, estatisticamente confiável e pode ser usado para prever preços com confiança. |
|
|
Todos os pressupostos estatísticos foram atendidos. |
|
|
""") |
|
|
elif r2 < 0.4: |
|
|
st.error(""" |
|
|
**❌ MODELO NÃO RECOMENDADO** |
|
|
|
|
|
O modelo explica pouco da variação nos preços. Sugere-se revisar as variáveis |
|
|
e considerar modelos não-lineares ou técnicas de machine learning mais avançadas. |
|
|
""") |
|
|
else: |
|
|
st.warning(""" |
|
|
**⚠️ USAR COM CAUTELA** |
|
|
|
|
|
O modelo é razoável, mas há indícios de violações em alguns pressupostos. |
|
|
Use com cautela e considere melhorias no modelo. |
|
|
""") |
|
|
|
|
|
|
|
|
with st.expander("💡 Recomendações para Melhorar o Modelo"): |
|
|
recomendacoes = [] |
|
|
|
|
|
if r2 < 0.6: |
|
|
recomendacoes.append("• **Adicionar mais variáveis explicativas** relevantes") |
|
|
recomendacoes.append("• **Considerar transformações não-lineares** das variáveis") |
|
|
recomendacoes.append("• **Explorar interações** entre variáveis") |
|
|
|
|
|
if shapiro_test.pvalue < 0.05: |
|
|
recomendacoes.append("• **Aplicar transformações** nos dados para normalizar resíduos") |
|
|
recomendacoes.append("• **Considerar modelos robustos** que não assumem normalidade") |
|
|
|
|
|
if not high_vif.empty: |
|
|
recomendacoes.append("• **Remover variáveis altamente correlacionadas**") |
|
|
recomendacoes.append("• **Usar técnicas de regularização** (Ridge, Lasso)") |
|
|
|
|
|
if recomendacoes: |
|
|
for rec in recomendacoes: |
|
|
st.markdown(rec) |
|
|
else: |
|
|
st.success("🎉 Seu modelo está bem ajustado! Parabéns!") |