ricardoadriano commited on
Commit
8804675
·
verified ·
1 Parent(s): 4998978

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +290 -365
src/streamlit_app.py CHANGED
@@ -2,7 +2,7 @@
2
  # coding: utf-8
3
 
4
  # =====================================================
5
- # Dashboard - Testes de Hipóteses com AmesHousing (Tarefa 4)
6
  # =====================================================
7
 
8
  import streamlit as st
@@ -12,7 +12,6 @@ import seaborn as sns
12
  import plotly.express as px
13
  import numpy as np
14
 
15
- from scipy import stats
16
  from scipy.stats import shapiro, levene, kruskal
17
  from statsmodels.formula.api import ols
18
  import statsmodels.api as sm
@@ -25,389 +24,315 @@ from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
25
  # Configuração da Página
26
  # -----------------------------------------------------
27
  st.set_page_config(
28
- page_title="Dashboard - Testes de Hipóteses com AmesHousing",
29
  layout="wide",
30
  initial_sidebar_state="expanded"
31
  )
32
 
33
- st.markdown("<h1 style='text-align:center;color:#003366;'>Simulador de Testes de Hipótese</h1>", unsafe_allow_html=True)
34
- st.markdown("<h3 style='text-align:center;color:#003366;'>Análise do Dataset AmesHousing</h3>", unsafe_allow_html=True)
35
  st.markdown("---")
36
 
37
  # -----------------------------------------------------
38
- # Abas do Dashboard
39
  # -----------------------------------------------------
