Spaces:
Sleeping
Sleeping
File size: 22,379 Bytes
1dc6e00 d96c3b5 460b3f8 53f6209 5aa12c9 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 68e8afb 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 1dc6e00 68e8afb 460b3f8 1dc6e00 460b3f8 5aa12c9 460b3f8 e04e8d9 5aa12c9 079e524 5aa12c9 cc1256f 5aa12c9 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 53f6209 1dc6e00 460b3f8 b7c7285 460b3f8 1dc6e00 460b3f8 1dc6e00 460b3f8 5d09271 6f452db 460b3f8 1dc6e00 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 460b3f8 b7c7285 ecb3b2c b7c7285 ef0fb9f b7c7285 ef0fb9f b7c7285 ef0fb9f b7c7285 ef0fb9f 460b3f8 b7c7285 68e8afb b7c7285 460b3f8 b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb 460b3f8 b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 68e8afb b7c7285 6a7ef4a 68e8afb b7c7285 68e8afb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 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 | # 1. Importar Bibliotecas
import streamlit as st
import pandas as pd
import numpy as np
import kagglehub
from kagglehub import KaggleDatasetAdapter
import plotly.express as px
import plotly.graph_objects as go
from scipy.stats import shapiro, levene, kruskal
import statsmodels.api as sm
from statsmodels.formula.api import ols
from statsmodels.stats.stattools import durbin_watson
from statsmodels.stats.diagnostic import het_breuschpagan
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.stats.stattools import jarque_bera
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.model_selection import cross_val_score, cross_val_predict, train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
import os
import warnings
warnings.filterwarnings("ignore")
# 2. Configuração da página e funções auxiliares
# Configuração da página do Streamlit
st.set_page_config(
page_title="Análise de Precificação Imobiliária ",
layout="wide",
initial_sidebar_state="expanded"
)
@st.cache_data
def load_data():
"""
Carrega o dataset do Kaggle diretamente para um DataFrame do Pandas
utilizando a API Kaggle Hub com o Pandas Adapter.
"""
try:
cache_path = "/tmp/kagglehub_cache"
os.makedirs(cache_path, exist_ok=True)
os.environ['KAGGLEHUB_CACHE'] = cache_path
df = kagglehub.load_dataset(
KaggleDatasetAdapter.PANDAS,
"prevek18/ames-housing-dataset",
"AmesHousing.csv",
)
# Limpeza e pré-processamento
if 'Order' in df.columns:
df.rename(columns={'Order': 'OrderID'}, inplace=True)
if 'PID' in df.columns:
df.rename(columns={'PID': 'PropertyID'}, inplace=True)
if 'MS SubClass' in df.columns:
df['MS SubClass'] = df['MS SubClass'].astype(str)
if 'Mo Sold' in df.columns:
df['Mo Sold'] = df['Mo Sold'].astype(str)
# Tratamento robusto de valores nulos
numeric_cols = df.select_dtypes(include=np.number).columns
df[numeric_cols] = df[numeric_cols].fillna(df[numeric_cols].median())
categorical_cols = df.select_dtypes(include=['object']).columns
df[categorical_cols] = df[categorical_cols].fillna('Missing')
return df
except Exception as e:
st.error(f"Erro ao baixar ou carregar os dados do Kaggle: {e}")
st.info("Verifique as credenciais do Kaggle (kaggle.json) nos Secrets do Space.")
return None
def safe_numeric_columns(df):
cols = df.select_dtypes(include=np.number).columns
exclude = [c for c in ['SalePrice', 'OrderID', 'PropertyID'] if c in cols]
return cols.drop(exclude).tolist()
def group_top_categories(df, col, top_n=10):
"""
Agrupa categorias menos frequentes em 'Other' para reduzir cardinalidade.
Retorna Série transformada.
"""
counts = df[col].value_counts(dropna=False)
top = counts.nlargest(top_n).index
return df[col].where(df[col].isin(top), other='Other')
def compute_vif(X_df):
"""
Computa VIF para DataFrame X_df (sem constante).
"""
vif_df = pd.DataFrame({
"feature": X_df.columns,
"VIF": [variance_inflation_factor(X_df.values, i) for i in range(X_df.shape[1])]
})
return vif_df
# Carrega os dados
data = load_data()
# 3. Layout do Dashboard (título e sidebar)
st.sidebar.title("Configurações da Análise")
st.sidebar.header("Tarefa 2 - SIEP")
st.sidebar.markdown("Esta aplicação interativa realiza uma análise de dados imobiliários com ANOVA e regressão linear.")
# Título Principal
st.title("Análise Preditiva de Preços de Imóveis")
st.markdown("Dashboard para a disciplina de Sistemas de Informação em Engenharia de Produção")
tab_intro, tab_eda, tab_anova, tab_regressao = st.tabs([
"Introdução", "Análise Exploratória (EDA)", "Análise de Variância (ANOVA)", "Modelo de Regressão Linear"
])
# 4. Conteudo das abas
if data is not None:
with tab_intro:
st.header("Contexto do Projeto")
st.write("""
Projeto: prever preço de venda (`SalePrice`) usando Regressão Linear Múltipla.
Requisitos principais da tarefa:
- Escolher 4 a 6 variáveis explicativas (pelo menos 1 contínua e 1 categórica).
- Ajustar modelo de regressão sem interações.
- Avaliar pressupostos (linearidade, normalidade, homocedasticidade, multicolinearidade).
- Aplicar transformações logarítmicas quando necessário e interpretar coeficientes.
- Avaliar desempenho (R², RMSE, MAE).
- Gerar recomendações e (bônus) disponibilizar interface interativa.
""")
with tab_eda:
st.header("Análise Exploratória de Dados (EDA)")
if st.checkbox("Mostrar uma amostra dos dados"):
st.dataframe(data.head())
st.write(f"O dataset contém **{data.shape[0]}** linhas e **{data.shape[1]}** colunas.")
st.subheader("Distribuição do Preço de Venda (SalePrice)")
col1, col2 = st.columns(2)
with col1:
fig_hist_price = px.histogram(data, x='SalePrice', nbins=100, title="Histograma de SalePrice")
st.plotly_chart(fig_hist_price, use_container_width=True)
with col2:
fig_box_price = px.box(data, y='SalePrice', title="Boxplot de SalePrice")
st.plotly_chart(fig_box_price, use_container_width=True)
st.markdown("**Interpretação:** `SalePrice` tende a ser assimétrico à direita — considere transformação logarítmica para modelagem.")
st.subheader("Correlação com SalePrice")
if st.checkbox("Calcular e mostrar correlações"):
numeric_cols = data.select_dtypes(include=np.number).columns.tolist()
corr_matrix = data[numeric_cols].corr()
corr_saleprice = corr_matrix['SalePrice'].sort_values(ascending=False).iloc[1:16]
fig_corr = px.bar(corr_saleprice, x=corr_saleprice.values, y=corr_saleprice.index, orientation='h',
title="Top 15 Variáveis Mais Correlacionadas com SalePrice",
labels={'x': 'Coeficiente de Correlação', 'y': 'Variável'})
st.plotly_chart(fig_corr, use_container_width=True)
st.markdown("**Interpretação:** `Overall Qual` e `Gr Liv Area` costumam ser fortes preditores.")
with tab_anova:
st.header("Análise de Variância (ANOVA)")
st.sidebar.header("Configurações da ANOVA")
suggested_cols_anova = ['Overall Qual', 'Neighborhood', 'Garage Cars', 'Full Bath', 'Kitchen Qual', 'Foundation']
valid_suggested_anova = [col for col in suggested_cols_anova if col in data.columns]
if not valid_suggested_anova:
st.warning("Nenhuma das colunas sugeridas para ANOVA foi encontrada no dataset.")
else:
anova_var = st.sidebar.selectbox("Selecione a variável categórica:", valid_suggested_anova)
alpha_anova = st.sidebar.slider("Nível de Significância (α) - ANOVA", 0.01, 0.10, 0.05, 0.01, key="alpha_anova")
st.subheader(f"Preço de Venda por '{anova_var}'")
fig_anova_box = px.box(data, x=anova_var, y='SalePrice', title=f"Boxplot de SalePrice por {anova_var}",
category_orders={anova_var: data.groupby(anova_var)['SalePrice'].median().sort_values().index})
st.plotly_chart(fig_anova_box, use_container_width=True)
if st.button("Executar Análise ANOVA"):
groups = [group["SalePrice"].dropna() for name, group in data.groupby(anova_var)]
stat_levene, p_levene = levene(*groups)
# Envolve a variável em Q("") para lidar com espaços no nome
formula = f'SalePrice ~ C(Q("{anova_var}"))'
model_ols = ols(formula, data=data).fit()
stat_shapiro, p_shapiro = shapiro(model_ols.resid)
st.subheader("Resultados dos Testes de Pressupostos")
st.markdown(f"**Levene (homocedasticidade):** p = {p_levene:.4f}")
st.markdown(f"**Shapiro-Wilk (normalidade resíduos):** p = {p_shapiro:.4f}")
st.info("Shapiro-Wilk pode rejeitar normalidade em grandes amostras — verifique QQ-plot e JB.")
if p_levene >= alpha_anova and p_shapiro >= alpha_anova:
st.info("Pressupostos atendidos — aplicando ANOVA (F).")
anova_result = sm.stats.anova_lm(model_ols, typ=2)
st.dataframe(anova_result)
p_value_main = anova_result['PR(>F)'][0]
test_used = "ANOVA"
else:
st.warning("Pressupostos não atendidos — aplicando Kruskal-Wallis (teste não paramétrico).")
stat_kruskal, p_kruskal = kruskal(*groups)
st.markdown(f"**Kruskal-Wallis:** estatística = {stat_kruskal:.3f}, p = {p_kruskal:.4f}")
p_value_main = p_kruskal
test_used = "Kruskal-Wallis"
if p_value_main < alpha_anova:
st.success(f"✅ Diferença significativa entre grupos detectada (p = {p_value_main:.4f} < {alpha_anova})")
else:
st.info(f"ℹ️ Nenhuma diferença estatisticamente significativa encontrada entre grupos (p = {p_value_main:.4f} >= {alpha_anova})")
st.subheader("📊 Interpretação Prática e Impacto para o Negócio")
group_stats = data.groupby(anova_var)['SalePrice'].agg([
('n', 'count'),
('média', 'mean'),
('mediana', 'median'),
('desvio_padrão', 'std'),
('mínimo', 'min'),
('máximo', 'max')
]).round(2)
group_stats = group_stats.sort_values('média', ascending=False)
st.markdown("**Estatísticas Descritivas por Categoria:**")
st.dataframe(group_stats.style.format({
'média': '${:,.2f}',
'mediana': '${:,.2f}',
'desvio_padrão': '${:,.2f}',
'mínimo': '${:,.2f}',
'máximo': '${:,.2f}'
}))
if p_value_main < alpha_anova:
st.markdown("---")
st.markdown("### 🎯 Insights Estratégicos para Tomada de Decisão")
categoria_mais_cara = group_stats.index[0]
preco_mais_caro = group_stats.iloc[0]['média']
categoria_mais_barata = group_stats.index[-1]
preco_mais_barato = group_stats.iloc[-1]['média']
diferenca_absoluta = preco_mais_caro - preco_mais_barato
diferenca_percentual = ((preco_mais_caro / preco_mais_barato) - 1) * 100
st.markdown(f"""
**1️⃣ Diferença de Valorização Identificada:**
- A categoria **'{categoria_mais_cara}'** apresenta o maior preço médio: **${preco_mais_caro:,.2f}**
- A categoria **'{categoria_mais_barata}'** apresenta o menor preço médio: **${preco_mais_barato:,.2f}**
- **Diferença:** ${diferenca_absoluta:,.2f} ({diferenca_percentual:.1f}% mais caro)
""")
if anova_var in ['Neighborhood', 'MS Zoning']:
st.markdown(f"""
**2️⃣ Recomendações para Investidores:**
- **Oportunidade de alto retorno:** Focar em imóveis na categoria '{categoria_mais_cara}' pode maximizar o valor de revenda.
- **Oportunidade de valorização:** Imóveis em '{categoria_mais_barata}' podem ser boas opções para reforma/revitalização se houver potencial de melhoria da região.
""")
elif anova_var in ['Overall Qual', 'Kitchen Qual', 'Exter Qual']:
st.markdown(f"""
**2️⃣ Recomendações para Proprietários:**
- **Investimento em melhorias:** Elevar a qualidade de '{categoria_mais_barata}' para '{categoria_mais_cara}' pode agregar até ${diferenca_absoluta:,.2f} ao valor do imóvel.
- **Priorização de reformas:** Foque em melhorias que elevem a classificação de qualidade, pois o impacto no preço é estatisticamente comprovado.
""")
elif anova_var in ['Garage Cars', 'Full Bath', 'Bedroom AbvGr']:
st.markdown(f"""
**2️⃣ Recomendações de Design e Reforma:**
- **Impacto quantitativo:** Aumentar de '{categoria_mais_barata}' para '{categoria_mais_cara}' está associado a um acréscimo médio de ${diferenca_absoluta:,.2f} no valor.
- **Reforma estratégica:** Se viável, considere adicionar unidades/capacidade nesta característica para valorizar o imóvel.
""")
else:
st.markdown("---")
st.markdown("### ℹ️ Interpretação")
st.markdown(f"""
Não foram encontradas diferenças estatisticamente significativas nos preços médios entre as categorias de **'{anova_var}'** (p = {p_value_main:.4f}).
**Implicações práticas:** Esta característica **não é um diferencial significativo** na precificação de imóveis quando analisada isoladamente.
""")
st.caption(f"💡 Fonte: Análise do Ames Housing Dataset usando {test_used} (α = {alpha_anova})")
with tab_regressao:
st.header("Modelo de Regressão Linear Múltipla")
st.sidebar.header("Configurações da Regressão")
numeric_cols_reg = safe_numeric_columns(data)
categorical_cols_reg = [c for c in ['Neighborhood', 'House Style', 'Overall Qual', 'Kitchen Qual', 'Foundation', 'Exter Qual'] if c in data.columns]
selected_numeric = st.sidebar.multiselect("Selecione variáveis numéricas (contínuas):", numeric_cols_reg,
default=['Gr Liv Area', 'Garage Area', 'Total Bsmt SF', '1st Flr SF'])
selected_categorical = st.sidebar.multiselect("Selecione variáveis categóricas:", categorical_cols_reg,
default=['Overall Qual', 'Neighborhood'])
st.sidebar.markdown("**Requisitos:** escolha entre 4 e 6 variáveis no total; pelo menos 1 contínua e 1 categórica.")
use_log_transform = st.sidebar.checkbox("Usar transformação log1p em y e em variáveis numéricas selecionadas", value=True)
alpha_reg = st.sidebar.slider("Nível de Significância (α) - Regressão", 0.01, 0.10, 0.05, 0.01, key="alpha_reg")
if st.button("Executar Modelo de Regressão"):
total_selected = len(selected_numeric) + len(selected_categorical)
if not (4 <= total_selected <= 6):
st.error("Seleção inválida: escolha entre 4 e 6 variáveis no total (numéricas + categóricas).")
st.stop()
if len(selected_numeric) < 1 or len(selected_categorical) < 1:
st.error("Seleção inválida: deve haver pelo menos 1 variável contínua e 1 categórica.")
st.stop()
cols_model = selected_numeric + selected_categorical + ['SalePrice']
df_model = data[cols_model].copy()
df_model[selected_numeric] = df_model[selected_numeric].fillna(df_model[selected_numeric].median())
for c in selected_categorical:
if c in df_model.columns:
df_model[c] = df_model[c].fillna('Missing')
high_card_cols = [c for c in selected_categorical if df_model[c].nunique() > 15]
for c in high_card_cols:
st.info(f"A coluna '{c}' tem alta cardinalidade ({df_model[c].nunique()} categorias). Agrupando menores frequências em 'Other' (top 10 mantidos).")
df_model[c] = group_top_categories(df_model, c, top_n=10)
if use_log_transform:
df_model['SalePrice'] = np.log1p(df_model['SalePrice'])
for col in selected_numeric:
if (df_model[col] >= 0).all():
df_model[col] = np.log1p(df_model[col])
else:
st.warning(f"A variável '{col}' contém valores negativos e permanecerá na escala original.")
X = pd.get_dummies(df_model.drop('SalePrice', axis=1), drop_first=True, dtype=float)
y = df_model['SalePrice']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
X_train_sm = sm.add_constant(X_train)
X_test_sm = sm.add_constant(X_test, has_constant='add')
model = sm.OLS(y_train, X_train_sm).fit()
st.subheader("Sumário do Modelo (Treino)")
st.text(model.summary().as_text())
pred_test = model.predict(X_test_sm)
y_test_orig = np.expm1(y_test) if use_log_transform else y_test
pred_test_orig = np.expm1(pred_test) if use_log_transform else pred_test
rmse_test = np.sqrt(mean_squared_error(y_test_orig, pred_test_orig))
mae_test = mean_absolute_error(y_test_orig, pred_test_orig)
st.subheader("Métricas de Desempenho (no conjunto de teste)")
col1, col2 = st.columns(2)
col1.metric("RMSE (teste)", f"${rmse_test:,.2f}")
col2.metric("MAE (teste)", f"${mae_test:,.2f}")
st.subheader("Diagnósticos dos Pressupostos (no conjunto de treino)")
resid = model.resid
_, p_shapiro = shapiro(resid)
jb_stat, jb_pvalue, _, _ = jarque_bera(resid)
st.markdown(f"- **Normalidade dos resíduos (Shapiro-Wilk):** p = {p_shapiro:.4f} {'✅' if p_shapiro >= alpha_reg else '❌'}")
st.markdown(f"- **Normalidade dos resíduos (Jarque-Bera):** p = {jb_pvalue:.4f} {'✅' if jb_pvalue >= alpha_reg else '❌'}")
fig_qq = sm.qqplot(resid, line='45', fit=True)
st.pyplot(fig_qq)
_, bp_lm_pvalue, _, _ = het_breuschpagan(resid, model.model.exog)
st.markdown(f"- **Homocedasticidade (Breusch-Pagan):** p = {bp_lm_pvalue:.4f} {'✅' if bp_lm_pvalue >= alpha_reg else '❌'}")
dw = durbin_watson(resid)
st.markdown(f"- **Autocorrelação de resíduos (Durbin-Watson):** {dw:.3f} (valores próximos de 2 são ideais)")
predictions_train = model.fittedvalues
fig_line = px.scatter(x=predictions_train, y=resid, labels={'x': 'Valores Preditos', 'y': 'Resíduos'}, title='Resíduos vs. Valores Preditos (Linearidade)')
fig_line.add_hline(y=0, line_dash="dash", line_color="red")
st.plotly_chart(fig_line, use_container_width=True)
X_vif = X_train_sm.drop(columns=['const'], errors='ignore')
if not X_vif.empty:
vif_df = compute_vif(X_vif)
st.subheader("VIF (Multicolinearidade)")
st.dataframe(vif_df.sort_values('VIF', ascending=False).reset_index(drop=True))
if (vif_df['VIF'] > 10).any():
st.warning("VIFs > 10 indicam multicolinearidade severa.")
if bp_lm_pvalue < alpha_reg:
st.warning("Heterocedasticidade detectada. Exibindo resultados com erros robustos (HC3).")
robust_res = model.get_robustcov_results(cov_type='HC3')
st.text(robust_res.summary().as_text())
st.subheader("🎯 Recomendações Práticas")
if bp_lm_pvalue < alpha_reg:
final_model_params = pd.Series(robust_res.params, index=model.params.index)
final_model_pvalues = pd.Series(robust_res.pvalues, index=model.params.index)
else:
final_model_params = model.params
final_model_pvalues = model.pvalues
for var in final_model_params.index:
if var == 'const': continue
if final_model_pvalues[var] < alpha_reg:
coef = final_model_params[var]
is_dummy = any(cat_col in var for cat_col in selected_categorical)
if use_log_transform:
if is_dummy:
impact = (np.exp(coef) - 1) * 100
st.write(f"🏷️ **{var}**: Associado a uma alteração de **{impact:+.2f}%** no preço.")
else: # contínua
impact = coef * 100
st.write(f"📈 **{var}**: Aumento de 1% está associado a **{impact:+.2f}%** no preço.")
else: # modelo linear
st.write(f"**{var}**: Aumento de 1 unidade está associado a **${coef:,.2f}** no preço.")
st.subheader("Validação Cruzada")
if st.checkbox("Mostrar Validação Cruzada (5 folds)"):
pipeline = make_pipeline(StandardScaler(), LinearRegression())
y_cv_pred = cross_val_predict(pipeline, X, y, cv=5)
y_orig = np.expm1(y) if use_log_transform else y
y_cv_pred_orig = np.expm1(y_cv_pred) if use_log_transform else y_cv_pred
rmse_cv = np.sqrt(mean_squared_error(y_orig, y_cv_pred_orig))
st.metric("RMSE Médio (CV 5-fold)", f"${rmse_cv:,.2f}")
else:
st.warning("Aguardando o carregamento dos dados... Verifique a conexão e as credenciais do Kaggle.")
|