Atividade4 / src /streamlit_app.py
ricardoadriano's picture
Update src/streamlit_app.py
19f6a77 verified
raw
history blame
19.2 kB
#!/usr/bin/env python
# coding: utf-8
# =====================================================
# Dashboard - Testes de Hipóteses com AmesHousing (Tarefa 4)
# =====================================================
import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import numpy as np
from scipy import stats
from scipy.stats import shapiro, levene, kruskal
from statsmodels.formula.api import ols
import statsmodels.api as sm
from statsmodels.stats.diagnostic import het_breuschpagan
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
# -----------------------------------------------------
# Configuração da Página
# -----------------------------------------------------
st.set_page_config(
page_title="Dashboard - Testes de Hipóteses com AmesHousing",
layout="wide",
initial_sidebar_state="expanded"
)
st.markdown("<h1 style='text-align:center;color:#003366;'>Simulador de Testes de Hipótese</h1>", unsafe_allow_html=True)
st.markdown("<h3 style='text-align:center;color:#003366;'>Análise do Dataset AmesHousing</h3>", unsafe_allow_html=True)
st.markdown("---")
# -----------------------------------------------------
# Abas do Dashboard
# -----------------------------------------------------
tabs = st.tabs(["Simulações Teóricas", "Análise AmesHousing"]) # mantém as abas existentes
# -----------------------------------------------------
# Aba 1: Simulações Teóricas (inalterada)
# -----------------------------------------------------
with tabs[0]:
st.subheader("Teste de Hipótese para Proporção de Testes Positivos de COVID-19")
st.sidebar.markdown("### Parâmetros do Teste (Proporção)")
p_pop = st.sidebar.slider("Proporção populacional (H0)", 0.0, 1.0, 0.1, 0.01, key="p_pop")
p_sample = st.sidebar.slider("Proporção amostral", 0.0, 1.0, 0.12, 0.01, key="p_sample")
n = st.sidebar.slider("Tamanho da amostra", 100, 10000, 1000, 10, key="n_sample")
alpha_prop = st.sidebar.slider("Nível de significância (α)", 0.01, 0.10, 0.05, 0.01, key="alpha_prop")
se = np.sqrt(p_pop*(1-p_pop)/n)
z = (p_sample - p_pop)/se
p_value = 2*(1 - stats.norm.cdf(abs(z)))
st.write(f"**Z** = {z:.4f}")
st.write(f"**p-valor** = {p_value:.4f}")
if p_value < alpha_prop:
st.write("**Rejeitamos H0**: diferença significativa.")
else:
st.write("**Não rejeitamos H0**: sem diferença significativa.")
# -----------------------------------------------------
# Aba 2: Análise AmesHousing + (NOVO) Regressão para Tarefa 4
# -----------------------------------------------------
with tabs[1]:
st.subheader("Análise de Variância - AmesHousing Dataset")
st.markdown("---")
# Leitura do CSV — ajustado para usar o arquivo enviado com o app
@st.cache_data
def carregar_dados():
paths_tentativa = [
"AmesHousing.csv", # mesma pasta do app
"/mnt/data/AmesHousing.csv", # caminho do ambiente atual
"../Dados/AmesHousing.csv", # caminho original do código
]
last_err = None
for p in paths_tentativa:
try:
df = pd.read_csv(p)
return df
except Exception as e:
last_err = e
continue
raise RuntimeError(f"Não foi possível carregar o AmesHousing.csv. Último erro: {last_err}")
casa_data = carregar_dados()
casa_data.columns = casa_data.columns.str.strip().str.replace(" ", "_")
# -----------------------------
# Amostragem (mantém comportamento anterior)
# -----------------------------
n_amostra = st.session_state.get("n_sample", len(casa_data))
if n_amostra < len(casa_data):
dados = casa_data.sample(n=n_amostra, random_state=42)
else:
dados = casa_data.copy()
# -----------------------------
# Filtro interativo no sidebar
# -----------------------------
st.sidebar.markdown("### Filtros AmesHousing")
bairros = st.sidebar.multiselect(
"Selecione bairros",
options=sorted(dados["Neighborhood"].dropna().unique()),
default=None
)
# Aplicar filtro
dados_filtrados = dados.copy()
if bairros:
dados_filtrados = dados_filtrados[dados_filtrados["Neighborhood"].isin(bairros)]
# -------------------------------------------------
# Análise Exploratória (existente)
# -------------------------------------------------
st.markdown("### Distribuição do Preço de Venda")
if not dados_filtrados.empty:
fig, ax = plt.subplots(figsize=(8,5))
sns.histplot(dados_filtrados['SalePrice'], kde=True, ax=ax)
ax.set_title("Distribuição do Preço de Venda")
st.pyplot(fig)
else:
st.warning("Nenhum dado disponível com os filtros aplicados.")
# Boxplots (existente)
st.markdown("### Boxplots das Variáveis Selecionadas")
variavel = st.selectbox(
"Escolha a variável categórica para comparar preços:",
["Neighborhood","Garage_Type","Fireplaces"]
)
if not dados_filtrados.empty:
if len(dados_filtrados[variavel].dropna().unique()) > 1:
fig2, ax2 = plt.subplots(figsize=(12,6))
sns.boxplot(x=variavel, y="SalePrice", data=dados_filtrados, ax=ax2)
plt.xticks(rotation=90)
ax2.set_title(f"Preço de Venda por {variavel}")
st.pyplot(fig2)
else:
st.warning(f"Não é possível gerar boxplot: apenas uma categoria em {variavel} após os filtros.")
# Scatter interativo (média de preço por bairro) — existente
st.markdown("### Preço Médio de Venda por Bairro")
if not dados_filtrados.empty:
bairro_grouped = dados_filtrados.groupby('Neighborhood').agg(
count=('SalePrice','size'),
mean_price=('SalePrice','mean')
).reset_index()
bairro_filtered = bairro_grouped[bairro_grouped['count'] >= 5]
if not bairro_filtered.empty:
fig3 = px.scatter(
bairro_filtered,
x='mean_price',
y='Neighborhood',
size='count',
color='Neighborhood',
title='Preço Médio de Venda vs Bairro (Ames, Iowa)',
labels={'mean_price': 'Preço Médio de Venda', 'Neighborhood':'Bairro'},
opacity=0.8
)
st.plotly_chart(fig3, use_container_width=True)
else:
st.warning("Não há bairros suficientes após filtros para gerar o gráfico.")
# -------------------------------------------------
# ANOVA — existente
# -------------------------------------------------
st.markdown("### ANOVA para Neighborhood, Garage_Type e Fireplaces")
alpha = st.sidebar.slider(
"Nível de significância (α) - ANOVA AmesHousing",
0.01,0.10,0.05,0.01,
key="alpha_ames"
)
if not dados_filtrados.empty:
for nome in ["Neighborhood", "Garage_Type", "Fireplaces"]:
categorias = dados_filtrados[nome].dropna().unique()
if len(categorias) < 2:
st.warning(f"ANOVA não pôde ser realizada para **{nome}** (menos de 2 grupos após os filtros).")
continue
modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
st.markdown(f"#### ANOVA - {nome}")
anova = sm.stats.anova_lm(modelo, typ=2)
st.dataframe(anova)
# -------------------------------------------------
# Validação dos Pressupostos — existente
# -------------------------------------------------
st.markdown("### Validação dos Pressupostos da ANOVA")
st.markdown("#### Teste de Normalidade (Shapiro-Wilk)")
for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
categorias = dados_filtrados[nome].dropna().unique()
if len(categorias) < 2:
st.warning(f"Shapiro-Wilk não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
continue
modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
residuos = modelo.resid
stat, p = shapiro(residuos.dropna())
st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
+ ("resíduos normais" if p >= alpha else "violação de normalidade"))
st.markdown("#### Teste de Homocedasticidade (Levene)")
for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
if len(grupos) < 2:
st.warning(f"Levene não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
continue
stat, p = levene(*grupos)
st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
+ ("variâncias iguais" if p >= alpha else "variâncias diferentes"))
# -------------------------------------------------
# Kruskal-Wallis — existente
# -------------------------------------------------
st.markdown("### Teste não-paramétrico (Kruskal-Wallis)")
for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
if len(grupos) < 2:
st.warning(f"Kruskal-Wallis não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
continue
stat, p = kruskal(*grupos)
st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
+ ("diferenças significativas" if p < alpha else "sem diferença significativa"))
# =================================================
# (NOVO) Regressão Linear — atende à Tarefa 4 (itens a–d)
# =================================================
st.markdown("---")
st.subheader("Regressão Linear — Tarefa 4 (PPCA/UnB)")
st.markdown("Selecione variáveis para modelagem do **SalePrice** (alvo).")
# Sugestões de variáveis comumente utilizadas
candidatos_numericas = [
c for c in [
'Bedroom_AbvGr','Full_Bath','Half_Bath','TotRms_AbvGrd','Gr_Liv_Area',
'Garage_Cars','Garage_Area','Overall_Qual','Overall_Cond','Year_Built','Lot_Area',
'Fireplaces'
] if c in dados_filtrados.columns
]
candidatos_categoricas = [c for c in ['Neighborhood','House_Style','Bldg_Type','Garage_Type','Kitchen_Qual'] if c in dados_filtrados.columns]
with st.expander("Seleção de variáveis"):
cols = st.columns(2)
with cols[0]:
feats_num = st.multiselect("Numéricas", options=candidatos_numericas, default=['Bedroom_AbvGr','Garage_Cars','Gr_Liv_Area'] if 'Garage_Cars' in candidatos_numericas else candidatos_numericas[:2])
with cols[1]:
feats_cat = st.multiselect("Categóricas", options=candidatos_categoricas, default=['Neighborhood'] if 'Neighborhood' in candidatos_categoricas else [])
interagir = st.checkbox("Adicionar interação entre duas variáveis", value=False)
inter_1 = inter_2 = None
if interagir:
inter_1 = st.selectbox("Variável 1 (para interação)", options=feats_num + feats_cat, index=0 if feats_num else 0)
inter_2 = st.selectbox("Variável 2 (para interação)", options=[v for v in feats_num + feats_cat if v != inter_1], index=0)
usar_logy = st.checkbox("Aplicar transformação log(SalePrice) caso pressuspostos sejam violados", value=False)
teste_size = st.slider("Proporção de teste (holdout)", 0.1, 0.5, 0.2, 0.05)
alpha_reg = st.slider("Nível de significância (α) — Regressão", 0.01, 0.10, 0.05, 0.01)
# Construção da fórmula patsy
def construir_formula(y, feats_num, feats_cat, inter_1=None, inter_2=None):
termos = []
termos += feats_num
termos += [f"C({c})" for c in feats_cat]
if inter_1 and inter_2:
a = f"C({inter_1})" if inter_1 in feats_cat else inter_1
b = f"C({inter_2})" if inter_2 in feats_cat else inter_2
termos.append(f"{a}:{b}") # interação
rhs = " + ".join(termos) if termos else "1"
return f"{y} ~ {rhs}"
if st.button("Ajustar modelo"):
# Pré-seleção de colunas necessárias
cols_necessarias = ['SalePrice'] + feats_num + feats_cat
if interagir and inter_1 and inter_2:
cols_necessarias += [inter_1, inter_2]
df_modelo = dados_filtrados[cols_necessarias].dropna().copy()
if df_modelo.empty:
st.error("Sem dados suficientes após remoção de NAs nas variáveis selecionadas.")
else:
y_col = 'SalePrice'
if usar_logy:
df_modelo['SalePrice'] = np.log(df_modelo['SalePrice'].astype(float))
y_col = 'SalePrice'
formula = construir_formula(y_col, feats_num, feats_cat, inter_1 if interagir else None, inter_2 if interagir else None)
# Split treino/teste
df_treino, df_teste = train_test_split(df_modelo, test_size=teste_size, random_state=42)
# Ajuste usando a fórmula (statsmodels cuida de dummies de C())
model = ols(formula, data=df_treino).fit()
st.markdown("#### Especificação do Modelo")
st.code(formula)
st.markdown("#### Coeficientes e Inferência")
st.dataframe(model.summary2().tables[1])
# Interpretação simples (sinal e significância)
sig = model.pvalues < alpha_reg
interpretacoes = []
for nome, beta in model.params.items():
if nome == 'Intercept':
continue
tag = "significativo" if sig.get(nome, False) else "não significativo"
direcao = "aumenta" if beta > 0 else "reduz"
interpretacoes.append(f"• {nome}: {tag}; coef. {beta:.3f}{direcao} o preço esperado, ceteris paribus.")
if interpretacoes:
st.markdown("**Leitura rápida dos coeficientes:**\n" + "\n".join(interpretacoes))
# Predição e métricas
y_true = df_teste['SalePrice']
y_pred = model.predict(df_teste)
if usar_logy:
# volta à escala original
y_true_exp = np.exp(y_true)
y_pred_exp = np.exp(y_pred)
R2 = r2_score(y_true_exp, y_pred_exp)
RMSE = mean_squared_error(y_true_exp, y_pred_exp, squared=False)
MAE = mean_absolute_error(y_true_exp, y_pred_exp)
else:
R2 = r2_score(y_true, y_pred)
RMSE = mean_squared_error(y_true, y_pred, squared=False)
MAE = mean_absolute_error(y_true, y_pred)
met_df = pd.DataFrame({
'Métrica': ['R²', 'RMSE', 'MAE'],
'Valor': [R2, RMSE, MAE]
})
st.markdown("#### Avaliação do Modelo (holdout)")
st.dataframe(met_df)
# Resíduos para pressupostos
residuos = model.resid
fitted = model.fittedvalues
cols = st.columns(3)
with cols[0]:
st.markdown("**Resíduos vs Ajustados** (homocedasticidade)")
fig_r, ax_r = plt.subplots(figsize=(4,3))
ax_r.scatter(fitted, residuos, alpha=0.5)
ax_r.axhline(0, color='red', linestyle='--')
ax_r.set_xlabel('Ajustados')
ax_r.set_ylabel('Resíduos')
st.pyplot(fig_r)
with cols[1]:
st.markdown("**QQ-Plot** (normalidade)")
fig_q = sm.qqplot(residuos, line='45', fit=True)
st.pyplot(fig_q)
with cols[2]:
st.markdown("**Histograma dos Resíduos**")
fig_h, ax_h = plt.subplots(figsize=(4,3))
sns.histplot(residuos, kde=True, ax=ax_h)
st.pyplot(fig_h)
# Testes formais — Normalidade (Shapiro) e Homocedasticidade (Breusch-Pagan)
sh_stat, sh_p = shapiro(residuos)
bp_stat, bp_p, _, _ = het_breuschpagan(residuos, model.model.exog)
st.markdown("#### Testes de Pressupostos (Regressão)")
st.write(f"Shapiro-Wilk: estatística={sh_stat:.3f}, p={sh_p:.3f} → " + ("normalidade OK" if sh_p>=alpha_reg else "violação de normalidade"))
st.write(f"Breusch-Pagan: estatística={bp_stat:.3f}, p={bp_p:.3f} → " + ("homocedasticidade OK" if bp_p>=alpha_reg else "heterocedasticidade detectada"))
# Multicolinearidade — VIF somente para variáveis numéricas (dummies já expandidas pelo modelo)
try:
design = model.model.exog
nomes = model.model.exog_names
vif_vals = []
for i in range(design.shape[1]):
try:
vif_vals.append(variance_inflation_factor(design, i))
except Exception:
vif_vals.append(np.nan)
vif_df = pd.DataFrame({'Variável': nomes, 'VIF': vif_vals})
st.markdown("#### VIF (Multicolinearidade)")
st.dataframe(vif_df)
except Exception as e:
st.info(f"Não foi possível calcular VIF: {e}")
# Orientações conforme pressupostos
avisos = []
if sh_p < alpha_reg:
avisos.append("Normalidade violada — considere log-transformar o alvo (marque a opção log) ou técnicas robustas.")
if bp_p < alpha_reg:
avisos.append("Heterocedasticidade — avalie transformação ou erros-padrão robustos (HC).")
if (pd.Series(vif_vals).dropna() > 10).any():
avisos.append("VIF > 10 em alguma variável — possível multicolinearidade; reavalie/features ou regularização.")
if avisos:
st.warning("\n".join(avisos))
# Interação — destaque se foi significativa
if interagir and inter_1 and inter_2:
termo = f"C({inter_1}):C({inter_2})" if (inter_1 in feats_cat and inter_2 in feats_cat) else (
f"C({inter_1}):{inter_2}" if inter_1 in feats_cat else (
f"{inter_1}:C({inter_2})" if inter_2 in feats_cat else f"{inter_1}:{inter_2}"))
if termo in model.pvalues:
p_int = model.pvalues[termo]
st.info(f"Interação {termo} — p={p_int:.4f} → " + ("**significativa**" if p_int < alpha_reg else "não significativa"))
st.success("Modelagem concluída.")