40
- tabs = st.tabs(["Simulações Teóricas", "Análise AmesHousing"]) # mantém as abas existentes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- # -----------------------------------------------------
43
- # Aba 1: Simulações Teóricas (inalterada)
44
- # -----------------------------------------------------
45
- with tabs[0]:
46
- #st.subheader("")
47
-
48
- st.sidebar.markdown("### Parâmetros do Teste (Proporção)")
49
- p_pop = st.sidebar.slider("Proporção populacional (H0)", 0.0, 1.0, 0.1, 0.01, key="p_pop")
50
- p_sample = st.sidebar.slider("Proporção amostral", 0.0, 1.0, 0.12, 0.01, key="p_sample")
51
- n = st.sidebar.slider("Tamanho da amostra", 100, 10000, 1000, 10, key="n_sample")
52
- alpha_prop = st.sidebar.slider("Nível de significância (α)", 0.01, 0.10, 0.05, 0.01, key="alpha_prop")
53
-
54
- se = np.sqrt(p_pop*(1-p_pop)/n)
55
- z = (p_sample - p_pop)/se
56
- p_value = 2*(1 - stats.norm.cdf(abs(z)))
57
-
58
- st.write(f"**Z** = {z:.4f}")
59
- st.write(f"**p-valor** = {p_value:.4f}")
60
- if p_value < alpha_prop:
61
- st.write("**Rejeitamos H0**: diferença significativa.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  else:
63
- st.write("**Não rejeitamos H0**: sem diferença significativa.")
 
 
 
 
 
 
 
 
64
 
65
- # -----------------------------------------------------
66
- # Aba 2: Análise AmesHousing + (NOVO) Regressão para Tarefa 4
67
- # -----------------------------------------------------
68
- with tabs[1]:
69
- st.subheader("Análise de Variância - AmesHousing Dataset")
70
- st.markdown("---")
71
-
72
- # Leitura do CSV ajustado para usar o arquivo enviado com o app
73
- @st.cache_data
74
- def carregar_dados():
75
- paths_tentativa = [
76
- "AmesHousing.csv", # mesma pasta do app
77
- "/mnt/data/AmesHousing.csv", # caminho do ambiente atual
78
- "../Dados/AmesHousing.csv", # caminho original do código
79
- ]
80
- last_err = None
81
- for p in paths_tentativa:
82
- try:
83
- df = pd.read_csv(p)
84
- return df
85
- except Exception as e:
86
- last_err = e
87
- continue
88
- raise RuntimeError(f"Não foi possível carregar o AmesHousing.csv. Último erro: {last_err}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
- casa_data = carregar_dados()
91
- casa_data.columns = casa_data.columns.str.strip().str.replace(" ", "_")
92
 
93
- # -----------------------------
94
- # Amostragem (mantém comportamento anterior)
95
- # -----------------------------
96
- n_amostra = st.session_state.get("n_sample", len(casa_data))
97
- if n_amostra < len(casa_data):
98
- dados = casa_data.sample(n=n_amostra, random_state=42)
99
- else:
100
- dados = casa_data.copy()
101
-
102
- # -----------------------------
103
- # Filtro interativo no sidebar
104
- # -----------------------------
105
- st.sidebar.markdown("### Filtros AmesHousing")
106
-
107
- bairros = st.sidebar.multiselect(
108
- "Selecione bairros",
109
- options=sorted(dados["Neighborhood"].dropna().unique()),
110
- default=None
111
- )
112
-
113
- # Aplicar filtro
114
- dados_filtrados = dados.copy()
115
- if bairros:
116
- dados_filtrados = dados_filtrados[dados_filtrados["Neighborhood"].isin(bairros)]
117
-
118
- # -------------------------------------------------
119
- # Análise Exploratória (existente)
120
- # -------------------------------------------------
121
- st.markdown("### Distribuição do Preço de Venda")
122
- if not dados_filtrados.empty:
123
- fig, ax = plt.subplots(figsize=(8,5))
124
- sns.histplot(dados_filtrados['SalePrice'], kde=True, ax=ax)
125
- ax.set_title("Distribuição do Preço de Venda")
126
- st.pyplot(fig)
127
- else:
128
- st.warning("Nenhum dado disponível com os filtros aplicados.")
129
-
130
- # Boxplots (existente)
131
- st.markdown("### Boxplots das Variáveis Selecionadas")
132
- variavel = st.selectbox(
133
- "Escolha a variável categórica para comparar preços:",
134
- ["Neighborhood","Garage_Type","Fireplaces"]
135
- )
136
-
137
- if not dados_filtrados.empty:
138
- if len(dados_filtrados[variavel].dropna().unique()) > 1:
139
- fig2, ax2 = plt.subplots(figsize=(12,6))
140
- sns.boxplot(x=variavel, y="SalePrice", data=dados_filtrados, ax=ax2)
141
- plt.xticks(rotation=90)
142
- ax2.set_title(f"Preço de Venda por {variavel}")
143
- st.pyplot(fig2)
144
- else:
145
- st.warning(f"Não é possível gerar boxplot: apenas uma categoria em {variavel} após os filtros.")
146
-
147
- # Scatter interativo (média de preço por bairro) — existente
148
- st.markdown("### Preço Médio de Venda por Bairro")
149
- if not dados_filtrados.empty:
150
- bairro_grouped = dados_filtrados.groupby('Neighborhood').agg(
151
- count=('SalePrice','size'),
152
- mean_price=('SalePrice','mean')
153
- ).reset_index()
154
-
155
- bairro_filtered = bairro_grouped[bairro_grouped['count'] >= 5]
156
- if not bairro_filtered.empty:
157
- fig3 = px.scatter(
158
- bairro_filtered,
159
- x='mean_price',
160
- y='Neighborhood',
161
- size='count',
162
- color='Neighborhood',
163
- title='Preço Médio de Venda vs Bairro (Ames, Iowa)',
164
- labels={'mean_price': 'Preço Médio de Venda', 'Neighborhood':'Bairro'},
165
- opacity=0.8
166
- )
167
- st.plotly_chart(fig3, use_container_width=True)
168
- else:
169
- st.warning("Não há bairros suficientes após filtros para gerar o gráfico.")
170
-
171
- # -------------------------------------------------
172
- # ANOVA — existente
173
- # -------------------------------------------------
174
- st.markdown("### ANOVA para Neighborhood, Garage_Type e Fireplaces")
175
- alpha = st.sidebar.slider(
176
- "Nível de significância (α) - ANOVA AmesHousing",
177
- 0.01,0.10,0.05,0.01,
178
- key="alpha_ames"
179
- )
180
-
181
- if not dados_filtrados.empty:
182
- for nome in ["Neighborhood", "Garage_Type", "Fireplaces"]:
183
- categorias = dados_filtrados[nome].dropna().unique()
184
- if len(categorias) < 2:
185
- st.warning(f"ANOVA não pôde ser realizada para **{nome}** (menos de 2 grupos após os filtros).")
186
- continue
187
 
188
- modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
189
- st.markdown(f"#### ANOVA - {nome}")
190
- anova = sm.stats.anova_lm(modelo, typ=2)
191
- st.dataframe(anova)
192
-
193
- # -------------------------------------------------
194
- # Validação dos Pressupostos — existente
195
- # -------------------------------------------------
196
- st.markdown("### Validação dos Pressupostos da ANOVA")
197
-
198
- st.markdown("#### Teste de Normalidade (Shapiro-Wilk)")
199
- for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
200
- categorias = dados_filtrados[nome].dropna().unique()
201
- if len(categorias) < 2:
202
- st.warning(f"Shapiro-Wilk não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
203
- continue
204
 
205
- modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
206
- residuos = modelo.resid
207
- stat, p = shapiro(residuos.dropna())
208
- st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
209
- + ("resíduos normais" if p >= alpha else "violação de normalidade"))
210
-
211
- st.markdown("#### Teste de Homocedasticidade (Levene)")
212
- for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
213
- grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
214
- if len(grupos) < 2:
215
- st.warning(f"Levene não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
216
- continue
217
 
218
- stat, p = levene(*grupos)
219
- st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
220
- + ("variâncias iguais" if p >= alpha else "variâncias diferentes"))
221
-
222
- # -------------------------------------------------
223
- # Kruskal-Wallis — existente
224
- # -------------------------------------------------
225
- st.markdown("### Teste não-paramétrico (Kruskal-Wallis)")
226
- for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
227
- grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
228
- if len(grupos) < 2:
229
- st.warning(f"Kruskal-Wallis não pôde ser realizado para **{nome}** (menos de 2 grupos após os filtros).")
230
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- stat, p = kruskal(*grupos)
233
- st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
234
- + ("diferenças significativas" if p < alpha else "sem diferença significativa"))
235
-
236
- # =================================================
237
- # (NOVO) Regressão Linear — atende à Tarefa 4 (itens a–d)
238
- # =================================================
239
- st.markdown("---")
240
- st.subheader("Regressão Linear — Tarefa 4 (PPCA/UnB)")
241
-
242
- st.markdown("Selecione variáveis para modelagem do **SalePrice** (alvo).")
243
-
244
- # Sugestões de variáveis comumente utilizadas
245
- candidatos_numericas = [
246
- c for c in [
247
- 'Bedroom_AbvGr','Full_Bath','Half_Bath','TotRms_AbvGrd','Gr_Liv_Area',
248
- 'Garage_Cars','Garage_Area','Overall_Qual','Overall_Cond','Year_Built','Lot_Area',
249
- 'Fireplaces'
250
- ] if c in dados_filtrados.columns
251
- ]
252
- candidatos_categoricas = [c for c in ['Neighborhood','House_Style','Bldg_Type','Garage_Type','Kitchen_Qual'] if c in dados_filtrados.columns]
253
 
254
- with st.expander("Seleção de variáveis"):
255
- cols = st.columns(2)
256
  with cols[0]:
257
- feats_num = st.multiselect("Numéricas", options=candidatos_numericas, default=['Bedroom_AbvGr','Garage_Cars','Gr_Liv_Area'] if 'Garage_Cars' in candidatos_numericas else candidatos_numericas[:2])
 
 
 
 
 
 
258
  with cols[1]:
259
- feats_cat = st.multiselect("Categóricas", options=candidatos_categoricas, default=['Neighborhood'] if 'Neighborhood' in candidatos_categoricas else [])
260
-
261
- interagir = st.checkbox("Adicionar interação entre duas variáveis", value=False)
262
- inter_1 = inter_2 = None
263
- if interagir:
264
- inter_1 = st.selectbox("Variável 1 (para interação)", options=feats_num + feats_cat, index=0 if feats_num else 0)
265
- inter_2 = st.selectbox("Variável 2 (para interação)", options=[v for v in feats_num + feats_cat if v != inter_1], index=0)
266
-
267
- usar_logy = st.checkbox("Aplicar transformação log(SalePrice) caso pressuspostos sejam violados", value=False)
268
- teste_size = st.slider("Proporção de teste (holdout)", 0.1, 0.5, 0.2, 0.05)
269
- alpha_reg = st.slider("Nível de significância (α) — Regressão", 0.01, 0.10, 0.05, 0.01)
270
-
271
- # Construção da fórmula patsy
272
- def construir_formula(y, feats_num, feats_cat, inter_1=None, inter_2=None):
273
- termos = []
274
- termos += feats_num
275
- termos += [f"C({c})" for c in feats_cat]
276
- if inter_1 and inter_2:
277
- a = f"C({inter_1})" if inter_1 in feats_cat else inter_1
278
- b = f"C({inter_2})" if inter_2 in feats_cat else inter_2
279
- termos.append(f"{a}:{b}") # interação
280
- rhs = " + ".join(termos) if termos else "1"
281
- return f"{y} ~ {rhs}"
282
-
283
- if st.button("Ajustar modelo"):
284
- # Pré-seleção de colunas necessárias
285
- cols_necessarias = ['SalePrice'] + feats_num + feats_cat
286
- if interagir and inter_1 and inter_2:
287
- cols_necessarias += [inter_1, inter_2]
288
- df_modelo = dados_filtrados[cols_necessarias].dropna().copy()
289
- if df_modelo.empty:
290
- st.error("Sem dados suficientes após remoção de NAs nas variáveis selecionadas.")
291
- else:
292
- y_col = 'SalePrice'
293
- if usar_logy:
294
- df_modelo['SalePrice'] = np.log(df_modelo['SalePrice'].astype(float))
295
- y_col = 'SalePrice'
296
-
297
- formula = construir_formula(y_col, feats_num, feats_cat, inter_1 if interagir else None, inter_2 if interagir else None)
298
-
299
- # Split treino/teste
300
- df_treino, df_teste = train_test_split(df_modelo, test_size=teste_size, random_state=42)
301
-
302
- # Ajuste usando a fórmula (statsmodels cuida de dummies de C())
303
- model = ols(formula, data=df_treino).fit()
304
-
305
- st.markdown("#### Especificação do Modelo")
306
- st.code(formula)
307
-
308
- st.markdown("#### Coeficientes e Inferência")
309
- st.dataframe(model.summary2().tables[1])
310
-
311
- # Interpretação simples (sinal e significância)
312
- sig = model.pvalues < alpha_reg
313
- interpretacoes = []
314
- for nome, beta in model.params.items():
315
- if nome == 'Intercept':
316
- continue
317
- tag = "significativo" if sig.get(nome, False) else "não significativo"
318
- direcao = "aumenta" if beta > 0 else "reduz"
319
- interpretacoes.append(f"• {nome}: {tag}; coef. {beta:.3f} ⇒ {direcao} o preço esperado, ceteris paribus.")
320
- if interpretacoes:
321
- st.markdown("**Leitura rápida dos coeficientes:**\n" + "\n".join(interpretacoes))
322
-
323
- # Predição e métricas
324
- y_true = df_teste['SalePrice']
325
- y_pred = model.predict(df_teste)
326
- if usar_logy:
327
- # volta à escala original
328
- y_true_exp = np.exp(y_true)
329
- y_pred_exp = np.exp(y_pred)
330
- R2 = r2_score(y_true_exp, y_pred_exp)
331
- RMSE = mean_squared_error(y_true_exp, y_pred_exp, squared=False)
332
- MAE = mean_absolute_error(y_true_exp, y_pred_exp)
333
- else:
334
- R2 = r2_score(y_true, y_pred)
335
- RMSE = mean_squared_error(y_true, y_pred, squared=False)
336
- MAE = mean_absolute_error(y_true, y_pred)
337
-
338
- met_df = pd.DataFrame({
339
- 'Métrica': ['R²', 'RMSE', 'MAE'],
340
- 'Valor': [R2, RMSE, MAE]
341
- })
342
- st.markdown("#### Avaliação do Modelo (holdout)")
343
- st.dataframe(met_df)
344
-
345
- # Resíduos para pressupostos
346
- residuos = model.resid
347
- fitted = model.fittedvalues
348
-
349
- cols = st.columns(3)
350
- with cols[0]:
351
- st.markdown("**Resíduos vs Ajustados** (homocedasticidade)")
352
- fig_r, ax_r = plt.subplots(figsize=(4,3))
353
- ax_r.scatter(fitted, residuos, alpha=0.5)
354
- ax_r.axhline(0, color='red', linestyle='--')
355
- ax_r.set_xlabel('Ajustados')
356
- ax_r.set_ylabel('Resíduos')
357
- st.pyplot(fig_r)
358
- with cols[1]:
359
- st.markdown("**QQ-Plot** (normalidade)")
360
- fig_q = sm.qqplot(residuos, line='45', fit=True)
361
- st.pyplot(fig_q)
362
- with cols[2]:
363
- st.markdown("**Histograma dos Resíduos**")
364
- fig_h, ax_h = plt.subplots(figsize=(4,3))
365
- sns.histplot(residuos, kde=True, ax=ax_h)
366
- st.pyplot(fig_h)
367
-
368
- # Testes formais — Normalidade (Shapiro) e Homocedasticidade (Breusch-Pagan)
369
- sh_stat, sh_p = shapiro(residuos)
370
- bp_stat, bp_p, _, _ = het_breuschpagan(residuos, model.model.exog)
371
- st.markdown("#### Testes de Pressupostos (Regressão)")
372
- st.write(f"Shapiro-Wilk: estatística={sh_stat:.3f}, p={sh_p:.3f} → " + ("normalidade OK" if sh_p>=alpha_reg else "violação de normalidade"))
373
- st.write(f"Breusch-Pagan: estatística={bp_stat:.3f}, p={bp_p:.3f} → " + ("homocedasticidade OK" if bp_p>=alpha_reg else "heterocedasticidade detectada"))
374
-
375
- # Multicolinearidade — VIF somente para variáveis numéricas (dummies já expandidas pelo modelo)
376
- try:
377
- design = model.model.exog
378
- nomes = model.model.exog_names
379
- vif_vals = []
380
- for i in range(design.shape[1]):
381
- try:
382
- vif_vals.append(variance_inflation_factor(design, i))
383
- except Exception:
384
- vif_vals.append(np.nan)
385
- vif_df = pd.DataFrame({'Variável': nomes, 'VIF': vif_vals})
386
- st.markdown("#### VIF (Multicolinearidade)")
387
- st.dataframe(vif_df)
388
- except Exception as e:
389
- st.info(f"Não foi possível calcular VIF: {e}")
390
-
391
- # Orientações conforme pressupostos
392
- avisos = []
393
- if sh_p < alpha_reg:
394
- avisos.append("Normalidade violada — considere log-transformar o alvo (marque a opção log) ou técnicas robustas.")
395
- if bp_p < alpha_reg:
396
- avisos.append("Heterocedasticidade — avalie transformação ou erros-padrão robustos (HC).")
397
- if (pd.Series(vif_vals).dropna() > 10).any():
398
- avisos.append("VIF > 10 em alguma variável — possível multicolinearidade; reavalie/features ou regularização.")
399
- if avisos:
400
- st.warning("\n".join(avisos))
401
-
402
- # Interação — destaque se foi significativa
403
- if interagir and inter_1 and inter_2:
404
- termo = f"C({inter_1}):C({inter_2})" if (inter_1 in feats_cat and inter_2 in feats_cat) else (
405
- f"C({inter_1}):{inter_2}" if inter_1 in feats_cat else (
406
- f"{inter_1}:C({inter_2})" if inter_2 in feats_cat else f"{inter_1}:{inter_2}"))
407
- if termo in model.pvalues:
408
- p_int = model.pvalues[termo]
409
- st.info(f"Interação {termo} — p={p_int:.4f} → " + ("**significativa**" if p_int < alpha_reg else "não significativa"))
410
-
411
- st.success("Modelagem concluída.")
412
 
 
 
 
 
 
 
 
 
 
413
 
 
2
  # coding: utf-8
3
 
4
  # =====================================================
5
+ # Dashboard - AmesHousing (Tarefa 4)
6
  # =====================================================
7
 
8
  import streamlit as st
 
12
  import plotly.express as px
13
  import numpy as np
14
 
 
15
  from scipy.stats import shapiro, levene, kruskal
16
  from statsmodels.formula.api import ols
17
  import statsmodels.api as sm
 
24
  # Configuração da Página
25
  # -----------------------------------------------------
26
  st.set_page_config(
27
+ page_title="Dashboard - AmesHousing (Tarefa 4)",
28
  layout="wide",
29
  initial_sidebar_state="expanded"
30
  )
31
 
32
+ st.markdown("<h1 style='text-align:center;color:#003366;'>Análise do Dataset AmesHousing</h1>", unsafe_allow_html=True)
 
33
  st.markdown("---")
34
 
35
  # -----------------------------------------------------
36
+ # Leitura do CSV
37
  # -----------------------------------------------------
38
+ @st.cache_data
39
+ def carregar_dados():
40
+ paths_tentativa = [
41
+ "AmesHousing.csv",
42
+ "/mnt/data/AmesHousing.csv",
43
+ "../Dados/AmesHousing.csv",
44
+ ]
45
+ last_err = None
46
+ for p in paths_tentativa:
47
+ try:
48
+ df = pd.read_csv(p)
49
+ return df
50
+ except Exception as e:
51
+ last_err = e
52
+ continue
53
+ raise RuntimeError(f"Não foi possível carregar o AmesHousing.csv. Último erro: {last_err}")
54
+
55
+ casa_data = carregar_dados()
56
+ casa_data.columns = casa_data.columns.str.strip().str.replace(" ", "_")
57
+
58
+ # -----------------------------
59
+ # Sidebar — Filtros + Seleção de Variáveis (Regressão)
60
+ # -----------------------------
61
+ st.sidebar.markdown("### Filtros AmesHousing")
62
+
63
+ bairros = st.sidebar.multiselect(
64
+ "Selecione bairros",
65
+ options=sorted(casa_data["Neighborhood"].dropna().unique()),
66
+ default=None
67
+ )
68
 
69
+ dados_filtrados = casa_data.copy()
70
+ if bairros:
71
+ dados_filtrados = dados_filtrados[dados_filtrados["Neighborhood"].isin(bairros)]
72
+
73
+ st.sidebar.markdown("---")
74
+ st.sidebar.subheader("Regressão Linear — Tarefa 4 (PPCA/UnB)")
75
+ st.sidebar.markdown("Selecione variáveis para modelagem do **SalePrice** (alvo).")
76
+
77
+ candidatos_numericas = [
78
+ c for c in [
79
+ 'Bedroom_AbvGr','Full_Bath','Half_Bath','TotRms_AbvGrd','Gr_Liv_Area',
80
+ 'Garage_Cars','Garage_Area','Overall_Qual','Overall_Cond','Year_Built','Lot_Area',
81
+ 'Fireplaces'
82
+ ] if c in dados_filtrados.columns
83
+ ]
84
+ candidatos_categoricas = [c for c in ['Neighborhood','House_Style','Bldg_Type','Garage_Type','Kitchen_Qual'] if c in dados_filtrados.columns]
85
+
86
+ feats_num = st.sidebar.multiselect("Numéricas", options=candidatos_numericas, default=['Bedroom_AbvGr','Garage_Cars','Gr_Liv_Area'] if 'Garage_Cars' in candidatos_numericas else candidatos_numericas[:2])
87
+ feats_cat = st.sidebar.multiselect("Categóricas", options=candidatos_categoricas, default=['Neighborhood'] if 'Neighborhood' in candidatos_categoricas else [])
88
+
89
+ interagir = st.sidebar.checkbox("Adicionar interação entre duas variáveis", value=False)
90
+ inter_1 = inter_2 = None
91
+ if interagir:
92
+ inter_1 = st.sidebar.selectbox("Variável 1 (para interação)", options=feats_num + feats_cat, index=0 if feats_num else 0)
93
+ inter_2 = st.sidebar.selectbox("Variável 2 (para interação)", options=[v for v in feats_num + feats_cat if v != inter_1], index=0)
94
+
95
+ usar_logy = st.sidebar.checkbox("Aplicar transformação log(SalePrice) caso pressupostos sejam violados", value=False)
96
+ teste_size = st.sidebar.slider("Proporção de teste (holdout)", 0.1, 0.5, 0.2, 0.05)
97
+ alpha_reg = st.sidebar.slider("Nível de significância (α) — Regressão", 0.01, 0.10, 0.05, 0.01)
98
+
99
+ # -------------------------------------------------
100
+ # Conteúdo principal — Análises
101
+ # -------------------------------------------------
102
+
103
+ # Distribuição de Preço de Venda
104
+ st.subheader("Distribuição do Preço de Venda")
105
+ if not dados_filtrados.empty:
106
+ fig, ax = plt.subplots(figsize=(8,5))
107
+ sns.histplot(dados_filtrados['SalePrice'], kde=True, ax=ax)
108
+ ax.set_title("Distribuição do Preço de Venda")
109
+ st.pyplot(fig)
110
+ else:
111
+ st.warning("Nenhum dado disponível com os filtros aplicados.")
112
+
113
+ # Boxplots
114
+ st.subheader("Boxplots das Variáveis Selecionadas")
115
+ variavel = st.selectbox(
116
+ "Escolha a variável categórica para comparar preços:",
117
+ ["Neighborhood","Garage_Type","Fireplaces"]
118
+ )
119
+
120
+ if not dados_filtrados.empty:
121
+ if len(dados_filtrados[variavel].dropna().unique()) > 1:
122
+ fig2, ax2 = plt.subplots(figsize=(12,6))
123
+ sns.boxplot(x=variavel, y="SalePrice", data=dados_filtrados, ax=ax2)
124
+ plt.xticks(rotation=90)
125
+ ax2.set_title(f"Preço de Venda por {variavel}")
126
+ st.pyplot(fig2)
127
+ else:
128
+ st.warning(f"Não é possível gerar boxplot: apenas uma categoria em {variavel} após os filtros.")
129
+
130
+ # Scatter interativo
131
+ st.subheader("Preço Médio de Venda por Bairro")
132
+ if not dados_filtrados.empty:
133
+ bairro_grouped = dados_filtrados.groupby('Neighborhood').agg(
134
+ count=('SalePrice','size'),
135
+ mean_price=('SalePrice','mean')
136
+ ).reset_index()
137
+
138
+ bairro_filtered = bairro_grouped[bairro_grouped['count'] >= 5]
139
+ if not bairro_filtered.empty:
140
+ fig3 = px.scatter(
141
+ bairro_filtered,
142
+ x='mean_price',
143
+ y='Neighborhood',
144
+ size='count',
145
+ color='Neighborhood',
146
+ title='Preço Médio de Venda vs Bairro (Ames, Iowa)',
147
+ labels={'mean_price': 'Preço Médio de Venda', 'Neighborhood':'Bairro'},
148
+ opacity=0.8
149
+ )
150
+ st.plotly_chart(fig3, use_container_width=True)
151
  else:
152
+ st.warning("Não bairros suficientes após filtros para gerar o gráfico.")
153
+
154
+ # ANOVA
155
+ st.subheader("ANOVA para Neighborhood, Garage_Type e Fireplaces")
156
+ alpha = st.sidebar.slider(
157
+ "Nível de significância (α) - ANOVA AmesHousing",
158
+ 0.01,0.10,0.05,0.01,
159
+ key="alpha_ames"
160
+ )
161
 
162
+ if not dados_filtrados.empty:
163
+ for nome in ["Neighborhood", "Garage_Type", "Fireplaces"]:
164
+ categorias = dados_filtrados[nome].dropna().unique()
165
+ if len(categorias) < 2:
166
+ st.warning(f"ANOVA não pôde ser realizada para **{nome}** (menos de 2 grupos após os filtros).")
167
+ continue
168
+
169
+ modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
170
+ st.markdown(f"#### ANOVA - {nome}")
171
+ anova = sm.stats.anova_lm(modelo, typ=2)
172
+ st.dataframe(anova)
173
+
174
+ # Validação dos Pressupostos
175
+ st.markdown("### Validação dos Pressupostos da ANOVA")
176
+
177
+ st.markdown("#### Teste de Normalidade (Shapiro-Wilk)")
178
+ for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
179
+ categorias = dados_filtrados[nome].dropna().unique()
180
+ if len(categorias) < 2:
181
+ continue
182
+ modelo = ols(f'SalePrice ~ C({nome})', data=dados_filtrados).fit()
183
+ residuos = modelo.resid
184
+ stat, p = shapiro(residuos.dropna())
185
+ st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
186
+ + ("resíduos normais" if p >= alpha else "violação de normalidade"))
187
+
188
+ st.markdown("#### Teste de Homocedasticidade (Levene)")
189
+ for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
190
+ grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
191
+ if len(grupos) < 2:
192
+ continue
193
+ stat, p = levene(*grupos)
194
+ st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
195
+ + ("variâncias iguais" if p >= alpha else "variâncias diferentes"))
196
+
197
+ st.markdown("### Teste não-paramétrico (Kruskal-Wallis)")
198
+ for nome in ["Neighborhood","Garage_Type","Fireplaces"]:
199
+ grupos = [grupo["SalePrice"].dropna() for _, grupo in dados_filtrados.groupby(nome)]
200
+ if len(grupos) < 2:
201
+ continue
202
+ stat, p = kruskal(*grupos)
203
+ st.write(f"{nome}: estatística={stat:.3f}, p={p:.3f} "
204
+ + ("diferenças significativas" if p < alpha else "sem diferença significativa"))
205
+
206
+ # =================================================
207
+ # Regressão Linear — Tarefa 4
208
+ # =================================================
209
+
210
+ def construir_formula(y, feats_num, feats_cat, inter_1=None, inter_2=None):
211
+ termos = []
212
+ termos += feats_num
213
+ termos += [f"C({c})" for c in feats_cat]
214
+ if inter_1 and inter_2:
215
+ a = f"C({inter_1})" if inter_1 in feats_cat else inter_1
216
+ b = f"C({inter_2})" if inter_2 in feats_cat else inter_2
217
+ termos.append(f"{a}:{b}")
218
+ rhs = " + ".join(termos) if termos else "1"
219
+ return f"{y} ~ {rhs}"
220
+
221
+ if st.button("Ajustar modelo"):
222
+ cols_necessarias = ['SalePrice'] + feats_num + feats_cat
223
+ if interagir and inter_1 and inter_2:
224
+ cols_necessarias += [inter_1, inter_2]
225
+ df_modelo = dados_filtrados[cols_necessarias].dropna().copy()
226
+ if df_modelo.empty:
227
+ st.error("Sem dados suficientes após remoção de NAs nas variáveis selecionadas.")
228
+ else:
229
+ y_col = 'SalePrice'
230
+ if usar_logy:
231
+ df_modelo['SalePrice'] = np.log(df_modelo['SalePrice'].astype(float))
232
+ y_col = 'SalePrice'
233
 
