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