Atividade2 / src /streamlit_app.py
vsalgs's picture
Update src/streamlit_app.py
4dce935 verified
# streamlit_dashboard_unificado.py
import streamlit as st
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.formula.api import ols
from scipy.stats import shapiro, levene, kruskal, anderson
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.metrics import mean_squared_error, mean_absolute_error
import matplotlib.pyplot as plt
import seaborn as sns
# Configuração geral do Streamlit
st.set_page_config(layout="wide", page_title="Dashboard Imobiliário Integrado")
# --- Funções de Carregamento de Dados ---
@st.cache_data
def load_data_anova():
"""Carrega o Ames Housing Dataset e retorna para o módulo ANOVA."""
urls_tentativas = [
"https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv"
]
df = None
url_carregada = ""
for url in urls_tentativas:
try:
df = pd.read_csv(url)
url_carregada = url
break
except Exception:
continue
if df is None:
return None, None, [], []
df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower()
coluna_preco_nome = None
if 'saleprice' in df.columns:
coluna_preco_nome = 'saleprice'
elif 'sale_price' in df.columns:
df.rename(columns={'sale_price': 'saleprice'}, inplace=True)
coluna_preco_nome = 'saleprice'
if coluna_preco_nome:
df[coluna_preco_nome] = pd.to_numeric(df[coluna_preco_nome], errors='coerce')
df.dropna(subset=[coluna_preco_nome], inplace=True)
# Identificar colunas categóricas potenciais (inclui numéricas discretas < 20 níveis)
colunas_categoricas_potenciais = df.select_dtypes(include=['object']).columns.tolist()
colunas_numericas_discretas = [
col for col in df.select_dtypes(include=np.number).columns
if df[col].nunique() < 20 and col != coluna_preco_nome
]
colunas_categoricas_potenciais.extend(colunas_numericas_discretas)
colunas_categoricas_potenciais = sorted(
list(set(col for col in colunas_categoricas_potenciais if col != coluna_preco_nome))
)
return df, coluna_preco_nome, colunas_categoricas_potenciais, df.columns.tolist()
@st.cache_data
def load_data_reg():
"""Carrega o Ames Housing Dataset e retorna para o módulo de Regressão."""
fixed_url = "https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv"
try:
df = pd.read_csv(fixed_url)
url_carregada = fixed_url
except Exception as e:
return None, None, [], [], []
st.success(f"Dataset carregado com sucesso de: {url_carregada} (Shape: {df.shape})")
df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower()
coluna_preco_nome = None
possible_price_cols = ['saleprice', 'sale_price', 'price']
for col_candidate in possible_price_cols:
if col_candidate in df.columns:
if coluna_preco_nome is None:
coluna_preco_nome = 'saleprice'
if col_candidate != 'saleprice':
df.rename(columns={col_candidate: 'saleprice'}, inplace=True)
break
if coluna_preco_nome is None:
for col_candidate in df.columns:
if 'price' in col_candidate and 'sale' in col_candidate:
coluna_preco_nome = 'saleprice'
if col_candidate != 'saleprice':
df.rename(columns={col_candidate: 'saleprice'}, inplace=True)
st.warning(f"Coluna de preço identificada como '{col_candidate}' e renomeada para 'saleprice'.")
break
if coluna_preco_nome is None:
return df, None, [], [], df.columns.tolist()
df[coluna_preco_nome] = pd.to_numeric(df[coluna_preco_nome], errors='coerce')
df.dropna(subset=[coluna_preco_nome], inplace=True)
# Colunas contínuas e categóricas potenciais
vars_sempre_continuas_para_reg = [
'grlivarea', 'overallqual', 'yearbuilt', 'totalbsmtsf', 'lotarea',
'masvnrarea', 'bsmtfinsf1', 'bsmtunfsf', '1stflrsf', '2ndflrsf',
'garagearea', 'wooddecksf', 'openporchsf', 'yrsold', 'lotfrontage',
'garageyrblt', 'screensf', 'poolarea', 'miscval', 'mosold',
'lowqualfinsf', 'bsmthalfbath', 'fullbath', 'halfbath',
'bedroomabvgr', 'kitchenabvgr', 'totrmsabvgrd', 'fireplaces', 'garagecars'
]
colunas_categoricas_potenciais = df.select_dtypes(include=['object', 'category']).columns.tolist()
colunas_numericas_discretas = [
col for col in df.select_dtypes(include=np.number).columns
if df[col].nunique() < 20 and col != coluna_preco_nome and col not in vars_sempre_continuas_para_reg
]
colunas_categoricas_potenciais.extend(colunas_numericas_discretas)
colunas_categoricas_potenciais = sorted(
list(set(col for col in colunas_categoricas_potenciais if col in df.columns and col != coluna_preco_nome))
)
colunas_continuas_potenciais = [
col for col in df.select_dtypes(include=np.number).columns
if (
col not in colunas_categoricas_potenciais or col in vars_sempre_continuas_para_reg
) and col != coluna_preco_nome
]
colunas_continuas_potenciais = sorted(list(set(col for col in colunas_continuas_potenciais if col in df.columns)))
return df, coluna_preco_nome, colunas_categoricas_potenciais, colunas_continuas_potenciais, df.columns.tolist()
# --- Funções de ANOVA ---
def perform_anova_for_variable(df_analysis, var_cat, col_preco):
"""Executa ANOVA e testes de pressupostos para uma variável categórica."""
results = {"var_cat": var_cat, "plots": {}}
df_var = df_analysis[[var_cat, col_preco]].copy()
if df_var[var_cat].dtype != 'object' and not pd.api.types.is_categorical_dtype(df_var[var_cat]):
df_var[var_cat] = df_var[var_cat].astype('category')
df_var.dropna(inplace=True)
if df_var[var_cat].nunique() < 2 or len(df_var) < 10:
results["error"] = "Dados insuficientes ou poucos níveis após limpeza."
return results
formula = f'{col_preco} ~ C({var_cat})'
try:
modelo = ols(formula, data=df_var).fit()
results["anova_table"] = sm.stats.anova_lm(modelo, typ=2)
if f'C({var_cat})' in results["anova_table"].index:
results["p_valor_anova"] = results["anova_table"].loc[f'C({var_cat})', 'PR(>F)']
else:
results["p_valor_anova"] = results["anova_table"]['PR(>F)'].iloc[0]
residuos = modelo.resid
results["residuos_count"] = len(residuos)
normalidade_ok = False
if len(residuos) >= 3:
if len(residuos) <= 5000:
stat_shapiro, p_shapiro = shapiro(residuos)
results["shapiro_test"] = (stat_shapiro, p_shapiro)
if p_shapiro >= 0.05:
normalidade_ok = True
else:
ad_result = anderson(residuos)
results["anderson_test"] = ad_result
sig_level_idx = ad_result.significance_level.tolist().index(5.0)
if ad_result.statistic < ad_result.critical_values[sig_level_idx]:
normalidade_ok = True
results["normalidade_ok"] = normalidade_ok
# Plots de normalidade
fig_norm, ax_norm = plt.subplots(1, 2, figsize=(10, 4))
if len(residuos) > 1:
sns.histplot(residuos, kde=True, ax=ax_norm[0], stat="density", bins=30)
ax_norm[0].set_title(f'Histograma Resíduos ({var_cat})', fontsize=10)
sm.qqplot(residuos, line='s', ax=ax_norm[1], markerfacecolor="skyblue", markeredgecolor="dodgerblue", alpha=0.7)
ax_norm[1].set_title(f'Q-Q Plot Resíduos ({var_cat})', fontsize=10)
else:
ax_norm[0].text(0.5, 0.5, "Poucos dados", ha='center', va='center')
ax_norm[1].text(0.5, 0.5, "Poucos dados", ha='center', va='center')
plt.tight_layout()
results["plots"]["normalidade"] = fig_norm
# Teste de homocedasticidade (Levene)
homocedasticidade_ok = False
grupos = [df_var[col_preco][df_var[var_cat] == categoria].dropna() for categoria in df_var[var_cat].unique()]
grupos_validos = [g for g in grupos if len(g) >= 2]
if len(grupos_validos) >= 2:
stat_levene, p_levene = levene(*grupos_validos)
results["levene_test"] = (stat_levene, p_levene)
if p_levene >= 0.05:
homocedasticidade_ok = True
results["homocedasticidade_ok"] = homocedasticidade_ok
# Teste de Kruskal-Wallis (se necessário)
if not normalidade_ok or not homocedasticidade_ok:
if len(grupos_validos) >= 2:
stat_kruskal, p_kruskal = kruskal(*grupos_validos)
results["kruskal_test"] = (stat_kruskal, p_kruskal)
# Boxplot
fig_box, ax_box = plt.subplots(figsize=(10, 5))
unique_cats = df_var[var_cat].nunique()
order_boxplot = None
if 5 < unique_cats < 50:
try:
order_boxplot = df_var.groupby(var_cat)[col_preco].median().sort_values().index
except Exception:
order_boxplot = df_var[var_cat].unique()
sns.boxplot(x=var_cat, y=col_preco, data=df_var, order=order_boxplot, ax=ax_box, palette="viridis")
ax_box.set_title(f'Distribuição de {col_preco} por {var_cat}', fontsize=12)
if unique_cats > 10:
plt.setp(ax_box.get_xticklabels(), rotation=45, ha='right', fontsize=8)
else:
plt.setp(ax_box.get_xticklabels(), fontsize=9)
plt.tight_layout()
results["plots"]["boxplot"] = fig_box
except Exception as e:
results["error"] = str(e)
return results
# --- Função de Regressão Linear ---
def run_linear_regression_analysis(df_original, target_column_name, selected_cont_vars, selected_cat_vars):
"""Executa a análise de regressão linear."""
results_regression = {}
df_reg = df_original.copy()
all_selected_vars = selected_cont_vars + selected_cat_vars
if not all_selected_vars:
return {"error": "Nenhuma variável explicativa selecionada."}
actual_selected_vars = [var for var in all_selected_vars if var in df_reg.columns]
missing_vars = [var for var in all_selected_vars if var not in df_reg.columns]
if missing_vars:
st.warning(f"Variáveis ignoradas (não encontradas): {missing_vars}")
selected_cont_vars = [v for v in selected_cont_vars if v in actual_selected_vars]
selected_cat_vars = [v for v in selected_cat_vars if v in actual_selected_vars]
all_selected_vars = selected_cont_vars + selected_cat_vars
if not all_selected_vars:
return {"error": "Nenhuma variável válida para regressão após filtragem."}
df_reg = df_reg[all_selected_vars + [target_column_name]].copy()
df_reg.dropna(subset=all_selected_vars + [target_column_name], inplace=True)
if df_reg.empty:
return {"error": "DataFrame vazio após remoção de NaNs."}
if df_reg[target_column_name].min() > 0:
df_reg['log_saleprice'] = np.log(df_reg[target_column_name])
else:
df_reg['log_saleprice'] = np.log1p(df_reg[target_column_name])
new_target_column = 'log_saleprice'
transformed_continuous_vars = []
for var in selected_cont_vars:
log_var_name = f'log_{var}'
if var in df_reg.columns:
if var in ['overallqual', 'yearbuilt', 'yrsold', 'mosold', 'fireplaces', 'garagecars',
'bsmthalfbath', 'fullbath', 'halfbath', 'bedroomabvgr', 'kitchenabvgr', 'totrmsabvgrd']:
transformed_continuous_vars.append(var)
elif df_reg[var].min() > 0:
df_reg[log_var_name] = np.log(df_reg[var])
transformed_continuous_vars.append(log_var_name)
else:
df_reg[log_var_name] = np.log1p(df_reg[var])
transformed_continuous_vars.append(log_var_name)
processed_categorical_vars = []
for cat_var in selected_cat_vars:
if cat_var in df_reg.columns:
if df_reg[cat_var].dtype not in ['object', 'category']:
df_reg[cat_var] = df_reg[cat_var].astype('category')
processed_categorical_vars.append(cat_var)
df_reg = pd.get_dummies(df_reg, columns=processed_categorical_vars, drop_first=True, dtype=float)
final_explanatory_vars = [var for var in transformed_continuous_vars if var in df_reg.columns]
for cat_orig in processed_categorical_vars:
dummy_cols = [col for col in df_reg.columns if col.startswith(f"{cat_orig}_")]
final_explanatory_vars.extend(dummy_cols)
final_explanatory_vars = sorted(set(final_explanatory_vars))
final_explanatory_vars = [
var for var in final_explanatory_vars
if var in df_reg.columns and df_reg[var].isnull().sum() < len(df_reg) and df_reg[var].std(skipna=True) > 0
]
if not final_explanatory_vars:
return {"error": "Nenhuma variável explicativa válida após pré-processamento."}
X = df_reg[final_explanatory_vars]
y = df_reg[new_target_column]
X = sm.add_constant(X, has_constant='add')
try:
model = sm.OLS(y, X).fit()
results_regression['model_summary_obj'] = model.summary()
results_regression['model_object'] = model
except Exception as e:
return {"error": f"Erro ao ajustar modelo: {str(e)}. Variáveis em X: {X.columns.tolist()}"}
fitted_values = model.fittedvalues
r_squared = model.rsquared
adj_r_squared = model.rsquared_adj
rmse_log = np.sqrt(mean_squared_error(y, fitted_values))
mae_log = mean_absolute_error(y, fitted_values)
results_regression['performance_metrics'] = {
'R-squared': r_squared,
'Adjusted R-squared': adj_r_squared,
'RMSE (log)': rmse_log,
'MAE (log)': mae_log
}
# Interpretação de coeficientes
coeff_notes = """
**Interpretação dos Coeficientes (`log_saleprice`):**
- Variáveis contínuas transformadas em log (ex: `log_grlivarea`):
Aumento de 1% na variável resulta em ~[coef * 1]% de variação no `saleprice`.
- Variáveis contínuas não transformadas (ex: `overallqual`):
Aumento unitário resulta em ~[(exp(coef)-1)*100]% de variação no `saleprice`.
- Dummies (ex: `neighborhood_stonebr`):
Presença da categoria resulta em ~[(exp(coef)-1)*100]% de variação no `saleprice`.
**Significância:** Verifique `P>|t|` < 0.05.
"""
results_regression['coefficients_interpretation_notes'] = coeff_notes
# Recomendações práticas
recommendations_data = []
if hasattr(model, 'params') and hasattr(model, 'pvalues'):
params_df = pd.DataFrame({'Coeficiente': model.params, 'P-valor': model.pvalues})
significant_params = params_df[params_df['P-valor'] < 0.05]
if 'const' in significant_params.index:
significant_params = significant_params.drop('const')
if not significant_params.empty:
for var, row in significant_params.iterrows():
coef = row['Coeficiente']
is_log = var.startswith("log_") and var in transformed_continuous_vars
original_var = var.replace("log_", "") if is_log else var
display_name = original_var.replace('_', ' ').title()
# Dummies
is_dummy = False
for cat_orig in selected_cat_vars:
if var.startswith(f"{cat_orig}_"):
is_dummy = True
parts = var.split('_', 1)
cat_display = parts[1].replace('_', ' ').title() if len(parts) > 1 else "Categoria"
display_name = f"{cat_orig.replace('_', ' ').title()}: {cat_display}"
break
if is_log:
tipo = "Aumento Percentual (elasticidade)"
magnitude = f"{coef:.2f}% de variação no preço para 1% de aumento"
interpret = f"1% em '{original_var.title()}' causa {coef:.2f}% no preço."
else:
percentage_change = (np.exp(coef) - 1) * 100
tipo = "Aumento Percentual (nível ou dummy)"
magnitude = f"{percentage_change:.2f}% de variação no preço"
if is_dummy:
interpret = f"Presença em '{display_name}' causa {percentage_change:.2f}% no preço."
else:
interpret = f"Aumento unitário em '{display_name}' causa {percentage_change:.2f}% no preço."
recommendations_data.append({
"Variável": display_name,
"Tipo de Impacto": tipo,
"Interpretação": interpret,
"Magnitude Estimada": magnitude
})
else:
recommendations_data.append({
"Variável": "N/A",
"Tipo de Impacto": "-",
"Interpretação": "Nenhum coeficiente significativo.",
"Magnitude Estimada": "-"
})
else:
recommendations_data.append({
"Variável": "N/A",
"Tipo de Impacto": "-",
"Interpretação": "Modelo não ajustado.",
"Magnitude Estimada": "-"
})
results_regression['practical_recommendations_table_data'] = recommendations_data
return results_regression
# --- Lógica de Navegação via Botões ---
# Inicializa o estado de página (ANOVA ou REGRESSAO)
if 'page' not in st.session_state:
st.session_state.page = 'ANOVA'
# Botões de navegação
col1, col2 = st.columns([1, 1])
with col1:
if st.button('📊 ANOVA'):
st.session_state.page = 'ANOVA'
with col2:
if st.button('📈 Regressão'):
st.session_state.page = 'REGRESSAO'
st.markdown("---")
# --- Página ANOVA ---
if st.session_state.page == 'ANOVA':
st.title("🏠 Dashboard de ANOVA Imobiliária")
st.markdown("""
Esta seção permite realizar Análises de Variância (ANOVA) no Ames Housing Dataset,
investigando como diferentes variáveis categóricas impactam o preço de venda dos imóveis.
""")
df_anova, coluna_preco_anova, colunas_categoricas_selecionaveis, todas_colunas_anova = load_data_anova()
if df_anova is not None and coluna_preco_anova is not None:
st.header("1. Visão Geral dos Dados (ANOVA)")
if st.checkbox("Mostrar amostra dos dados"):
st.dataframe(df_anova.head())
st.write(f"Total de registros carregados: {len(df_anova)}")
st.write(f"Coluna alvo (preço): `{coluna_preco_anova}`")
st.sidebar.header("⚙️ Configurações ANOVA")
variaveis_selecionadas = st.sidebar.multiselect(
"Escolha 1 a 3 variáveis categóricas para ANOVA:",
options=colunas_categoricas_selecionaveis,
max_selections=3
)
if variaveis_selecionadas:
st.header("2. Resultados da Análise ANOVA")
st.markdown(f"Analisando **{', '.join(variaveis_selecionadas)}** sobre **{coluna_preco_anova}**.")
for var_analisada in variaveis_selecionadas:
st.subheader(f"Análise para: `{var_analisada}`")
df_analise_var = df_anova[[var_analisada, coluna_preco_anova]].copy()
df_analise_var.dropna(subset=[var_analisada, coluna_preco_anova], inplace=True)
if df_analise_var.empty or df_analise_var[var_analisada].nunique() < 2:
st.warning(f"Dados insuficientes ou poucos níveis para '{var_analisada}'. Pulando.")
continue
resultados_var = perform_anova_for_variable(df_analise_var, var_analisada, coluna_preco_anova)
if "error" in resultados_var:
st.error(f"Erro ao analisar '{var_analisada}': {resultados_var['error']}")
continue
# Tabela ANOVA
if "anova_table" in resultados_var:
st.markdown("**Tabela ANOVA:**")
st.dataframe(resultados_var["anova_table"])
p_anova = resultados_var.get("p_valor_anova")
if p_anova is not None:
if p_anova < 0.05:
st.success(f"✅ Diferença significativa (p-valor: {p_anova:.4e}).")
else:
st.info(f"ℹ️ Sem diferença significativa (p-valor: {p_anova:.4e}).")
# Pressupostos e Testes Alternativos
with st.expander("Verificar Pressupostos e Testes Alternativos"):
st.markdown("**Normalidade dos Resíduos:**")
if "shapiro_test" in resultados_var:
stat, p_val = resultados_var["shapiro_test"]
st.write(f"Shapiro-Wilk: Estatística={stat:.4f}, P-valor={p_val:.4e}")
elif "anderson_test" in resultados_var:
ad_res = resultados_var["anderson_test"]
st.write(f"Anderson-Darling: Estatística={ad_res.statistic:.4f}")
if resultados_var.get("normalidade_ok"):
st.success("✅ Resíduos parecem normalmente distribuídos.")
else:
st.warning("⚠️ Resíduos NÃO parecem normalmente distribuídos.")
if "normalidade" in resultados_var["plots"]:
st.pyplot(resultados_var["plots"]["normalidade"])
st.markdown("**Homogeneidade das Variâncias (Levene):**")
if "levene_test" in resultados_var:
stat_l, p_l = resultados_var["levene_test"]
st.write(f"Levene: Estatística={stat_l:.4f}, P-valor={p_l:.4e}")
if resultados_var.get("homocedasticidade_ok"):
st.success("✅ Variâncias homogêneas.")
else:
st.warning("⚠️ Variâncias NÃO homogêneas.")
else:
st.write("Levene não pôde ser realizado (insuficiente).")
if "kruskal_test" in resultados_var:
st.markdown("**Kruskal-Wallis (Não Paramétrico):**")
stat_k, p_k = resultados_var["kruskal_test"]
st.write(f"Kruskal-Wallis: Estatística={stat_k:.4f}, P-valor={p_k:.4e}")
if p_k < 0.05:
st.success("✅ Diferença significativa nas medianas.")
else:
st.info("ℹ️ Sem diferença significativa nas medianas.")
# Boxplot
if "boxplot" in resultados_var["plots"]:
st.markdown("**Distribuição de Preços por Categoria:**")
st.pyplot(resultados_var["plots"]["boxplot"])
st.markdown("---")
elif not variaveis_selecionadas:
st.sidebar.warning("Selecione ao menos uma variável para executar a ANOVA.")
st.sidebar.markdown("---")
st.sidebar.markdown("Desenvolvido para análise imobiliária.")
if variaveis_selecionadas:
st.header("3. Insights Gerais e Recomendações ANOVA")
with st.expander("Ver Recomendações"):
st.markdown("""
### Como interpretar:
- ANOVA ajuda a entender se variáveis categóricas (ex: estilo da casa, ano, telhado)
têm associação significativa com o preço.
- **Variáveis com p-valor < 0.05**: sugerem diferença estatisticamente significativa.
- Use essas informações para ajustar estratégia de precificação e marketing.
- Para análise multivariada, prossiga para Regressão Linear Múltipla.
""")
# NOVO: Inserir campo para a análise fornecida dentro da seção ANOVA
st.header("🔎 Análise de Modelo e Recomendações Exemplo")
st.markdown("""
Foram escolhidas 6 variáveis, incluindo variáveis contínuas e categóricas:
**Contínuas**:
- **grlivarea**: Área construída (acima do solo) em pés quadrados.
- **overallqual**: Qualidade geral do imóvel (escala de 1 a 10).
- **garagecars**: Capacidade da garagem (número de carros).
**Categóricas (convertidas em dummies)**:
- **neighborhood**: Bairro.
- **area_faixa**: Faixas da área construída.
📊 **Resultados do Modelo (Regressão Linear Múltipla)**:
- **R² = 0,7535** → O modelo explica aproximadamente 75,35% da variabilidade do preço de venda dos imóveis, o que é considerado muito bom para dados econômicos/sociais.
- **RMSE = 39.665** → Erro médio quadrático (desvio padrão dos erros).
- **MAE = 27.558** → Erro médio absoluto — em média, o modelo erra cerca de 27 mil dólares por previsão.
🏗️ **Coeficientes Estimados (Impacto das Variáveis)**:
- **grlivarea = 52,32** → Cada aumento de 1 pé² na área construída eleva o preço em 52,32 dólares. Ex.: 100 pés² = +5.232 dólares.
- **overallqual = 28.190** → Cada ponto a mais na qualidade geral do imóvel (1 a 10) aumenta o preço em 28.190 dólares. Impacto muito significativo.
- **garagecars = 19.700** → Cada vaga adicional na garagem eleva o preço em 19.700 dólares.
📏 **Análise dos Pressupostos**:
1. **Linearidade**: ✅
- O gráfico de resíduos vs valores ajustados não apresentou padrões severos, embora haja leve tendência nas caudas, o que é aceitável.
2. **Normalidade dos resíduos**: ❌
- Teste de Shapiro-Wilk: p < 0.0001 → Os resíduos não são normais.
- Contudo, com amostras grandes (> 2000 observações), a normalidade dos resíduos não é um pressuposto crítico para a validade dos coeficientes (teorema central do limite). Impacta mais a construção de intervalos de confiança.
3. **Homocedasticidade**: ❌
- O teste de Levene nas ANOVAs sugere heterocedasticidade (variância dos resíduos não é constante).
4. **Multicolinearidade**: ✅
- VIF (Fator de Inflação da Variância) está baixo para as variáveis:
- grlivarea: 1,56
- overallqual: 1,85
- garagecars: 1,64
- Sem risco de multicolinearidade.
🔥 **Principais Insights**:
- A variável de maior impacto absoluto é **overallqual** (qualidade geral).
→ Imóveis de melhor acabamento, materiais, design e estado de conservação são fortemente valorizados.
- **Garagem** também tem peso elevado: cada vaga adicional vale quase 20 mil dólares.
- A **área construída** tem impacto linear relevante: quanto maior o imóvel, maior o preço.
- As variáveis de localização (**neighborhood**) e faixa de área (**area_faixa**) também são importantes, mas os coeficientes não foram mostrados diretamente na tabela (porque são muitas dummies). Sabemos, porém, que bairros premium têm preços bem mais altos.
🔄 **Transformações Logarítmicas (Seriam Necessárias?)**:
- Dado que há:
- Não normalidade dos resíduos;
- Heterocedasticidade;
- ➡️ Seria adequado testar um modelo log-log (`log(preço) ~ log(área) + outras`) para melhorar os pressupostos.
- Vantagem do modelo log-log:
- Os coeficientes passam a ser interpretados como variações percentuais.
- Ex.: Um coeficiente de 0,15 indica que um aumento de 1% na área resulta em um aumento de 0,15% no preço.
**Recomendações**:
- Foque em imóveis com alta qualidade de construção.
→ A cada ponto a mais na qualidade, você valoriza o imóvel em quase 30 mil dólares.
- Invista em melhorar vagas de garagem.
→ Acrescentar uma vaga pode agregar aproximadamente 20 mil dólares ao valor.
- Área construída é importante, mas cresce de forma linear.
→ Estratégia: Ampliações moderadas são rentáveis até certo ponto.
- Atenção à localização (**Neighborhood**).
→ Embora os coeficientes individuais não estejam visíveis no resumo, é sabido que bairros mais valorizados impactam fortemente o preço. Você deve usar essa informação para selecionar imóveis em regiões de maior demanda.
- Para modelagem mais robusta:
→ Recomenda-se testar modelos logarítmicos que podem entregar previsões mais estáveis.
- Cuidado com imóveis fora do padrão:
→ O modelo tem maiores erros nos extremos — imóveis muito caros ou muito baratos.
📌 **Decisão com Confiança**:
- Priorize imóveis bem construídos, em bairros consolidados, com boa metragem e pelo menos 2 vagas de garagem.
- Foque sua argumentação nesses atributos para justificar o preço do imóvel aos clientes.
""")
else:
if df_anova is None:
st.warning("Aguardando carregamento dos dados ou verifique as URLs.")
elif coluna_preco_anova is None:
st.error(f"Coluna de preço não encontrada. Verifique as colunas: {todas_colunas_anova}")
# --- Página REGRESSÃO ---
elif st.session_state.page == 'REGRESSAO':
st.title("🏠 Dashboard de Regressão Imobiliária")
st.markdown("""
Esta seção permite realizar Modelagem Preditiva com Regressão Linear Múltipla
no Ames Housing Dataset para entender o impacto de variáveis contínuas e categóricas no preço.
""")
df_reg, coluna_preco_reg, colunas_categoricas_reg, colunas_continuas_reg, todas_colunas_reg = load_data_reg()
if df_reg is not None and coluna_preco_reg is not None:
st.header("1. Visão Geral dos Dados (Regressão)")
if st.checkbox("Mostrar amostra aleatória dos dados"):
st.dataframe(df_reg.sample(min(5, len(df_reg))))
st.write(f"Total registros: {len(df_reg)}")
st.write(f"Coluna alvo (preço): `{coluna_preco_reg}`")
# Configurações no sidebar
st.sidebar.title("⚙️ Configurações Regressão")
st.sidebar.header("Seleção de Variáveis")
st.sidebar.markdown("Escolha 4 a 6 variáveis (≥1 contínua e ≥1 categórica).")
default_cont = ['grlivarea', 'overallqual', 'yearbuilt', 'totalbsmtsf']
valid_default_cont = [v for v in default_cont if v in colunas_continuas_reg]
if not valid_default_cont and colunas_continuas_reg:
valid_default_cont = colunas_continuas_reg[:1]
reg_continuous_vars = st.sidebar.multiselect(
"Variáveis Contínuas:",
options=colunas_continuas_reg,
default=valid_default_cont,
key="reg_cont_vars"
)
default_cat = ['neighborhood', 'housestyle']
valid_default_cat = [v for v in default_cat if v in colunas_categoricas_reg]
if not valid_default_cat and colunas_categoricas_reg:
valid_default_cat = colunas_categoricas_reg[:1]
reg_categorical_vars = st.sidebar.multiselect(
"Variáveis Categóricas:",
options=colunas_categoricas_reg,
default=valid_default_cat,
key="reg_cat_vars"
)
total_vars = len(reg_continuous_vars) + len(reg_categorical_vars)
valid_selection = True
if not (4 <= total_vars <= 6):
st.sidebar.warning(f"Selecione entre 4 e 6 variáveis (total atual: {total_vars}).")
valid_selection = False
if not reg_continuous_vars:
st.sidebar.warning("Selecione ao menos 1 variável contínua.")
valid_selection = False
if not reg_categorical_vars:
st.sidebar.warning("Selecione ao menos 1 variável categórica.")
valid_selection = False
st.markdown("---")
st.header("2. Análise Exploratória das Variáveis Selecionadas")
if reg_continuous_vars or reg_categorical_vars:
if st.checkbox("Mostrar Distribuições das Variáveis Selecionadas", value=False):
st.markdown("##### Distribuições das Variáveis Contínuas")
for var_cont in reg_continuous_vars:
if var_cont in df_reg.columns:
fig, ax = plt.subplots(figsize=(6, 3))
sns.histplot(df_reg[var_cont], kde=True, ax=ax, bins=30)
ax.set_title(f"Distribuição de {var_cont}")
st.pyplot(fig)
else:
st.warning(f"'{var_cont}' não encontrada para plotar.")
st.markdown("##### Contagem das Categorias das Variáveis Categóricas")
for var_cat in reg_categorical_vars:
if var_cat in df_reg.columns:
fig, ax = plt.subplots(figsize=(7, 4))
if df_reg[var_cat].nunique() > 5:
sns.countplot(y=df_reg[var_cat], ax=ax,
order=df_reg[var_cat].value_counts().index, palette="viridis")
else:
sns.countplot(x=df_reg[var_cat], ax=ax,
order=df_reg[var_cat].value_counts().index, palette="viridis")
plt.xticks(rotation=45, ha="right")
ax.set_title(f"Contagem de {var_cat}")
plt.tight_layout()
st.pyplot(fig)
else:
st.warning(f"'{var_cat}' não encontrada para plotar contagem.")
if st.checkbox("Mostrar Mapa de Correlação das Contínuas + Preço", value=False):
st.markdown("##### Mapa de Correlação")
vars_corr = [var for var in reg_continuous_vars if var in df_reg.columns] + [coluna_preco_reg]
if len(vars_corr) > 1:
corr_matrix = df_reg[vars_corr].corr()
fig_corr, ax_corr = plt.subplots(
figsize=(min(10, len(vars_corr) * 1.5), min(8, len(vars_corr) * 1.2))
)
sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", fmt=".2f", linewidths=.5, ax=ax_corr)
ax_corr.set_title("Mapa de Correlação")
st.pyplot(fig_corr)
else:
st.info("Selecione ao menos duas variáveis numéricas para o mapa de correlação.")
st.markdown("---")
if st.button("Executar Regressão Linear", disabled=not valid_selection):
with st.spinner("Executando regressão..."):
output_reg = run_linear_regression_analysis(df_reg, coluna_preco_reg,
reg_continuous_vars, reg_categorical_vars)
if "error" in output_reg:
st.error(output_reg["error"])
else:
st.subheader("Resultados do Modelo de Regressão")
model_summary_obj = output_reg.get('model_summary_obj')
if model_summary_obj:
st.markdown("##### Sumário Geral do Modelo:")
sum_table0 = pd.read_html(model_summary_obj.tables[0].as_html(), header=None, index_col=None)[0]
st.table(sum_table0.iloc[:, :2].rename(columns={0: "Métrica", 1: "Valor"}))
st.table(sum_table0.iloc[:, 2:].rename(columns={2: "Métrica", 3: "Valor"}))
st.markdown("##### Coeficientes do Modelo:")
sum_table1 = pd.read_html(model_summary_obj.tables[1].as_html(), header=0, index_col=0)[0]
st.dataframe(sum_table1.style.format({
"coef": "{:.4f}", "std err": "{:.4f}", "t": "{:.3f}", "P>|t|": "{:.3e}",
"[0.025": "{:.4f}", "0.975]": "{:.4f}"
}))
if len(model_summary_obj.tables) > 2:
st.markdown("##### Outras Estatísticas e Notas:")
notes_html = model_summary_obj.tables[2].as_html()
notes_df = pd.read_html(notes_html, header=None, index_col=None)[0]
for i in range(len(notes_df)):
line = notes_df.iloc[i].tolist()
st.text(" ".join([str(x) for x in line if pd.notna(x)]))
st.subheader("Métricas de Desempenho")
if 'performance_metrics' in output_reg:
metrics_df = pd.DataFrame.from_dict(output_reg['performance_metrics'], orient='index', columns=['Valor'])
st.table(metrics_df.style.format("{:.4f}"))
st.markdown("""
* **R-squared / R-squared Ajustado:** Variância explicada pelo modelo.
* **RMSE (log) / MAE (log):** Erros médios na escala logarítmica.
""")
st.subheader("Interpretação dos Coeficientes")
if 'coefficients_interpretation_notes' in output_reg:
st.markdown(output_reg['coefficients_interpretation_notes'])
st.subheader("Recomendações Práticas")
if 'practical_recommendations_table_data' in output_reg:
recom_df = pd.DataFrame(output_reg['practical_recommendations_table_data'])
if not recom_df.empty:
st.dataframe(recom_df)
else:
st.info("Nenhuma recomendação gerada (verifique significância).")
st.sidebar.markdown("---")
st.sidebar.info("Dashboard de Regressão Imobiliária")
else:
if df_reg is None:
st.error("Falha ao carregar dados. Verifique a conexão ou a URL.")
elif coluna_preco_reg is None:
st.error(f"Coluna de preço não identificada. Colunas disponíveis: {todas_colunas_reg}")
else:
if not colunas_categoricas_reg and not colunas_continuas_reg:
st.error("Nenhuma coluna adequada identificada para regressão.")