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