Spaces:
Sleeping
Sleeping
| # 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" | |
| ) | |
| 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.") | |