vsalgs commited on
Commit
32daecd
·
verified ·
1 Parent(s): 5158316

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +580 -206
src/streamlit_app.py CHANGED
@@ -1,19 +1,24 @@
1
- # streamlit_dashboard_anova.py
 
2
  import streamlit as st
3
  import pandas as pd
 
4
  import statsmodels.api as sm
5
  from statsmodels.formula.api import ols
6
  from scipy.stats import shapiro, levene, kruskal, anderson
 
 
7
  import matplotlib.pyplot as plt
8
  import seaborn as sns
9
- import numpy as np # Adicionado para lidar com potenciais issues numéricas
10
 
 
 
11
 
12
- # --- Funções de Análise (Adaptadas do script anterior) ---
13
 
14
- @st.cache_data # Cache para otimizar o carregamento de dados
15
- def load_data():
16
- """Carrega o Ames Housing Dataset de uma URL e faz uma limpeza básica."""
17
  urls_tentativas = [
18
  "https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv"
19
  ]
@@ -25,13 +30,11 @@ def load_data():
25
  url_carregada = url
26
  break
27
  except Exception:
28
- continue # Tenta a próxima URL
29
 
30
  if df is None:
31
- st.error("Não foi possível carregar o dataset de nenhuma das URLs conhecidas.")
32
  return None, None, [], []
33
 
34
- st.success(f"Dataset carregado com sucesso de: {url_carregada}")
35
  df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower()
36
 
37
  coluna_preco_nome = None
@@ -40,38 +43,107 @@ def load_data():
40
  elif 'sale_price' in df.columns:
41
  df.rename(columns={'sale_price': 'saleprice'}, inplace=True)
42
  coluna_preco_nome = 'saleprice'
43
- # Adicionar mais heurísticas se necessário
44
 
45
  if coluna_preco_nome:
46
  df[coluna_preco_nome] = pd.to_numeric(df[coluna_preco_nome], errors='coerce')
47
  df.dropna(subset=[coluna_preco_nome], inplace=True)
48
 
 
49
  colunas_categoricas_potenciais = df.select_dtypes(include=['object']).columns.tolist()
50
- colunas_numericas_discretas = [col for col in df.select_dtypes(include=np.number).columns
51
- if df[col].nunique() < 20 and col != coluna_preco_nome] # Exemplo de heurística
 
 
52
  colunas_categoricas_potenciais.extend(colunas_numericas_discretas)
53
-
54
- # Remover duplicatas e garantir que a coluna de preço não está na lista
55
  colunas_categoricas_potenciais = sorted(
56
- list(set(col for col in colunas_categoricas_potenciais if col != coluna_preco_nome)))
 
57
 
58
  return df, coluna_preco_nome, colunas_categoricas_potenciais, df.columns.tolist()
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  def perform_anova_for_variable(df_analysis, var_cat, col_preco):
62
- """Executa ANOVA e testes de pressupostos para uma variável."""
63
  results = {"var_cat": var_cat, "plots": {}}
64
-
65
  df_var = df_analysis[[var_cat, col_preco]].copy()
66
 
67
- # Converter para categoria se não for e garantir que tem pelo menos 2 níveis
68
  if df_var[var_cat].dtype != 'object' and not pd.api.types.is_categorical_dtype(df_var[var_cat]):
69
  df_var[var_cat] = df_var[var_cat].astype('category')
70
 
71
- df_var.dropna(inplace=True) # Remove NaNs especificamente para este par
72
-
73
- if df_var[var_cat].nunique() < 2 or len(df_var) < 10: # Mínimo de observações e níveis
74
- results["error"] = "Dados insuficientes ou poucos níveis para análise após limpeza."
75
  return results
76
 
77
  formula = f'{col_preco} ~ C({var_cat})'
@@ -79,39 +151,35 @@ def perform_anova_for_variable(df_analysis, var_cat, col_preco):
79
  modelo = ols(formula, data=df_var).fit()
80
  results["anova_table"] = sm.stats.anova_lm(modelo, typ=2)
81
 
82
- p_valor_anova = None
83
  if f'C({var_cat})' in results["anova_table"].index:
84
- p_valor_anova = results["anova_table"].loc[f'C({var_cat})', 'PR(>F)']
85
- elif not results["anova_table"].empty:
86
- p_valor_anova = results["anova_table"]['PR(>F)'].iloc[0]
87
- results["p_valor_anova"] = p_valor_anova
88
 
89
  residuos = modelo.resid
90
  results["residuos_count"] = len(residuos)
91
 
92
- # 1. Normalidade dos resíduos
93
  normalidade_ok = False
94
  if len(residuos) >= 3:
95
  if len(residuos) <= 5000:
96
  stat_shapiro, p_shapiro = shapiro(residuos)
97
  results["shapiro_test"] = (stat_shapiro, p_shapiro)
98
- if p_shapiro >= 0.05: normalidade_ok = True
 
99
  else:
100
  ad_result = anderson(residuos)
101
  results["anderson_test"] = ad_result
102
- # Verifica se a estatística é menor que o valor crítico para 5%
103
  sig_level_idx = ad_result.significance_level.tolist().index(5.0)
104
  if ad_result.statistic < ad_result.critical_values[sig_level_idx]:
105
  normalidade_ok = True
106
  results["normalidade_ok"] = normalidade_ok
107
 
108
- # Plots de Normalidade
109
  fig_norm, ax_norm = plt.subplots(1, 2, figsize=(10, 4))
110
  if len(residuos) > 1:
111
  sns.histplot(residuos, kde=True, ax=ax_norm[0], stat="density", bins=30)
112
  ax_norm[0].set_title(f'Histograma Resíduos ({var_cat})', fontsize=10)
113
- sm.qqplot(residuos, line='s', ax=ax_norm[1], markerfacecolor="skyblue", markeredgecolor="dodgerblue",
114
- alpha=0.7)
115
  ax_norm[1].set_title(f'Q-Q Plot Resíduos ({var_cat})', fontsize=10)
116
  else:
117
  ax_norm[0].text(0.5, 0.5, "Poucos dados", ha='center', va='center')
@@ -119,17 +187,18 @@ def perform_anova_for_variable(df_analysis, var_cat, col_preco):
119
  plt.tight_layout()
120
  results["plots"]["normalidade"] = fig_norm
121
 
122
- # 2. Homocedasticidade (Teste de Levene)
123
  homocedasticidade_ok = False
124
  grupos = [df_var[col_preco][df_var[var_cat] == categoria].dropna() for categoria in df_var[var_cat].unique()]
125
- grupos_validos = [g for g in grupos if len(g) >= 2] # Levene precisa de grupos com pelo menos 2 obs
126
  if len(grupos_validos) >= 2:
127
  stat_levene, p_levene = levene(*grupos_validos)
128
  results["levene_test"] = (stat_levene, p_levene)
129
- if p_levene >= 0.05: homocedasticidade_ok = True
 
130
  results["homocedasticidade_ok"] = homocedasticidade_ok
131
 