234
+ formula = construir_formula(y_col, feats_num, feats_cat, inter_1 if interagir else None, inter_2 if interagir else None)
 
235
 
236
+ df_treino, df_teste = train_test_split(df_modelo, test_size=teste_size, random_state=42)
237
+ model = ols(formula, data=df_treino).fit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ st.markdown("#### Especificação do Modelo")
240
+ st.code(formula)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
+ st.markdown("#### Coeficientes e Inferência")
243
+ st.dataframe(model.summary2().tables[1])
 
 
 
 
 
 
 
 
 
 
244
 
245
+ sig = model.pvalues < alpha_reg
246
+ interpretacoes = []
247
+ for nome, beta in model.params.items():
248
+ if nome == 'Intercept':
 
 
 
 
 
 
 
 
249
  continue
250
+ tag = "significativo" if sig.get(nome, False) else "não significativo"
251
+ direcao = "aumenta" if beta > 0 else "reduz"
252
+ interpretacoes.append(f"• {nome}: {tag}; coef. {beta:.3f} ⇒ {direcao} o preço esperado, ceteris paribus.")
253
+ if interpretacoes:
254
+ st.markdown("**Leitura rápida dos coeficientes:**\n" + "\n".join(interpretacoes))
255
+
256
+ y_true = df_teste['SalePrice']
257
+ y_pred = model.predict(df_teste)
258
+ if usar_logy:
259
+ y_true_exp = np.exp(y_true)
260
+ y_pred_exp = np.exp(y_pred)
261
+ R2 = r2_score(y_true_exp, y_pred_exp)
262
+ RMSE = mean_squared_error(y_true_exp, y_pred_exp, squared=False)
263
+ MAE = mean_absolute_error(y_true_exp, y_pred_exp)
264
+ else:
265
+ R2 = r2_score(y_true, y_pred)
266
+ RMSE = mean_squared_error(y_true, y_pred, squared=False)
267
+ MAE = mean_absolute_error(y_true, y_pred)
268
 
