Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import statsmodels.api as sm | |
| import statsmodels.formula.api as smf | |
| from statsmodels.stats.anova import anova_lm | |
| from statsmodels.stats.outliers_influence import variance_inflation_factor | |
| from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score | |
| from scipy.stats import shapiro, levene, kruskal | |
| import kagglehub | |
| import os | |
| # --- Configuração da Página do Streamlit --- | |
| st.set_page_config(layout="wide", page_title="Análise de Precificação de Imóveis") | |
| # A linha st.set_option foi removida pois não é mais necessária. | |
| # --- Título e Introdução --- | |
| st.title("📊 Dashboard de Precificação Imobiliária") | |
| st.write(""" | |
| Esta análise utiliza o dataset *Ames Housing* para entender os fatores que mais influenciam | |
| o preço de venda dos imóveis. O dashboard está dividido em duas etapas principais: | |
| 1. **Análise de Variância (ANOVA):** Compara os preços médios entre diferentes categorias de imóveis. | |
| 2. **Regressão Linear Múltipla:** Cria modelos para prever o preço de venda e mede o impacto de cada característica. | |
| """) | |
| # --- Download e Carregamento do Dataset (com cache para performance) --- | |
| # --- Funções de Carregamento de Dados --- | |
| def load_data(): | |
| """ | |
| Carrega o Ames Housing Dataset a partir de uma URL do GitHub para evitar | |
| problemas de permissão em ambientes de nuvem. | |
| """ | |
| url = "https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv" | |
| try: | |
| df = pd.read_csv(url) | |
| except Exception as e: | |
| st.error(f"Erro ao carregar os dados da URL: {e}") | |
| return None # Retorna None em caso de falha | |
| # --- Preparação dos dados (mesma lógica de antes) --- | |
| df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower() | |
| if 'grlivarea' in df.columns: | |
| bins = [0, 1000, 1500, 2000, 2500, 3000, 4000, df['grlivarea'].max() + 1] | |
| labels = ['<1000', '1000-1500', '1500-2000', '2000-2500', '2500-3000', '3000-4000', '4000+'] | |
| df['area_faixa'] = pd.cut(df['grlivarea'], bins=bins, labels=labels, include_lowest=True) | |
| return df | |
| df = load_data() | |
| # Adicione esta verificação para o caso de falha no download | |
| if df is None: | |
| st.stop() # Interrompe a execução do app se os dados não puderam ser carregados | |
| # Exibir uma amostra dos dados | |
| if st.checkbox("Mostrar amostra dos dados brutos"): | |
| st.write("Amostra dos dados carregados:", df.head()) | |
| # --- ETAPA I: ANÁLISE COM ANOVA --- | |
| st.header("ETAPA I: Análise de Variância (ANOVA)") | |
| st.write(""" | |
| Aqui, verificamos se existem diferenças estatisticamente significativas nos preços de venda | |
| com base em características categóricas dos imóveis. | |
| """) | |
| anova_vars = ['overallqual', 'fireplaces', 'area_faixa'] | |
| var_selecionada = st.selectbox("Selecione a variável para análise ANOVA:", anova_vars, index=0) | |
| if var_selecionada: | |
| st.subheader(f"🔎 Análise para '{var_selecionada}' vs Preço de Venda") | |
| # Preparação dos dados para a função | |
| df_anova = df[[var_selecionada, 'saleprice']].dropna() | |
| df_anova[var_selecionada] = df_anova[var_selecionada].astype('category') | |
| # Gráfico Boxplot para visualização | |
| fig, ax = plt.subplots(figsize=(10, 6)) | |
| sns.boxplot(x=var_selecionada, y='saleprice', data=df_anova, ax=ax) | |
| plt.title(f'Distribuição do Preço de Venda por "{var_selecionada}"', fontsize=16) | |
| plt.ylabel("Preço de Venda (SalePrice)") | |
| plt.xlabel(f"Categoria de '{var_selecionada}'") | |
| plt.xticks(rotation=45) | |
| st.pyplot(fig) | |
| # Análise estatística | |
| modelo_anova = smf.ols(f"saleprice ~ C({var_selecionada})", data=df_anova).fit() | |
| anova_table = anova_lm(modelo_anova, typ=2) | |
| residuos = modelo_anova.resid | |
| shapiro_stat, shapiro_p = shapiro(residuos) | |
| levene_stat, levene_p = levene(*[df_anova['saleprice'][df_anova[var_selecionada] == cat] for cat in df_anova[var_selecionada].unique()]) | |
| kruskal_stat, kruskal_p = kruskal(*[df_anova['saleprice'][df_anova[var_selecionada] == cat] for cat in df_anova[var_selecionada].unique()]) | |
| st.write("Resultados da ANOVA:") | |
| st.dataframe(anova_table) | |
| st.write("Verificação dos Pressupostos:") | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("Teste Shapiro-Wilk (Normalidade)", f"p={shapiro_p:.4f}", "Não Normal" if shapiro_p < 0.05 else "Normal") | |
| col2.metric("Teste Levene (Homocedasticidade)", f"p={levene_p:.4f}", "Heterocedástico" if levene_p < 0.05 else "Homocedástico") | |
| col3.metric("Teste Kruskal-Wallis (Alternativa)", f"p={kruskal_p:.4f}", "Diferença Significativa" if kruskal_p < 0.05 else "Sem Diferença") | |
| st.info(""" | |
| **Interpretação:** | |
| - O **p-valor da ANOVA (PR(>F))** indica se há diferença significativa entre os grupos. Se for baixo (< 0.05), pelo menos um grupo é diferente. | |
| - Como os pressupostos de normalidade e/ou homocedasticidade geralmente não são atendidos, olhamos para o **Teste de Kruskal-Wallis**. Um p-valor baixo aqui confirma que a variável analisada tem um impacto significativo no preço. | |
| """) | |
| # --- ETAPA II: REGRESSÃO LINEAR MÚLTIPLA --- | |
| st.header("ETAPA II: Regressão Linear Múltipla") | |
| st.write(""" | |
| Nesta etapa, construímos um modelo para prever o preço de venda com base em múltiplas | |
| variáveis e avaliamos sua performance e pressupostos. | |
| """) | |
| # --- Preparação dos dados para Regressão --- | |
| df_model = df[['saleprice', 'grlivarea', 'overallqual', 'garagecars', 'neighborhood', 'area_faixa']].dropna() | |
| df_dummies = pd.get_dummies(df_model, columns=['neighborhood', 'area_faixa'], drop_first=True) | |
| X = df_dummies.drop('saleprice', axis=1) | |
| y = df_dummies['saleprice'] | |
| X = sm.add_constant(X) | |
| X = X.select_dtypes(include=np.number) | |
| # --- Modelo 1: Regressão Linear Padrão --- | |
| st.subheader("Modelo 1: Regressão Linear Padrão") | |
| modelo = sm.OLS(y, X).fit() | |
| y_pred = modelo.predict(X) | |
| st.write("Métricas de Desempenho do Modelo Padrão:") | |
| r2 = r2_score(y, y_pred) | |
| rmse = np.sqrt(mean_squared_error(y, y_pred)) | |
| mae = mean_absolute_error(y, y_pred) | |
| col1, col2, col3 = st.columns(3) | |
| col1.metric("R² (R-squared)", f"{r2:.4f}") | |
| col2.metric("RMSE", f"${rmse:,.2f}") | |
| col3.metric("MAE", f"${mae:,.2f}") | |
| with st.expander("Ver Resumo Completo e Análise de Pressupostos do Modelo Padrão"): | |
| st.text(modelo.summary()) | |
| st.write("**Análise dos Resíduos**") | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) | |
| # Gráfico de Resíduos vs Ajustados | |
| sns.residplot(x=modelo.fittedvalues, y=modelo.resid, lowess=True, ax=ax1, line_kws={'color': 'red', 'lw': 2}) | |
| ax1.set_title("Resíduos vs. Valores Ajustados") | |
| ax1.set_xlabel("Valores Ajustados") | |
| ax1.set_ylabel("Resíduos") | |
| # Q-Q Plot | |
| sm.qqplot(modelo.resid, line='s', ax=ax2) | |
| ax2.set_title("Q-Q Plot dos Resíduos") | |
| st.pyplot(fig) | |
| st.warning("**Observação:** Note a falta de normalidade dos resíduos (pontos se desviam da linha vermelha no Q-Q Plot) e uma leve heterocedasticidade (o espalhamento dos resíduos não é constante). Isso sugere que uma transformação de variáveis pode melhorar o modelo.") | |
| # --- Modelo 2: Regressão Log-Log (Sugestão Implementada) --- | |
| st.subheader("Modelo 2: Regressão Log-Log (Modelo Aprimorado)") | |
| st.write(""" | |
| Para corrigir os problemas de normalidade e heterocedasticidade, aplicamos uma transformação | |
| logarítmica na variável de preço e nas variáveis contínuas. Os coeficientes deste modelo são | |
| interpretados como **elasticidades** (variações percentuais). | |
| """) | |
| # Preparação dos dados para o modelo log | |
| df_log = df_model.copy() | |
| df_log['log_saleprice'] = np.log(df_log['saleprice']) | |
| df_log['log_grlivarea'] = np.log(df_log['grlivarea']) | |
| df_dummies_log = pd.get_dummies(df_log, columns=['neighborhood', 'area_faixa'], drop_first=True) | |
| X_log = df_dummies_log.drop(['saleprice', 'log_saleprice', 'grlivarea'], axis=1) | |
| y_log = df_dummies_log['log_saleprice'] | |
| X_log = sm.add_constant(X_log) | |
| # Garante que todas as colunas em X_log são numéricas antes de modelar | |
| X_log = X_log.select_dtypes(include=np.number) | |
| modelo_log = sm.OLS(y_log, X_log).fit() | |
| st.write("Métricas de Desempenho do Modelo Log-Log:") | |
| col1, col2, _ = st.columns(3) | |
| col1.metric("R² (R-squared)", f"{modelo_log.rsquared:.4f}", f"{modelo_log.rsquared - r2:+.4f} vs Padrão") | |
| col2.metric("AIC (Critério de Akaike)", f"{modelo_log.aic:,.2f}", f"{modelo_log.aic - modelo.aic:,.2f} vs Padrão", help="Menor é melhor") | |
| with st.expander("Ver Resumo Completo e Análise de Pressupostos do Modelo Log-Log"): | |
| st.text(modelo_log.summary()) | |
| st.write("**Análise dos Resíduos (Modelo Log-Log)**") | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) | |
| # Gráfico de Resíduos vs Ajustados (Log) | |
| sns.residplot(x=modelo_log.fittedvalues, y=modelo_log.resid, lowess=True, ax=ax1, line_kws={'color': 'red', 'lw': 2}) | |
| ax1.set_title("Resíduos vs. Valores Ajustados (Log-Log)") | |
| ax1.set_xlabel("Valores Ajustados (Log)") | |
| ax1.set_ylabel("Resíduos (Log)") | |
| # Q-Q Plot (Log) | |
| sm.qqplot(modelo_log.resid, line='s', ax=ax2) | |
| ax2.set_title("Q-Q Plot dos Resíduos (Log-Log)") | |
| st.pyplot(fig) | |
| st.success("**Observação:** A transformação Log-Log melhorou significativamente a distribuição dos resíduos. Eles agora estão muito mais próximos da normalidade (pontos alinhados no Q-Q plot) e o padrão de funil (heterocedasticidade) foi reduzido.") | |
| # --- Conclusões e Recomendações --- | |
| st.header("🧠 Conclusões e Recomendações para Investidores") | |
| st.write(""" | |
| Com base no modelo Log-Log, que é estatisticamente mais robusto, podemos extrair os seguintes insights para a tomada de decisão: | |
| """) | |
| st.subheader("O que mais impacta no preço de um imóvel?") | |
| coef_qual = modelo_log.params['overallqual'] | |
| coef_area = modelo_log.params['log_grlivarea'] | |
| coef_garagem = modelo_log.params['garagecars'] | |
| st.markdown(f""" | |
| - **Qualidade Geral (`OverallQual`):** Esta é a variável de **maior impacto**. Cada ponto a mais na escala de qualidade (de 1 a 10) está associado a um aumento médio de **`{(np.exp(coef_qual) - 1) * 100:.1f}%`** no preço do imóvel. É um fator multiplicativo poderoso. | |
| - **Área Construída (`GrLivArea`):** O impacto é direto e significativo. Um aumento de **10%** na área construída está associado a um aumento de aproximadamente **`{coef_area * 10:.1f}%`** no preço de venda. | |
| - **Vagas na Garagem (`GarageCars`):** Também é muito relevante. Cada vaga de garagem adicional aumenta o preço do imóvel em cerca de **`{(np.exp(coef_garagem) - 1) * 100:.1f}%`**. | |
| - **Localização (`Neighborhood`):** Embora os coeficientes individuais não estejam detalhados aqui, o modelo confirma que o bairro é um fator crítico para a precificação. | |
| """) | |
| st.subheader("Recomendações Práticas") | |
| st.success(""" | |
| - **Foque em Qualidade:** A recomendação mais forte é priorizar imóveis com **alta qualidade de construção e acabamento**. O retorno sobre este investimento é exponencialmente maior do que outros fatores. | |
| - **Invista em Garagens:** Para imóveis com espaço, adicionar ou ampliar uma garagem é um investimento com retorno claro e significativo. Passar de 1 para 2 vagas tem um impacto enorme no valor. | |
| - **Ampliações são Rentáveis:** Aumentar a área construída tem um impacto positivo e previsível. No entanto, o retorno é percentualmente menor em comparação com melhorias na qualidade. | |
| - **Decida com Confiança:** Use estes fatores — **Qualidade, Garagem, Área e Bairro** — como pilares para justificar preços, avaliar oportunidades de investimento e orientar clientes. | |
| """) |