132
- # 3. Kruskal-Wallis (se necessário)
133
  if not normalidade_ok or not homocedasticidade_ok:
134
  if len(grupos_validos) >= 2:
135
  stat_kruskal, p_kruskal = kruskal(*grupos_validos)
@@ -139,11 +208,11 @@ def perform_anova_for_variable(df_analysis, var_cat, col_preco):
139
  fig_box, ax_box = plt.subplots(figsize=(10, 5))
140
  unique_cats = df_var[var_cat].nunique()
141
  order_boxplot = None
142
- if unique_cats > 5 and unique_cats < 50: # Evitar ordenar muitas categorias
143
  try:
144
  order_boxplot = df_var.groupby(var_cat)[col_preco].median().sort_values().index
145
  except Exception:
146
- order_boxplot = df_var[var_cat].unique() # Fallback
147
 
148
  sns.boxplot(x=var_cat, y=col_preco, data=df_var, order=order_boxplot, ax=ax_box, palette="viridis")
149
  ax_box.set_title(f'Distribuição de {col_preco} por {var_cat}', fontsize=12)
@@ -151,185 +220,490 @@ def perform_anova_for_variable(df_analysis, var_cat, col_preco):
151
  plt.setp(ax_box.get_xticklabels(), rotation=45, ha='right', fontsize=8)
152
  else:
153
  plt.setp(ax_box.get_xticklabels(), fontsize=9)
154
-
155
  plt.tight_layout()
156
  results["plots"]["boxplot"] = fig_box
157
 
158
  except Exception as e:
159
  results["error"] = str(e)
160
- return results
161
-
162
-
163
- # --- Interface do Streamlit ---
164
- st.set_page_config(layout="wide", page_title="Dashboard de Análise Imobiliária ANOVA")
165
-
166
- st.title("🏠 Dashboard de Análise Imobiliária com ANOVA")
167
- st.markdown("""
168
- Esta ferramenta interativa permite realizar Análises de Variância (ANOVA) no Ames Housing Dataset
169
- para investigar como diferentes características categóricas impactam o preço de venda dos imóveis.
170
- """)
171
-
172
- # Carregar Dados
173
- df, coluna_preco, colunas_categoricas_selecionaveis, todas_colunas = load_data()
174
-
175
- if df is not None and coluna_preco is not None:
176
- st.header("1. Visão Geral dos Dados")
177
- if st.checkbox("Mostrar amostra dos dados"):
178
- st.dataframe(df.head())
179
- st.write(f"Total de registros carregados (após limpeza inicial na coluna '{coluna_preco}'): {len(df)}")
180
- st.write(f"Coluna alvo (preço): `{coluna_preco}`")
181
-
182
- st.sidebar.header("⚙️ Configurações da Análise")
183
- # Seleção de variáveis
184
- variaveis_selecionadas = st.sidebar.multiselect(
185
- "Escolha 1 a 3 variáveis categóricas para análise ANOVA:",
186
- options=colunas_categoricas_selecionaveis,
187
- max_selections=3
188
- )
189
-
190
- if variaveis_selecionadas:
191
- st.header("2. Resultados da Análise ANOVA")
192
- st.markdown(f"Analisando o impacto de **{', '.join(variaveis_selecionadas)}** sobre **{coluna_preco}**.")
193
 
194
- for var_analisada in variaveis_selecionadas:
195
- st.subheader(f"Análise para: `{var_analisada}`")
196
 
197
- # Prepara dados específicos para a variável (remove NaNs apenas para as colunas envolvidas)
198
- df_analise_var = df[[var_analisada, coluna_preco]].copy()
199
- df_analise_var.dropna(subset=[var_analisada, coluna_preco], inplace=True)
200
 
201
- if df_analise_var.empty or df_analise_var[var_analisada].nunique() < 2:
202
- st.warning(f"Não há dados suficientes ou níveis para '{var_analisada}' após limpeza. Pulando.")
203
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
- resultados_var = perform_anova_for_variable(df_analise_var, var_analisada, coluna_preco)
 
206
 
207
- if "error" in resultados_var:
208
- st.error(f"Erro ao analisar '{var_analisada}': {resultados_var['error']}")
209
- continue
210
 