269
+ met_df = pd.DataFrame({
270
+ 'Métrica': ['R²', 'RMSE', 'MAE'],
271
+ 'Valor': [R2, RMSE, MAE]
272
+ })
273
+ st.markdown("#### Avaliação do Modelo (holdout)")
274
+ st.dataframe(met_df)
275
+
276
+ residuos = model.resid
277
+ fitted = model.fittedvalues
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ cols = st.columns(3)
 
280
  with cols[0]:
281
+ st.markdown("**Resíduos vs Ajustados** (homocedasticidade)")
282
+ fig_r, ax_r = plt.subplots(figsize=(4,3))
283
+ ax_r.scatter(fitted, residuos, alpha=0.5)
284
+ ax_r.axhline(0, color='red', linestyle='--')
285
+ ax_r.set_xlabel('Ajustados')
286
+ ax_r.set_ylabel('Resíduos')
287
+ st.pyplot(fig_r)
288
  with cols[1]:
289
+ st.markdown("**QQ-Plot** (normalidade)")
290
+ fig_q = sm.qqplot(residuos, line='45', fit=True)
291
+ st.pyplot(fig_q)
292
+ with cols[2]:
293
+ st.markdown("**Histograma dos Resíduos**")
294
+ fig_h, ax_h = plt.subplots(figsize=(4,3))
295
+ sns.histplot(residuos, kde=True, ax=ax_h)
296
+ st.pyplot(fig_h)
297
+
298
+ sh_stat, sh_p = shapiro(residuos)
299
+ bp_stat, bp_p, _, _ = het_breuschpagan(residuos, model.model.exog)
300
+ st.markdown("#### Testes de Pressupostos (Regressão)")
301
+ st.write(f"Shapiro-Wilk: estatística={sh_stat:.3f}, p={sh_p:.3f} " + ("normalidade OK" if sh_p>=alpha_reg else "violação de normalidade"))
302
+ st.write(f"Breusch-Pagan: estatística={bp_stat:.3f}, p={bp_p:.3f} → " + ("homocedasticidade OK" if bp_p>=alpha_reg else "heterocedasticidade detectada"))
303
+
304
+ try:
305
+ design = model.model.exog
306
+ nomes = model.model.exog_names
307
+ vif_vals = []
308
+ for i in range(design.shape[1]):
309
+ try:
310
+ vif_vals.append(variance_inflation_factor(design, i))
311
+ except Exception:
312
+ vif_vals.append(np.nan)
313
+ vif_df = pd.DataFrame({'Variável': nomes, 'VIF': vif_vals})
314
+ st.markdown("#### VIF (Multicolinearidade)")
315
+ st.dataframe(vif_df)
316
+ except Exception as e:
317
+ st.info(f"Não foi possível calcular VIF: {e}")
318
+
319
+ avisos = []
320
+ if sh_p < alpha_reg:
321
+ avisos.append("Normalidade violada — considere log-transformar o alvo ou técnicas robustas.")
322
+ if bp_p < alpha_reg:
323
+ avisos.append("Heterocedasticidade — avalie transformação ou erros-padrão robustos (HC).")
324
+ if (pd.Series(vif_vals).dropna() > 10).any():
325
+ avisos.append("VIF > 10 em alguma variável — possível multicolinearidade.")
326
+ if avisos:
327
+ st.warning("\n".join(avisos))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
+ if interagir and inter_1 and inter_2:
330
+ termo = f"C({inter_1}):C({inter_2})" if (inter_1 in feats_cat and inter_2 in feats_cat) else (
331
+ f"C({inter_1}):{inter_2}" if inter_1 in feats_cat else (
332
+ f"{inter_1}:C({inter_2})" if inter_2 in feats_cat else f"{inter_1}:{inter_2}"))
333
+ if termo in model.pvalues:
334
+ p_int = model.pvalues[termo]
335
+ st.info(f"Interação {termo} — p={p_int:.4f} → " + ("**significativa**" if p_int < alpha_reg else "não significativa"))
336
+
337
+ st.success("Modelagem concluída.")
338