ricardoadriano commited on
Commit
19f6a77
·
verified ·
1 Parent(s): 9e60da0

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +410 -60
src/streamlit_app.py CHANGED
@@ -1,63 +1,413 @@
1
  #!/usr/bin/env python
2
- st.pyplot(fig_r)
3
- with cols[1]:
4
- st.markdown("**QQ-Plot** (normalidade)")
5
- fig_q = sm.qqplot(residuos, line='45', fit=True)
6
- st.pyplot(fig_q)
7
- with cols[2]:
8
- st.markdown("**Histograma dos Resíduos**")
9
- fig_h, ax_h = plt.subplots(figsize=(4,3))
10
- sns.histplot(residuos, kde=True, ax=ax_h)
11
- st.pyplot(fig_h)
12
-
13
-
14
- # Testes formais — Normalidade (Shapiro) e Homocedasticidade (Breusch-Pagan)
15
- sh_stat, sh_p = shapiro(residuos)
16
- bp_stat, bp_p, _, _ = het_breuschpagan(residuos, model.model.exog)
17
- st.markdown("#### Testes de Pressupostos (Regressão)")
18
- 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"))
19
- st.write(f"Breusch-Pagan: estatística={bp_stat:.3f}, p={bp_p:.3f} → " + ("homocedasticidade OK" if bp_p>=alpha_reg else "heterocedasticidade detectada"))
20
-
21
-
22
- # Multicolinearidade VIF somente para variáveis numéricas (dummies já expandidas pelo modelo)
23
- try:
24
- design = model.model.exog
25
- nomes = model.model.exog_names
26
- vif_vals = []
27
- for i in range(design.shape[1]):
28
- try:
29
- vif_vals.append(variance_inflation_factor(design, i))
30
- except Exception:
31
- vif_vals.append(np.nan)
32
- vif_df = pd.DataFrame({'Variável': nomes, 'VIF': vif_vals})
33
- st.markdown("#### VIF (Multicolinearidade)")
34
- st.dataframe(vif_df)
35
- except Exception as e:
36
- st.info(f"Não foi possível calcular VIF: {e}")
37
-
38
-
39
- # Orientações conforme pressupostos
40
- avisos = []
41
- if sh_p < alpha_reg:
42
- avisos.append("Normalidade violada — considere log-transformar o alvo (marque a opção log) ou técnicas robustas.")
43
- if bp_p < alpha_reg:
44
- avisos.append("Heterocedasticidade — avalie transformação ou erros-padrão robustos (HC).")
45
- if (pd.Series(vif_vals).dropna() > 10).any():
46
- avisos.append("VIF > 10 em alguma variável possível multicolinearidade; reavalie/features ou regularização.")
47
- if avisos:
48
- st.warning("\n".join(avisos))
49
-
50
-
51
- # Interação destaque se foi significativa
52
- if interagir and inter_1 and inter_2:
53
- termo = f"C({inter_1}):C({inter_2})" if (inter_1 in feats_cat and inter_2 in feats_cat) else (
54
- f"C({inter_1}):{inter_2}" if inter_1 in feats_cat else (
55
- f"{inter_1}:C({inter_2})" if inter_2 in feats_cat else f"{inter_1}:{inter_2}"))
56
- if termo in model.pvalues:
57
- p_int = model.pvalues[termo]
58
- st.info(f"Interação {termo} — p={p_int:.4f}" + ("**significativa**" if p_int < alpha_reg else "não significativa"))
59
-
60
-
61
- st.success("Modelagem concluída.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
 
1
  #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ # =====================================================
5
+ # Dashboard - Testes de Hipóteses com AmesHousing (Tarefa 4)
6
+ # =====================================================
7
+
8
+ import streamlit as st
9
+ import pandas as pd
10
+ import matplotlib.pyplot as plt
11
+ 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
19
+ from statsmodels.stats.diagnostic import het_breuschpagan
20
+ from statsmodels.stats.outliers_influence import variance_inflation_factor
21
+ from sklearn.model_selection import train_test_split
22
+ from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
23
+
24
+ # -----------------------------------------------------
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("Teste de Hipótese para Proporção de Testes Positivos de COVID-19")
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