vsalgs commited on
Commit
5158316
·
verified ·
1 Parent(s): 6c4cbd6

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +333 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,335 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
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
+ ]
20
+ df = None
21
+ url_carregada = ""
22
+ for url in urls_tentativas:
23
+ try:
24
+ df = pd.read_csv(url)
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
38
+ if 'saleprice' in df.columns:
39
+ coluna_preco_nome = 'saleprice'
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})'
78
+ try:
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')
118
+ ax_norm[1].text(0.5, 0.5, "Poucos dados", ha='center', va='center')
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)
136
+ results["kruskal_test"] = (stat_kruskal, p_kruskal)
137
+
138
+ # Boxplot
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)
150
+ if unique_cats > 10:
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
+ """)