211
- # Exibir Tabela ANOVA
212
- if "anova_table" in resultados_var:
213
- st.markdown("**Tabela ANOVA:**")
214
- st.dataframe(resultados_var["anova_table"])
215
- p_anova = resultados_var.get("p_valor_anova")
216
- if p_anova is not None:
217
- if p_anova < 0.05:
218
- st.success(
219
- f"✅ ANOVA: Há uma diferença estatisticamente significativa nos preços (p-valor: {p_anova:.4e}).")
220
- else:
221
- st.info(
222
- f"ℹ️ ANOVA: Não há uma diferença estatisticamente significativa nos preços (p-valor: {p_anova:.4e}).")
223
-
224
- # Pressupostos e Testes Alternativos
225
- with st.expander("Verificar Pressupostos da ANOVA e Testes Alternativos"):
226
- st.markdown("**Normalidade dos Resíduos:**")
227
- if "shapiro_test" in resultados_var:
228
- stat, p_val = resultados_var["shapiro_test"]
229
- st.write(f"Shapiro-Wilk: Estatística={stat:.4f}, P-valor={p_val:.4e}")
230
- elif "anderson_test" in resultados_var:
231
- ad_res = resultados_var["anderson_test"]
232
- st.write(f"Anderson-Darling: Estatística={ad_res.statistic:.4f}")
233
- # st.write(f" Valores Críticos: {ad_res.critical_values}")
234
- # st.write(f" Níveis de Significância: {ad_res.significance_level}")
235
-
236
- if resultados_var.get("normalidade_ok"):
237
- st.success("✅ Resíduos parecem ser normalmente distribuídos.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  else:
239
- st.warning("⚠️ Resíduos NÃO parecem ser normalmente distribuídos.")
240
-
241
- if "normalidade" in resultados_var["plots"]:
242
- st.pyplot(resultados_var["plots"]["normalidade"])
243
-
244
- st.markdown("**Homogeneidade das Variâncias (Homocedasticidade):**")
245
- if "levene_test" in resultados_var:
246
- stat_l, p_l = resultados_var["levene_test"]
247
- st.write(f"Teste de Levene: Estatística={stat_l:.4f}, P-valor={p_l:.4e}")
248
- if resultados_var.get("homocedasticidade_ok"):
249
- st.success(" Variâncias parecem ser homogêneas.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  else:
251
- st.warning("⚠️ Variâncias NÃO parecem ser homogêneas.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  else:
253
- st.write("Teste de Levene não pôde ser realizado (dados insuficientes).")
254
-
255
- if "kruskal_test" in resultados_var:
256
- st.markdown("**Teste de Kruskal-Wallis (Alternativa Não Paramétrica):**")
257
- stat_k, p_k = resultados_var["kruskal_test"]
258
- st.write(f"Kruskal-Wallis: Estatística={stat_k:.4f}, P-valor={p_k:.4e}")
259
- if p_k < 0.05:
260
- st.success(f" Kruskal-Wallis: Diferença significativa nas medianas dos preços.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  else:
262
- st.info(f"ℹ️ Kruskal-Wallis: Sem diferença significativa nas medianas dos preços.")
263
-
264
- # Boxplot
265
- if "boxplot" in resultados_var["plots"]:
266
- st.markdown("**Distribuição de Preços por Categoria:**")
267
- st.pyplot(resultados_var["plots"]["boxplot"])
268
-
269
- st.markdown("---") # Separador entre variáveis
270
-
271
- elif not variaveis_selecionadas and st.sidebar.button("Analisar", type="primary",
272
- help="Clique para iniciar após selecionar as variáveis.",
273
- use_container_width=True, disabled=True):
274
- # Botão fica desabilitado até selecionar algo, apenas para feedback visual
275
- pass
276
-
277
- st.sidebar.markdown("---")
278
- st.sidebar.markdown("Desenvolvido como parte de uma análise de dados imobiliários.")
279
-
280
- elif df is None and coluna_preco is None: # Falha no carregamento
281
- st.warning("Aguardando carregamento dos dados ou verifique os erros acima.")
282
- else: # Carregou mas não achou coluna de preço ou não há categóricas
283
- if coluna_preco is None:
284
- st.error(
285
- f"A coluna de preço de venda ('saleprice' ou similar) não foi encontrada no dataset. Verifique as colunas disponíveis: {todas_colunas}")
286
- if not colunas_categoricas_selecionaveis:
287
- st.error("Nenhuma coluna categórica adequada para análise foi identificada.")
288
- # No final do script Streamlit, após o loop de análise das variáveis
289
-
290
- if variaveis_selecionadas: # Somente mostrar se alguma análise foi feita
291
- st.header("3. Insights Gerais e Recomendações")
292
- with st.expander("Ver Análise Detalhada e Recomendações"):
293
- st.markdown("""
294
- ### Como Interpretar os Resultados para Tomada de Decisão:
295
-
296
- A análise ANOVA nos ajuda a entender se uma característica específica da casa (como estilo da casa,
297
- ano da venda, ou estilo do telhado) tem uma associação estatisticamente significativa com o preço
298
- médio de venda.
299
-
300
- **Para as variáveis analisadas (`HouseStyle`, `YrSold`, `RoofStyle`):**
301
-
302
- #### `HouseStyle` (Estilo da Moradia):
303
- * **Impacto Geral:** Geralmente significativo. Estilos diferentes (Térrea, Dois Andares, Níveis Divididos)
304
- atraem diferentes compradores e têm diferentes custos e áreas construídas.
305
- * **Orientação para Corretores:** Utilize o estilo para segmentar o marketing e justificar faixas de preço.
306
- * **Orientação para Investidores:** Analise a popularidade e o potencial de valorização de diferentes estilos
307
- na sua área de interesse.
308
-
309
- #### `YrSold` (Ano da Venda):
310
- * **Impacto Geral:** Pode ser significativo se o mercado passou por mudanças (altas ou baixas) durante
311
- os anos analisados (ex: 2006-2010). Reflete tendências macroeconômicas.
312
- * **Orientação para Corretores:** Fornece contexto histórico para a precificação atual e ajuda a gerenciar
313
- expectativas.
314
- * **Orientação para Investidores:** Sublinha a importância de entender os ciclos de mercado, embora os dados
315
- históricos de `YrSold` não prevejam o futuro diretamente.
316
-
317
- #### `RoofStyle` (Estilo do Telhado):
318
- * **Impacto Geral:** Pode ser significativo. Certos estilos (ex: Quatro Águas vs. Duas Águas) podem estar
319
- associados a diferentes níveis de custo, durabilidade e estética.
320
- * **Orientação para Corretores:** Um detalhe que pode agregar valor, especialmente se o telhado for novo
321
- ou de um estilo particularmente desejável ou durável.
322
- * **Orientação para Investidores:** O custo de manutenção e substituição pode variar com o estilo do telhado.
323
- A condição do telhado é mais crítica que o estilo em si, mas o estilo influencia o custo.
324
-
325
- **Recomendações Gerais:**
326
- * **Corretores:** Usem esses insights para refinar suas estratégias de precificação, marketing e aconselhamento
327
- aos clientes. Uma casa não é apenas um conjunto de quartos, mas um conjunto de características que, juntas,
328
- determinam seu valor.
329
- * **Investidores:** Considerem como essas características (e outras) se alinham com seus objetivos de
330
- investimento, seja para renda, "flipping" ou valorização a longo prazo. Focar em características
331
- que têm um impacto positivo e duradouro no valor é fundamental.
332
-
333
- *Lembre-se que a ANOVA univariada mostra a relação de uma variável por vez com o preço.
334
- Para uma análise mais completa do impacto combinado de múltiplas variáveis, a Regressão Linear (Parte II da sua tarefa original) seria o próximo passo.*
335
- """)
 
1
+ # streamlit_dashboard_unificado.py
2
+
3
  import streamlit as st
4
  import pandas as pd
5
+ import numpy as np
6
  import statsmodels.api as sm
7
  from statsmodels.formula.api import ols
8
  from scipy.stats import shapiro, levene, kruskal, anderson
9
+ from statsmodels.stats.outliers_influence import variance_inflation_factor
10
+ from sklearn.metrics import mean_squared_error, mean_absolute_error
11
  import matplotlib.pyplot as plt
12
  import seaborn as sns
 
13
 
14
+ # Configuração geral do Streamlit
15
+ st.set_page_config(layout="wide", page_title="Dashboard Imobiliário Integrado")
16
 
17
+ # --- Funções de Carregamento de Dados ---
18
 
19
+ @st.cache_data
20
+ def load_data_anova():
21
+ """Carrega o Ames Housing Dataset e retorna para o módulo ANOVA."""
22
  urls_tentativas = [
23
  "https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv"
24
  ]
 
30
  url_carregada = url
31
  break
32
  except Exception:
33
+ continue
34
 
35
  if df is None:
 
36
  return None, None, [], []
37
 
 
38
  df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower()
39
 
40
  coluna_preco_nome = None
 
43
  elif 'sale_price' in df.columns:
44
  df.rename(columns={'sale_price': 'saleprice'}, inplace=True)
45
  coluna_preco_nome = 'saleprice'
 
46
 
47
  if coluna_preco_nome:
48
  df[coluna_preco_nome] = pd.to_numeric(df[coluna_preco_nome], errors='coerce')
49
  df.dropna(subset=[coluna_preco_nome], inplace=True)
50
 
51
+ # Identificar colunas categóricas potenciais (inclui numéricas discretas < 20 níveis)
52
  colunas_categoricas_potenciais = df.select_dtypes(include=['object']).columns.tolist()
53
+ colunas_numericas_discretas = [
54
+ col for col in df.select_dtypes(include=np.number).columns
55
+ if df[col].nunique() < 20 and col != coluna_preco_nome
56
+ ]
57
  colunas_categoricas_potenciais.extend(colunas_numericas_discretas)
 
 
58
  colunas_categoricas_potenciais = sorted(
59
+ list(set(col for col in colunas_categoricas_potenciais if col != coluna_preco_nome))
60
+ )
61
 
62
  return df, coluna_preco_nome, colunas_categoricas_potenciais, df.columns.tolist()
63
 
64
 
65
+ @st.cache_data
66
+ def load_data_reg():
67
+ """Carrega o Ames Housing Dataset e retorna para o módulo de Regressão."""
68
+ fixed_url = "https://raw.githubusercontent.com/Viniciusalgueiro/Ameshousing/refs/heads/main/AmesHousing.csv"
69
+ try:
70
+ df = pd.read_csv(fixed_url)
71
+ url_carregada = fixed_url
72
+ except Exception as e:
73
+ return None, None, [], [], []
74
+
75
+ st.success(f"Dataset carregado com sucesso de: {url_carregada} (Shape: {df.shape})")
76
+ df.columns = df.columns.str.replace('[^A-Za-z0-9_]+', '', regex=True).str.lower()
77
+
78
+ coluna_preco_nome = None
79
+ possible_price_cols = ['saleprice', 'sale_price', 'price']
80
+ for col_candidate in possible_price_cols:
81
+ if col_candidate in df.columns:
82
+ if coluna_preco_nome is None:
83
+ coluna_preco_nome = 'saleprice'
84
+ if col_candidate != 'saleprice':
85
+ df.rename(columns={col_candidate: 'saleprice'}, inplace=True)
86
+ break
87
+
88
+ if coluna_preco_nome is None:
89
+ for col_candidate in df.columns:
90
+ if 'price' in col_candidate and 'sale' in col_candidate:
91
+ coluna_preco_nome = 'saleprice'
92
+ if col_candidate != 'saleprice':
93
+ df.rename(columns={col_candidate: 'saleprice'}, inplace=True)
94
+ st.warning(f"Coluna de preço identificada como '{col_candidate}' e renomeada para 'saleprice'.")
95
+ break
96
+
97
+ if coluna_preco_nome is None:
98
+ return df, None, [], [], df.columns.tolist()
99
+
100
+ df[coluna_preco_nome] = pd.to_numeric(df[coluna_preco_nome], errors='coerce')
101
+ df.dropna(subset=[coluna_preco_nome], inplace=True)
102
+
103
+ # Colunas contínuas e categóricas potenciais
104
+ vars_sempre_continuas_para_reg = [
105
+ 'grlivarea', 'overallqual', 'yearbuilt', 'totalbsmtsf', 'lotarea',
106
+ 'masvnrarea', 'bsmtfinsf1', 'bsmtunfsf', '1stflrsf', '2ndflrsf',
107
+ 'garagearea', 'wooddecksf', 'openporchsf', 'yrsold', 'lotfrontage',
108
+ 'garageyrblt', 'screensf', 'poolarea', 'miscval', 'mosold',
109
+ 'lowqualfinsf', 'bsmthalfbath', 'fullbath', 'halfbath',
110
+ 'bedroomabvgr', 'kitchenabvgr', 'totrmsabvgrd', 'fireplaces', 'garagecars'
111
+ ]
112
+
113
+ colunas_categoricas_potenciais = df.select_dtypes(include=['object', 'category']).columns.tolist()
114
+ colunas_numericas_discretas = [
115
+ col for col in df.select_dtypes(include=np.number).columns
116
+ if df[col].nunique() < 20 and col != coluna_preco_nome and col not in vars_sempre_continuas_para_reg
117
+ ]
118
+ colunas_categoricas_potenciais.extend(colunas_numericas_discretas)
119
+ colunas_categoricas_potenciais = sorted(
120
+ list(set(col for col in colunas_categoricas_potenciais if col in df.columns and col != coluna_preco_nome))
121
+ )
122
+
123
+ colunas_continuas_potenciais = [
124
+ col for col in df.select_dtypes(include=np.number).columns
125
+ if (
126
+ col not in colunas_categoricas_potenciais or col in vars_sempre_continuas_para_reg
127
+ ) and col != coluna_preco_nome
128
+ ]
129
+ colunas_continuas_potenciais = sorted(list(set(col for col in colunas_continuas_potenciais if col in df.columns)))
130
+
131
+ return df, coluna_preco_nome, colunas_categoricas_potenciais, colunas_continuas_potenciais, df.columns.tolist()
132
+
133
+
134
+ # --- Funções de ANOVA ---
135
+
136
  def perform_anova_for_variable(df_analysis, var_cat, col_preco):
137
+ """Executa ANOVA e testes de pressupostos para uma variável categórica."""
138
  results = {"var_cat": var_cat, "plots": {}}
 
139
  df_var = df_analysis[[var_cat, col_preco]].copy()
140
 
 
141
  if df_var[var_cat].dtype != 'object' and not pd.api.types.is_categorical_dtype(df_var[var_cat]):
142
  df_var[var_cat] = df_var[var_cat].astype('category')
143
 
144
+ df_var.dropna(inplace=True)
145
+ if df_var[var_cat].nunique() < 2 or len(df_var) < 10:
146
+ results["error"] = "Dados insuficientes ou poucos níveis após limpeza."
 
147
  return results
148
 
149
  formula = f'{col_preco} ~ C({var_cat})'
 
151
  modelo = ols(formula, data=df_var).fit()
152
  results["anova_table"] = sm.stats.anova_lm(modelo, typ=2)
153
 
 
154
  if f'C({var_cat})' in results["anova_table"].index:
155
+ results["p_valor_anova"] = results["anova_table"].loc[f'C({var_cat})', 'PR(>F)']
156
+ else:
157
+ results["p_valor_anova"] = results["anova_table"]['PR(>F)'].iloc[0]
 
158
 
159
  residuos = modelo.resid
160
  results["residuos_count"] = len(residuos)
161
 
 
162
  normalidade_ok = False
163
  if len(residuos) >= 3:
164
  if len(residuos) <= 5000:
165
  stat_shapiro, p_shapiro = shapiro(residuos)
166
  results["shapiro_test"] = (stat_shapiro, p_shapiro)
167
+ if p_shapiro >= 0.05:
168
+ normalidade_ok = True
169
  else:
170
  ad_result = anderson(residuos)
171
  results["anderson_test"] = ad_result
 
172
  sig_level_idx = ad_result.significance_level.tolist().index(5.0)
173
  if ad_result.statistic < ad_result.critical_values[sig_level_idx]:
174
  normalidade_ok = True
175
  results["normalidade_ok"] = normalidade_ok
176
 
177
+ # Plots de normalidade
178
  fig_norm, ax_norm = plt.subplots(1, 2, figsize=(10, 4))
179
  if len(residuos) > 1:
180
  sns.histplot(residuos, kde=True, ax=ax_norm[0], stat="density", bins=30)
181
  ax_norm[0].set_title(f'Histograma Resíduos ({var_cat})', fontsize=10)
182
+ sm.qqplot(residuos, line='s', ax=ax_norm[1], markerfacecolor="skyblue", markeredgecolor="dodgerblue", alpha=0.7)
 
183
  ax_norm[1].set_title(f'Q-Q Plot Resíduos ({var_cat})', fontsize=10)
184
  else:
185
  ax_norm[0].text(0.5, 0.5, "Poucos dados", ha='center', va='center')
 
187
  plt.tight_layout()
188
  results["plots"]["normalidade"] = fig_norm
189
 
190
+ # Teste de homocedasticidade (Levene)
191
  homocedasticidade_ok = False
192
  grupos = [df_var[col_preco][df_var[var_cat] == categoria].dropna() for categoria in df_var[var_cat].unique()]
193
+ grupos_validos = [g for g in grupos if len(g) >= 2]
194
  if len(grupos_validos) >= 2:
195
  stat_levene, p_levene = levene(*grupos_validos)
196
  results["levene_test"] = (stat_levene, p_levene)
197
+ if p_levene >= 0.05:
198
+ homocedasticidade_ok = True
199
  results["homocedasticidade_ok"] = homocedasticidade_ok
200
 
201
+ # Teste de Kruskal-Wallis (se necessário)
202
  if not normalidade_ok or not homocedasticidade_ok:
203
  if len(grupos_validos) >= 2:
204
  stat_kruskal, p_kruskal = kruskal(*grupos_validos)
 
208
  fig_box, ax_box = plt.subplots(figsize=(10, 5))
209
  unique_cats = df_var[var_cat].nunique()
210
  order_boxplot = None
211
+ if 5 < unique_cats < 50:
212
  try:
213
  order_boxplot = df_var.groupby(var_cat)[col_preco].median().sort_values().index
214
  except Exception:
215
+ order_boxplot = df_var[var_cat].unique()
216
 
217
  sns.boxplot(x=var_cat, y=col_preco, data=df_var, order=order_boxplot, ax=ax_box, palette="viridis")
218
  ax_box.set_title(f'Distribuição de {col_preco} por {var_cat}', fontsize=12)
 
220
  plt.setp(ax_box.get_xticklabels(), rotation=45, ha='right', fontsize=8)
221
  else:
222
  plt.setp(ax_box.get_xticklabels(), fontsize=9)
 
223
  plt.tight_layout()
224
  results["plots"]["boxplot"] = fig_box
225
 
226
  except Exception as e:
227
  results["error"] = str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ return results
 
230
 
 
 
 
231
 
232
+ # --- Função de Regressão Linear ---
233
+
234
+ def run_linear_regression_analysis(df_original, target_column_name, selected_cont_vars, selected_cat_vars):
235
+ """Executa a análise de regressão linear."""
236
+ results_regression = {}
237
+ df_reg = df_original.copy()
238
+ all_selected_vars = selected_cont_vars + selected_cat_vars
239
+
240
+ if not all_selected_vars:
241
+ return {"error": "Nenhuma variável explicativa selecionada."}
242
+
243
+ actual_selected_vars = [var for var in all_selected_vars if var in df_reg.columns]
244
+ missing_vars = [var for var in all_selected_vars if var not in df_reg.columns]
245
+ if missing_vars:
246
+ st.warning(f"Variáveis ignoradas (não encontradas): {missing_vars}")
247
+ selected_cont_vars = [v for v in selected_cont_vars if v in actual_selected_vars]
248
+ selected_cat_vars = [v for v in selected_cat_vars if v in actual_selected_vars]
249
+ all_selected_vars = selected_cont_vars + selected_cat_vars
250
+ if not all_selected_vars:
251
+ return {"error": "Nenhuma variável válida para regressão após filtragem."}
252
+
253
+ df_reg = df_reg[all_selected_vars + [target_column_name]].copy()
254
+ df_reg.dropna(subset=all_selected_vars + [target_column_name], inplace=True)
255
+ if df_reg.empty:
256
+ return {"error": "DataFrame vazio após remoção de NaNs."}
257
+
258
+ if df_reg[target_column_name].min() > 0:
259
+ df_reg['log_saleprice'] = np.log(df_reg[target_column_name])
260
+ else:
261
+ df_reg['log_saleprice'] = np.log1p(df_reg[target_column_name])
262
+ new_target_column = 'log_saleprice'
263
+
264
+ transformed_continuous_vars = []
265
+ for var in selected_cont_vars:
266
+ log_var_name = f'log_{var}'
267
+ if var in df_reg.columns:
268
+ if var in ['overallqual', 'yearbuilt', 'yrsold', 'mosold', 'fireplaces', 'garagecars',
269
+ 'bsmthalfbath', 'fullbath', 'halfbath', 'bedroomabvgr', 'kitchenabvgr', 'totrmsabvgrd']:
270
+ transformed_continuous_vars.append(var)
271
+ elif df_reg[var].min() > 0:
272
+ df_reg[log_var_name] = np.log(df_reg[var])
273
+ transformed_continuous_vars.append(log_var_name)
274
+ else:
275
+ df_reg[log_var_name] = np.log1p(df_reg[var])
276
+ transformed_continuous_vars.append(log_var_name)
277
+
278
+ processed_categorical_vars = []
279
+ for cat_var in selected_cat_vars:
280
+ if cat_var in df_reg.columns:
281
+ if df_reg[cat_var].dtype not in ['object', 'category']:
282
+ df_reg[cat_var] = df_reg[cat_var].astype('category')
283
+ processed_categorical_vars.append(cat_var)
284
+
285
+ df_reg = pd.get_dummies(df_reg, columns=processed_categorical_vars, drop_first=True, dtype=float)
286
+
287
+ final_explanatory_vars = [var for var in transformed_continuous_vars if var in df_reg.columns]
288
+ for cat_orig in processed_categorical_vars:
289
+ dummy_cols = [col for col in df_reg.columns if col.startswith(f"{cat_orig}_")]
290
+ final_explanatory_vars.extend(dummy_cols)
291
+
292
+ final_explanatory_vars = sorted(set(final_explanatory_vars))
293
+ final_explanatory_vars = [
294
+ var for var in final_explanatory_vars
295
+ if var in df_reg.columns and df_reg[var].isnull().sum() < len(df_reg) and df_reg[var].std(skipna=True) > 0
296
+ ]
297
 
298
+ if not final_explanatory_vars:
299
+ return {"error": "Nenhuma variável explicativa válida após pré-processamento."}
300
 
301
+ X = df_reg[final_explanatory_vars]
302
+ y = df_reg[new_target_column]
303
+ X = sm.add_constant(X, has_constant='add')
304
 
305
+ try:
306
+ model = sm.OLS(y, X).fit()
307
+ results_regression['model_summary_obj'] = model.summary()
308
+ results_regression['model_object'] = model
309
+ except Exception as e:
310
+ return {"error": f"Erro ao ajustar modelo: {str(e)}. Variáveis em X: {X.columns.tolist()}"}
311
+
312
+ fitted_values = model.fittedvalues
313
+ r_squared = model.rsquared
314
+ adj_r_squared = model.rsquared_adj
315
+ rmse_log = np.sqrt(mean_squared_error(y, fitted_values))
316
+ mae_log = mean_absolute_error(y, fitted_values)
317
+ results_regression['performance_metrics'] = {
318
+ 'R-squared': r_squared,
319
+ 'Adjusted R-squared': adj_r_squared,
320
+ 'RMSE (log)': rmse_log,
321
+ 'MAE (log)': mae_log
322
+ }
323
+
324
+ # Interpretação de coeficientes
325
+ coeff_notes = """
326
+ **Interpretação dos Coeficientes (`log_saleprice`):**
327
+ - Variáveis contínuas transformadas em log (ex: `log_grlivarea`):
328
+ Aumento de 1% na variável resulta em ~[coef * 1]% de variação no `saleprice`.
329
+ - Variáveis contínuas não transformadas (ex: `overallqual`):
330
+ Aumento unitário resulta em ~[(exp(coef)-1)*100]% de variação no `saleprice`.
331
+ - Dummies (ex: `neighborhood_stonebr`):
332
+ Presença da categoria resulta em ~[(exp(coef)-1)*100]% de variação no `saleprice`.
333
+ **Significância:** Verifique `P>|t|` < 0.05.
334
+ """
335
+ results_regression['coefficients_interpretation_notes'] = coeff_notes
336
+
337
+ # Recomendações práticas
338
+ recommendations_data = []
339
+ if hasattr(model, 'params') and hasattr(model, 'pvalues'):
340
+ params_df = pd.DataFrame({'Coeficiente': model.params, 'P-valor': model.pvalues})
341
+ significant_params = params_df[params_df['P-valor'] < 0.05]
342
+ if 'const' in significant_params.index:
343
+ significant_params = significant_params.drop('const')
344
+
345
+ if not significant_params.empty:
346
+ for var, row in significant_params.iterrows():
347
+ coef = row['Coeficiente']
348
+ is_log = var.startswith("log_") and var in transformed_continuous_vars
349
+ original_var = var.replace("log_", "") if is_log else var
350
+ display_name = original_var.replace('_', ' ').title()
351
+
352
+ # Dummies
353
+ is_dummy = False
354
+ for cat_orig in selected_cat_vars:
355
+ if var.startswith(f"{cat_orig}_"):
356
+ is_dummy = True
357
+ parts = var.split('_', 1)
358
+ cat_display = parts[1].replace('_', ' ').title() if len(parts) > 1 else "Categoria"
359
+ display_name = f"{cat_orig.replace('_', ' ').title()}: {cat_display}"
360
+ break
361
+
362
+ if is_log:
363
+ tipo = "Aumento Percentual (elasticidade)"
364
+ magnitude = f"{coef:.2f}% de variação no preço para 1% de aumento"
365
+ interpret = f"1% em '{original_var.title()}' causa {coef:.2f}% no preço."
366
  else:
367
+ percentage_change = (np.exp(coef) - 1) * 100
368
+ tipo = "Aumento Percentual (nível ou dummy)"
369
+ magnitude = f"{percentage_change:.2f}% de variação no preço"
370
+ if is_dummy:
371
+ interpret = f"Presença em '{display_name}' causa {percentage_change:.2f}% no preço."
372
+ else:
373
+ interpret = f"Aumento unitário em '{display_name}' causa {percentage_change:.2f}% no preço."
374
+
375
+ recommendations_data.append({
376
+ "Variável": display_name,
377
+ "Tipo de Impacto": tipo,
378
+ "Interpretação": interpret,
379
+ "Magnitude Estimada": magnitude
380
+ })
381
+ else:
382
+ recommendations_data.append({
383
+ "Variável": "N/A",
384
+ "Tipo de Impacto": "-",
385
+ "Interpretação": "Nenhum coeficiente significativo.",
386
+ "Magnitude Estimada": "-"
387
+ })
388
+ else:
389
+ recommendations_data.append({
390
+ "Variável": "N/A",
391
+ "Tipo de Impacto": "-",
392
+ "Interpretação": "Modelo não ajustado.",
393
+ "Magnitude Estimada": "-"
394
+ })
395
+
396
+ results_regression['practical_recommendations_table_data'] = recommendations_data
397
+ return results_regression
398
+
399
+
400
+ # --- Lógica de Navegação via Botões ---
401
+
402
+ # Inicializa o estado de página (ANOVA ou REGRESSAO)
403
+ if 'page' not in st.session_state:
404
+ st.session_state.page = 'ANOVA'
405
+
406
+ # Botões de navegação
407
+ col1, col2 = st.columns([1, 1])
408
+ with col1:
409
+ if st.button('📊 ANOVA'):
410
+ st.session_state.page = 'ANOVA'
411
+ with col2:
412
+ if st.button('📈 Regressão'):
413
+ st.session_state.page = 'REGRESSAO'
414
+
415
+ st.markdown("---")
416
+
417
+ # --- Página ANOVA ---
418
+ if st.session_state.page == 'ANOVA':
419
+ st.title("🏠 Dashboard de ANOVA Imobiliária")
420
+ st.markdown("""
421
+ Esta seção permite realizar Análises de Variância (ANOVA) no Ames Housing Dataset,
422
+ investigando como diferentes variáveis categóricas impactam o preço de venda dos imóveis.
423
+ """)
424
+
425
+ df_anova, coluna_preco_anova, colunas_categoricas_selecionaveis, todas_colunas_anova = load_data_anova()
426
+
427
+ if df_anova is not None and coluna_preco_anova is not None:
428
+ st.header("1. Visão Geral dos Dados (ANOVA)")
429
+ if st.checkbox("Mostrar amostra dos dados"):
430
+ st.dataframe(df_anova.head())
431
+ st.write(f"Total de registros carregados: {len(df_anova)}")
432
+ st.write(f"Coluna alvo (preço): `{coluna_preco_anova}`")
433
+
434
+ st.sidebar.header("⚙️ Configurações ANOVA")
435
+ variaveis_selecionadas = st.sidebar.multiselect(
436
+ "Escolha 1 a 3 variáveis categóricas para ANOVA:",
437
+ options=colunas_categoricas_selecionaveis,
438
+ max_selections=3
439
+ )
440
+
441
+ if variaveis_selecionadas:
442
+ st.header("2. Resultados da Análise ANOVA")
443
+ st.markdown(f"Analisando **{', '.join(variaveis_selecionadas)}** sobre **{coluna_preco_anova}**.")
444
+
445
+ for var_analisada in variaveis_selecionadas:
446
+ st.subheader(f"Análise para: `{var_analisada}`")
447
+ df_analise_var = df_anova[[var_analisada, coluna_preco_anova]].copy()
448
+ df_analise_var.dropna(subset=[var_analisada, coluna_preco_anova], inplace=True)
449
+
450
+ if df_analise_var.empty or df_analise_var[var_analisada].nunique() < 2:
451
+ st.warning(f"Dados insuficientes ou poucos níveis para '{var_analisada}'. Pulando.")
452
+ continue
453
+
454
+ resultados_var = perform_anova_for_variable(df_analise_var, var_analisada, coluna_preco_anova)
455
+ if "error" in resultados_var:
456
+ st.error(f"Erro ao analisar '{var_analisada}': {resultados_var['error']}")
457
+ continue
458
+
459
+ # Tabela ANOVA
460
+ if "anova_table" in resultados_var:
461
+ st.markdown("**Tabela ANOVA:**")
462
+ st.dataframe(resultados_var["anova_table"])
463
+ p_anova = resultados_var.get("p_valor_anova")
464
+ if p_anova is not None:
465
+ if p_anova < 0.05:
466
+ st.success(f"✅ Diferença significativa (p-valor: {p_anova:.4e}).")
467
+ else:
468
+ st.info(f"ℹ️ Sem diferença significativa (p-valor: {p_anova:.4e}).")
469
+
470
+ # Pressupostos e Testes Alternativos
471
+ with st.expander("Verificar Pressupostos e Testes Alternativos"):
472
+ st.markdown("**Normalidade dos Resíduos:**")
473
+ if "shapiro_test" in resultados_var:
474
+ stat, p_val = resultados_var["shapiro_test"]
475
+ st.write(f"Shapiro-Wilk: Estatística={stat:.4f}, P-valor={p_val:.4e}")
476
+ elif "anderson_test" in resultados_var:
477
+ ad_res = resultados_var["anderson_test"]
478
+ st.write(f"Anderson-Darling: Estatística={ad_res.statistic:.4f}")
479
+
480
+ if resultados_var.get("normalidade_ok"):
481
+ st.success("✅ Resíduos parecem normalmente distribuídos.")
482
+ else:
483
+ st.warning("⚠️ Resíduos NÃO parecem normalmente distribuídos.")
484
+
485
+ if "normalidade" in resultados_var["plots"]:
486
+ st.pyplot(resultados_var["plots"]["normalidade"])
487
+
488
+ st.markdown("**Homogeneidade das Variâncias (Levene):**")
489
+ if "levene_test" in resultados_var:
490
+ stat_l, p_l = resultados_var["levene_test"]
491
+ st.write(f"Levene: Estatística={stat_l:.4f}, P-valor={p_l:.4e}")
492
+ if resultados_var.get("homocedasticidade_ok"):
493
+ st.success("✅ Variâncias homogêneas.")
494
+ else:
495
+ st.warning("⚠️ Variâncias NÃO homogêneas.")
496
+ else:
497
+ st.write("Levene não pôde ser realizado (insuficiente).")
498
+
499
+ if "kruskal_test" in resultados_var:
500
+ st.markdown("**Kruskal-Wallis (Não Paramétrico):**")
501
+ stat_k, p_k = resultados_var["kruskal_test"]
502
+ st.write(f"Kruskal-Wallis: Estatística={stat_k:.4f}, P-valor={p_k:.4e}")
503
+ if p_k < 0.05:
504
+ st.success("✅ Diferença significativa nas medianas.")
505
+ else:
506
+ st.info("ℹ️ Sem diferença significativa nas medianas.")
507
+
508
+ # Boxplot
509
+ if "boxplot" in resultados_var["plots"]:
510
+ st.markdown("**Distribuição de Preços por Categoria:**")
511
+ st.pyplot(resultados_var["plots"]["boxplot"])
512
+
513
+ st.markdown("---")
514
+
515
+ elif not variaveis_selecionadas:
516
+ st.sidebar.warning("Selecione ao menos uma variável para executar a ANOVA.")
517
+
518
+ st.sidebar.markdown("---")
519
+ st.sidebar.markdown("Desenvolvido para análise imobiliária.")
520
+
521
+ if variaveis_selecionadas:
522
+ st.header("3. Insights Gerais e Recomendações ANOVA")
523
+ with st.expander("Ver Recomendações"):
524
+ st.markdown("""
525
+ ### Como interpretar:
526
+ - ANOVA ajuda a entender se variáveis categóricas (ex: estilo da casa, ano, telhado)
527
+ têm associação significativa com o preço.
528
+ - **Variáveis com p-valor < 0.05**: sugerem diferença estatisticamente significativa.
529
+ - Use essas informações para ajustar estratégia de precificação e marketing.
530
+ - Para análise multivariada, prossiga para Regressão Linear Múltipla.
531
+ """)
532
+
533
+ else:
534
+ if df_anova is None:
535
+ st.warning("Aguardando carregamento dos dados ou verifique as URLs.")
536
+ elif coluna_preco_anova is None:
537
+ st.error(f"Coluna de preço não encontrada. Verifique as colunas: {todas_colunas_anova}")
538
+
539
+
540
+ # --- Página REGRESSÃO ---
541
+ elif st.session_state.page == 'REGRESSAO':
542
+ st.title("🏠 Dashboard de Regressão Imobiliária")
543
+ st.markdown("""
544
+ Esta seção permite realizar Modelagem Preditiva com Regressão Linear Múltipla
545
+ no Ames Housing Dataset para entender o impacto de variáveis contínuas e categóricas no preço.
546
+ """)
547
+
548
+ df_reg, coluna_preco_reg, colunas_categoricas_reg, colunas_continuas_reg, todas_colunas_reg = load_data_reg()
549
+
550
+ if df_reg is not None and coluna_preco_reg is not None:
551
+ st.header("1. Visão Geral dos Dados (Regressão)")
552
+ if st.checkbox("Mostrar amostra aleatória dos dados"):
553
+ st.dataframe(df_reg.sample(min(5, len(df_reg))))
554
+ st.write(f"Total registros: {len(df_reg)}")
555
+ st.write(f"Coluna alvo (preço): `{coluna_preco_reg}`")
556
+
557
+ # Configurações no sidebar
558
+ st.sidebar.title("⚙️ Configurações Regressão")
559
+ st.sidebar.header("Seleção de Variáveis")
560
+ st.sidebar.markdown("Escolha 4 a 6 variáveis (≥1 contínua e ≥1 categórica).")
561
+
562
+ default_cont = ['grlivarea', 'overallqual', 'yearbuilt', 'totalbsmtsf']
563
+ valid_default_cont = [v for v in default_cont if v in colunas_continuas_reg]
564
+ if not valid_default_cont and colunas_continuas_reg:
565
+ valid_default_cont = colunas_continuas_reg[:1]
566
+
567
+ reg_continuous_vars = st.sidebar.multiselect(
568
+ "Variáveis Contínuas:",
569
+ options=colunas_continuas_reg,
570
+ default=valid_default_cont,
571
+ key="reg_cont_vars"
572
+ )
573
+
574
+ default_cat = ['neighborhood', 'housestyle']
575
+ valid_default_cat = [v for v in default_cat if v in colunas_categoricas_reg]
576
+ if not valid_default_cat and colunas_categoricas_reg:
577
+ valid_default_cat = colunas_categoricas_reg[:1]
578
+
579
+ reg_categorical_vars = st.sidebar.multiselect(
580
+ "Variáveis Categóricas:",
581
+ options=colunas_categoricas_reg,
582
+ default=valid_default_cat,
583
+ key="reg_cat_vars"
584
+ )
585
+
586
+ total_vars = len(reg_continuous_vars) + len(reg_categorical_vars)
587
+ valid_selection = True
588
+ if not (4 <= total_vars <= 6):
589
+ st.sidebar.warning(f"Selecione entre 4 e 6 variáveis (total atual: {total_vars}).")
590
+ valid_selection = False
591
+ if not reg_continuous_vars:
592
+ st.sidebar.warning("Selecione ao menos 1 variável contínua.")
593
+ valid_selection = False
594
+ if not reg_categorical_vars:
595
+ st.sidebar.warning("Selecione ao menos 1 variável categórica.")
596
+ valid_selection = False
597
+
598
+ st.markdown("---")
599
+ st.header("2. Análise Exploratória das Variáveis Selecionadas")
600
+
601
+ if reg_continuous_vars or reg_categorical_vars:
602
+ if st.checkbox("Mostrar Distribuições das Variáveis Selecionadas", value=False):
603
+ st.markdown("##### Distribuições das Variáveis Contínuas")
604
+ for var_cont in reg_continuous_vars:
605
+ if var_cont in df_reg.columns:
606
+ fig, ax = plt.subplots(figsize=(6, 3))
607
+ sns.histplot(df_reg[var_cont], kde=True, ax=ax, bins=30)
608
+ ax.set_title(f"Distribuição de {var_cont}")
609
+ st.pyplot(fig)
610
  else:
611
+ st.warning(f"'{var_cont}' não encontrada para plotar.")
612
+
613
+ st.markdown("##### Contagem das Categorias das Variáveis Categóricas")
614
+ for var_cat in reg_categorical_vars:
615
+ if var_cat in df_reg.columns:
616
+ fig, ax = plt.subplots(figsize=(7, 4))
617
+ if df_reg[var_cat].nunique() > 5:
618
+ sns.countplot(y=df_reg[var_cat], ax=ax,
619
+ order=df_reg[var_cat].value_counts().index, palette="viridis")
620
+ else:
621
+ sns.countplot(x=df_reg[var_cat], ax=ax,
622
+ order=df_reg[var_cat].value_counts().index, palette="viridis")
623
+ plt.xticks(rotation=45, ha="right")
624
+ ax.set_title(f"Contagem de {var_cat}")
625
+ plt.tight_layout()
626
+ st.pyplot(fig)
627
+ else:
628
+ st.warning(f"'{var_cat}' não encontrada para plotar contagem.")
629
+
630
+ if st.checkbox("Mostrar Mapa de Correlação das Contínuas + Preço", value=False):
631
+ st.markdown("##### Mapa de Correlação")
632
+ vars_corr = [var for var in reg_continuous_vars if var in df_reg.columns] + [coluna_preco_reg]
633
+ if len(vars_corr) > 1:
634
+ corr_matrix = df_reg[vars_corr].corr()
635
+ fig_corr, ax_corr = plt.subplots(
636
+ figsize=(min(10, len(vars_corr) * 1.5), min(8, len(vars_corr) * 1.2))
637
+ )
638
+ sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", fmt=".2f", linewidths=.5, ax=ax_corr)
639
+ ax_corr.set_title("Mapa de Correlação")
640
+ st.pyplot(fig_corr)
641
  else:
642
+ st.info("Selecione ao menos duas variáveis numéricas para o mapa de correlação.")
643
+ st.markdown("---")
644
+
645
+ if st.button("Executar Regressão Linear", disabled=not valid_selection):
646
+ with st.spinner("Executando regressão..."):
647
+ output_reg = run_linear_regression_analysis(df_reg, coluna_preco_reg,
648
+ reg_continuous_vars, reg_categorical_vars)
649
+ if "error" in output_reg:
650
+ st.error(output_reg["error"])
651
+ else:
652
+ st.subheader("Resultados do Modelo de Regressão")
653
+
654
+ model_summary_obj = output_reg.get('model_summary_obj')
655
+ if model_summary_obj:
656
+ st.markdown("##### Sumário Geral do Modelo:")
657
+ sum_table0 = pd.read_html(model_summary_obj.tables[0].as_html(), header=None, index_col=None)[0]
658
+ st.table(sum_table0.iloc[:, :2].rename(columns={0: "Métrica", 1: "Valor"}))
659
+ st.table(sum_table0.iloc[:, 2:].rename(columns={2: "Métrica", 3: "Valor"}))
660
+
661
+ st.markdown("##### Coeficientes do Modelo:")
662
+ sum_table1 = pd.read_html(model_summary_obj.tables[1].as_html(), header=0, index_col=0)[0]
663
+ st.dataframe(sum_table1.style.format({
664
+ "coef": "{:.4f}", "std err": "{:.4f}", "t": "{:.3f}", "P>|t|": "{:.3e}",
665
+ "[0.025": "{:.4f}", "0.975]": "{:.4f}"
666
+ }))
667
+
668
+ if len(model_summary_obj.tables) > 2:
669
+ st.markdown("##### Outras Estatísticas e Notas:")
670
+ notes_html = model_summary_obj.tables[2].as_html()
671
+ notes_df = pd.read_html(notes_html, header=None, index_col=None)[0]
672
+ for i in range(len(notes_df)):
673
+ line = notes_df.iloc[i].tolist()
674
+ st.text(" ".join([str(x) for x in line if pd.notna(x)]))
675
+
676
+ st.subheader("Métricas de Desempenho")
677
+ if 'performance_metrics' in output_reg:
678
+ metrics_df = pd.DataFrame.from_dict(output_reg['performance_metrics'], orient='index', columns=['Valor'])
679
+ st.table(metrics_df.style.format("{:.4f}"))
680
+ st.markdown("""
681
+ * **R-squared / R-squared Ajustado:** Variância explicada pelo modelo.
682
+ * **RMSE (log) / MAE (log):** Erros médios na escala logarítmica.
683
+ """)
684
+
685
+ st.subheader("Interpretação dos Coeficientes")
686
+ if 'coefficients_interpretation_notes' in output_reg:
687
+ st.markdown(output_reg['coefficients_interpretation_notes'])
688
+
689
+ st.subheader("Recomendações Práticas")
690
+ if 'practical_recommendations_table_data' in output_reg:
691
+ recom_df = pd.DataFrame(output_reg['practical_recommendations_table_data'])
692
+ if not recom_df.empty:
693
+ st.dataframe(recom_df)
694
  else:
695
+ st.info("Nenhuma recomendação gerada (verifique significância).")
696
+
697
+ st.sidebar.markdown("---")
698
+ st.sidebar.info("Dashboard de Regressão Imobiliária")
699
+
700
+ else:
701
+ if df_reg is None:
702
+ st.error("Falha ao carregar dados. Verifique a conexão ou a URL.")
703
+ elif coluna_preco_reg is None:
704
+ st.error(f"Coluna de preço não identificada. Colunas disponíveis: {todas_colunas_reg}")
705
+ else:
706
+ if not colunas_categoricas_reg and not colunas_continuas_reg:
707
+ st.error("Nenhuma coluna adequada identificada para regressão.")
708
+
709
+