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
# === CONFIGURAÇÃO DA PÁGINA ===
st.set_page_config(
page_title="📊 Análise Estatística Avançada",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# === CSS CUSTOMIZADO ===
st.markdown("""
""", unsafe_allow_html=True)
# === HEADER PRINCIPAL ===
st.markdown("""
📊 Análise Estatística Avançada
ANOVA & Regressão Linear com Diagnósticos Completos
""", unsafe_allow_html=True)
# === CARREGAMENTO DO DATASET ===
@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:
# === INFORMAÇÕES DO DATASET ===
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown("""
📏 Linhas
{}
""".format(len(df)), unsafe_allow_html=True)
with col2:
st.markdown("""
📊 Colunas
{}
""".format(len(df.columns)), unsafe_allow_html=True)
with col3:
st.markdown("""
💰 Preço Médio
${:,.0f}
""".format(df['SalePrice'].mean()), unsafe_allow_html=True)
with col4:
st.markdown("""
🏠 Preço Mediano
${:,.0f}
""".format(df['SalePrice'].median()), unsafe_allow_html=True)
# === PRÉ-VISUALIZAÇÃO DOS DADOS ===
with st.expander("🔍 Pré-visualização dos Dados", expanded=False):
st.dataframe(df.head(10), use_container_width=True)
# Gráfico de distribuição do preço
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)
# === SELEÇÃO DE VARIÁVEIS ===
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("""
⚙️ Configurações
""", 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 "
)
# === CONFIGURAÇÕES AVANÇADAS ===
st.sidebar.markdown("---")
st.sidebar.markdown("### ⚙️ Configurações Avançadas")
# Opções de visualização
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)
# Informações do sistema
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):,}")
# === MATRIZ DE CORRELAÇÃO ===
if show_correlations and regressao_vars:
st.markdown("""
🔗 Matriz de Correlação
""", unsafe_allow_html=True)
# Selecionar apenas variáveis numéricas
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()
# Criar heatmap interativo
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.")
# === ANOVA ===
if anova_vars:
st.markdown("""
🔬 Análise ANOVA
""", unsafe_allow_html=True)
for var in anova_vars:
with st.container():
st.markdown(f"""
📋 ANOVA - {var}
""", 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)
# Resultados em formato mais bonito
col1, col2 = st.columns(2)
with col1:
st.markdown("#### 📊 Resultados ANOVA")
st.dataframe(anova_result.round(4), use_container_width=True)
with col2:
# Testes de normalidade dos resíduos
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"""
✅ Shapiro-Wilk
p-valor: {shapiro_result.pvalue:.4f}
Normalidade confirmada
""", unsafe_allow_html=True)
else:
st.markdown(f"""
⚠️ Shapiro-Wilk
p-valor: {shapiro_result.pvalue:.4f}
Normalidade rejeitada
""", unsafe_allow_html=True)
# Teste de homocedasticidade
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"""
✅ Teste de Levene
p-valor: {levene_result.pvalue:.4f}
Homocedasticidade confirmada
""", unsafe_allow_html=True)
else:
st.markdown(f"""
⚠️ Teste de Levene
p-valor: {levene_result.pvalue:.4f}
Heterocedasticidade detectada
""", unsafe_allow_html=True)
with col2:
if kruskal_result.pvalue < 0.05:
st.markdown(f"""
📊 Kruskal-Wallis
p-valor: {kruskal_result.pvalue:.4f}
Diferenças significativas
""", unsafe_allow_html=True)
else:
st.markdown(f"""
📊 Kruskal-Wallis
p-valor: {kruskal_result.pvalue:.4f}
Sem diferenças significativas
""", unsafe_allow_html=True)
# Calcula as medianas e ordena
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:
# Categoria mais vantajosa
mediana_max = medianas.max()
categorias_top = medianas[medianas == mediana_max].index.tolist()
if len(categorias_top) == 1:
st.markdown(f"""
🏆 Categoria Mais Vantajosa
{categorias_top[0]}
Mediana: ${mediana_max:,.0f}
""", unsafe_allow_html=True)
else:
categorias_str = ", ".join(categorias_top)
st.markdown(f"""
🏆 Categorias Mais Vantajosas (Empate)
{categorias_str}
Mediana: ${mediana_max:,.0f}
""", unsafe_allow_html=True)
# Gráfico interativo com Plotly
if show_advanced_plots:
# Criar subplot com múltiplas visualizações
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}]]
)
# Violin plot
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
)
# Box plot
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
)
# Médias
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
)
# Contagens
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:
# Gráfico simples
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("---")
# === REGRESSÃO LINEAR ===
if regressao_vars:
# Header principal com ícone
st.markdown("""
📊 Regressão Linear Múltipla
Análise Estatística Avançada
""", 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 simulado para engajamento
progress_bar = st.progress(0)
status_text = st.empty()
status_text.text('🔄 Preparando dados...')
progress_bar.progress(20)
# Transformar variáveis categóricas em dummies
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']
# Garantir tipos numéricos
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)
# Remove progress bar após completar
import time
time.sleep(1)
progress_bar.empty()
status_text.empty()
# Tabs para organizar melhor o conteúdo
tab1, tab2, tab3, tab4 = st.tabs(["📈 Resumo do Modelo", "🔍 Diagnósticos", "📊 Avaliação", "💡 Interpretação"])
with tab1:
st.markdown("### 📋 Resumo Estatístico")
# Caixa expansível para o resumo completo
with st.expander("🔍 Ver Resumo Completo do Modelo", expanded=False):
st.text(modelo.summary())
# Métricas principais em destaque
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
# Layout em colunas para os gráficos
col1, col2 = st.columns(2)
with col1:
# Gráfico de resíduos vs valores ajustados (melhorado)
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:
# Q-Q plot para normalidade
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)
# Testes estatísticos em cards coloridos
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"""
🎯 Teste Shapiro-Wilk
Status: {normalidade_status}
P-valor: {shapiro_test.pvalue:.4f}
""", unsafe_allow_html=True)
with col2:
hetero_status = "✅ Homocedasticidade" if het_test[1] >= 0.05 else "❌ Heterocedasticidade"
st.markdown(f"""
📊 Teste Breusch-Pagan
Status: {hetero_status}
P-valor: {het_test[1]:.4f}
""", unsafe_allow_html=True)
with tab3:
st.markdown("### 📊 Avaliação do Modelo")
# VIF em uma tabela mais bonita
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)")
# Estilizar a tabela 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
)
# Métricas de performance
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"
)
# Gráfico de Valores Reais vs Preditos
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)
# Linha de referência perfeita
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")
# Análise automática do modelo com cards coloridos
# Qualidade geral 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.
""")
# Análise dos pressupostos
st.markdown("#### 🔍 Análise dos Pressupostos")
# Normalidade dos resíduos
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). ✅
""")
# Heterocedasticidade
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). ✅
""")
# Multicolinearidade (VIF)
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). ✅")
# Conclusão final em um card especial
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.
""")
# Recomendações interativas
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!")