anova / app.py
252106862eder's picture
Update app.py
4506799 verified
import streamlit as st
import pandas as pd
import numpy as np
import scipy.stats as stats
import statsmodels.api as sm
from statsmodels.formula.api import ols
import matplotlib.pyplot as plt
import seaborn as sns
# --- 1. Configuração da Página Streamlit ---
st.set_page_config(
page_title="Análise ANOVA Interativa - Ames Housing",
page_icon="📊",
layout="wide", # Usa a largura total da tela
initial_sidebar_state="expanded" # Sidebar expandida por padrão
)
from PIL import Image
import os
# ... outros elementos do Streamlit ...
image_path="Logo/logo_ppca.png"
if os.path.exists(image_path):
try:
logo = Image.open(image_path)
st.image(logo, caption='ANÁLISE ESTATÍSTICA DE DADOS E INFORMAÇÕES', width=350)
except Exception as e:
st.error(f"Erro ao carregar a imagem '{image_path}': {e}")
st.write("Por favor, verifique se o arquivo é uma imagem válida (ex: .png, .jpg).")
st.header("""📊 Análise de Variância (ANOVA) Interativa
Programa de Pós-Graduação em Computação Aplicada - UNB - PPCA
Mestrado Profissional""")
st.subheader("Departamento de Ciência da Computação")
#st.write("""Prof. João Gabriel de Moraes Souza.""")
st.info("""Tarefa Avaliativa 3 – Análise com ANOVA no Mercado Imobiliário! 🏠📊""")
st.markdown(
"""
Bem-vindos!!
Eu sou Éder Marcelo Ponte Cunha, Mestrando do PPCA da UNB!\n
Esta aplicação interativa foi desenvolvida para explorar a relação
entre diferentes características de imóveis (categóricas) e seus preços\n
de venda no dataset **Ames Housing** através da Análise de Variância (ANOVA).\n
A ANOVA nos ajuda a entender se as médias de um grupo
(neste caso, preços de venda para diferentes tipos de garagem, vizinhança, Qualidade Geral, etc)
são significativamente diferentes entre si.\n
Aqui, você pode:
- **Visualizar as distribuições** de preços de venda por categoria.
- **Revisar os resultados da ANOVA** para cada característica.
- **Validar os pressupostos da ANOVA** (Normalidade, Homocedasticidade, Independência)
para garantir a validade dos resultados.
"""
)
st.sidebar.header("Configurações e Dados")
# --- 2. Carregamento dos Dados ---
@st.cache_data # Armazena em cache o carregamento e pré-processamento dos dados
def load_and_preprocess_data():
"""
Carrega o dataset AmesHousing.csv e realiza o pré-processamento necessário.
Retorna o DataFrame limpo.
"""
data_path = 'AmesHousing.csv' # Assumindo que o arquivo está no mesmo diretório
df = None
if os.path.exists(data_path):
try:
df = pd.read_csv(data_path)
st.sidebar.success("Base de dados 'AmesHousing.csv' carregada com sucesso.")
except Exception as e:
st.sidebar.error(f"Erro ao carregar 'AmesHousing.csv': {e}. Usando DataFrame de exemplo.")
else:
st.sidebar.warning(f"Arquivo '{data_path}' não encontrado. Usando DataFrame de exemplo.")
if df is None:
# DataFrame de exemplo para garantir que a aplicação funcione mesmo sem o arquivo
data = {
'Garage Type': ['Attchd', 'Detchd', 'BuiltIn', 'Attchd', 'Detchd', 'CarPort', np.nan, 'Attchd', 'No_Garage'],
'Neighborhood': ['CollgCr', 'Veenker', 'Crawfor', 'NoRidge', 'Mitchel', 'Somerst', 'MeadowV', 'StoneBr', 'IDYLWOD'],
'Overall Qual': [7, 8, 7, 9, 5, 6, 4, 8, 5],
'SalePrice': [200000, 250000, 180000, 350000, 120000, 150000, 90000, 300000, 110000]
}
df = pd.DataFrame(data)
# Renomear colunas para remover espaços e caracteres especiais
df.rename(columns={'Garage Type': 'Garage_Type', 'Overall Qual': 'Overall_Qual'}, inplace=True)
# Pré-processamento dos dados para ANOVA
# Preencher NaNs na coluna 'Garage_Type' com 'No_Garage'
df['Garage_Type'].fillna('No_Garage', inplace=True)
# Remover linhas com NaNs nas colunas críticas para a análise
df_clean = df.dropna(subset=['SalePrice', 'Neighborhood', 'Overall_Qual', 'Garage_Type'])
# Converter 'Overall_Qual' para tipo categórico (importante para ANOVA)
df_clean['Overall_Qual'] = df_clean['Overall_Qual'].astype('category')
return df_clean
df_clean = load_and_preprocess_data()
#selected_feature_for_code = next(key for key, value in feature_display_names.items() if value == selected_display_name)
st.sidebar.subheader("Visão Geral dos Dados")
if st.sidebar.checkbox("Mostrar as primeiras linhas do dataset"):
st.sidebar.dataframe(df_clean.head())
st.sidebar.info(f"Dataset carregado com {df_clean.shape[0]} linhas e {df_clean.shape[1]} colunas após a limpeza.")
# Lista de características para análise ANOVA
features_for_anova = ['Garage_Type', 'Neighborhood', 'Overall_Qual']
selected_feature = st.sidebar.selectbox(
"Selecione a Característica para Análise Detalhada:",
features_for_anova
)
# --- 3. Análise ANOVA e Visualizações (Seção Principal) ---
st.header("🔍 Análise ANOVA por Característica")
st.markdown(
f"Selecione uma característica na barra lateral para ver a análise ANOVA e os box plots. "
f"Atualmente, a análise detalhada está focada em **'{selected_feature.replace('_', ' ')}'**."
)
st.subheader(f"Distribuição de Preços de Venda por {selected_feature.replace('_', ' ')}")
st.markdown(
f"O box plot abaixo mostra a distribuição do Preço de Venda ('SalePrice') para cada categoria de '{selected_feature.replace('_', ' ')}'. "
"Isso ajuda a visualizar as diferenças nas medianas e na dispersão dos preços entre os grupos, "
"sugerindo se há diferenças significativas que a ANOVA pode confirmar."
)
fig_box = plt.figure(figsize=(12, 5))
sns.boxplot(x=selected_feature, y='SalePrice', data=df_clean)
plt.title(f'Distribuição de Preços de Venda por {selected_feature.replace("_", " ")}')
plt.xlabel(selected_feature.replace('_', ' '))
plt.ylabel('Preço de Venda')
plt.xticks(rotation=45, ha='right')
plt.grid(axis='y', linestyle='--', alpha=0.7)
st.pyplot(fig_box)
st.subheader(f"Tabela ANOVA para {selected_feature.replace('_', ' ')}")
st.markdown(
f"A tabela ANOVA abaixo resume os resultados estatísticos para a característica '{selected_feature.replace('_', ' ')}'. "
"O valor P ('PR(>F)') indica a probabilidade de observar diferenças tão grandes ou maiores entre as médias dos grupos "
"se não houvesse diferença real. Um P-valor < 0.05 (geralmente) sugere que há uma diferença estatisticamente significativa "
"entre pelo menos duas médias de grupo."
)
# Realizando a ANOVA usando statsmodels
formula = f'SalePrice ~ C({selected_feature})'
model = ols(formula, data=df_clean).fit()
anova_table = sm.stats.anova_lm(model, typ=2)
st.dataframe(anova_table)
# Interpretação básica do resultado da ANOVA
p_value_anova = anova_table['PR(>F)'][0] # P-valor da característica
st.info(
f"**Conclusão da ANOVA para '{selected_feature.replace('_', ' ')}':**\n\n"
f"- O P-valor para '{selected_feature.replace('_', ' ')}' é **{p_value_anova:.4f}**.\n"
f"- Se {p_value_anova:.4f} < 0.05, podemos rejeitar a hipótese nula (H0), indicando que há diferenças estatisticamente significativas "
f"nas médias de 'SalePrice' entre as categorias de '{selected_feature.replace('_', ' ')}'."
f"- Caso contrário, não há evidência estatística suficiente para afirmar que as médias são diferentes."
)
# --- 4. Validação dos Pressupostos da ANOVA (para a característica selecionada) ---
st.header("✔️ Validação dos Pressupostos da ANOVA")
st.markdown(
f"Para que os resultados da ANOVA sejam válidos, alguns pressupostos precisam ser atendidos. "
f"Vamos verificar esses pressupostos para a análise de 'SalePrice' por **'{selected_feature.replace('_', ' ')}'**."
)
# Calcular resíduos e valores ajustados para a característica selecionada
residuals = model.resid
fitted_values = model.fittedvalues
# --- 4.1. Teste de Normalidade dos Resíduos ---
st.subheader("1. Normalidade dos Resíduos")
st.markdown(
"A ANOVA assume que os resíduos (diferenças entre valores observados e previstos) "
"são normalmente distribuídos. Isso pode ser verificado visualmente e por testes estatísticos."
)
st.markdown("### 1.1. Histograma dos Resíduos")
st.markdown(
"Um histograma que se assemelha a uma curva em forma de sino (distribuição normal) "
"sugere que os resíduos são normalmente distribuídos."
)
fig_hist = plt.figure(figsize=(6, 3))
sns.histplot(residuals, kde=True, bins=50)
plt.title(f'Histograma dos Resíduos para {selected_feature.replace("_", " ")}')
plt.xlabel('Resíduos')
plt.ylabel('Frequência')
plt.grid(axis='y', linestyle='--', alpha=0.7)
st.pyplot(fig_hist)
st.markdown("### 1.2. Q-Q Plot dos Resíduos")
st.markdown(
"No Q-Q Plot, se os pontos seguirem de perto a linha diagonal, isso indica "
"que os resíduos são normalmente distribuídos."
)
fig_qq = sm.qqplot(residuals, line='s') # 's' para linha padronizada
plt.title(f'Q-Q Plot dos Resíduos para {selected_feature.replace("_", " ")}')
st.pyplot(fig_qq)
st.markdown("### 1.3. Teste Estatístico: Shapiro-Wilk Test")
st.markdown(
"**H0 (Hipótese Nula):** Os resíduos são normalmente distribuídos.\n"
"**Ha (Hipótese Alternativa):** Os resíduos NÃO são normalmente distribuídos.\n"
"Um P-valor menor que 0.05 (geralmente) sugere que devemos rejeitar H0, \n"
"indicando falta de normalidade. Este teste é sensível a grandes amostras."
)
shapiro_test_statistic, shapiro_p_value = stats.shapiro(residuals)
st.write(f"Estatística de Shapiro-Wilk: `{shapiro_test_statistic:.4f}`")
st.write(f"P-valor de Shapiro-Wilk: `{shapiro_p_value:.4f}`")
if shapiro_p_value < 0.05:
st.warning("Conclusão: Rejeitamos H0. Os resíduos **NÃO** são normalmente distribuídos. Isso pode afetar a validade da ANOVA.")
else:
st.success("Conclusão: Não rejeitamos H0. Os resíduos são normalmente distribuídos.")
# --- 4.2. Teste de Homocedasticidade ---
st.subheader("2. Homocedasticidade (Homogeneidade das Variâncias)")
st.markdown(
"A homocedasticidade significa que a variância dos resíduos é aproximadamente igual "
"para todos os grupos. A heterocedasticidade (variâncias desiguais) pode levar a "
"conclusões errôneas na ANOVA."
)
st.markdown("### 2.1. Visualização: Resíduos vs. Valores Ajustados")
st.markdown(
"Neste gráfico, esperamos ver uma 'nuvem' de pontos distribuída aleatoriamente em torno "
"da linha zero, sem padrões distintos (como um funil, o que indicaria heterocedasticidade)."
)
fig_res_vs_fit = plt.figure(figsize=(6, 3))
plt.scatter(fitted_values, residuals, alpha=0.5)
plt.axhline(y=0, color='r', linestyle='--')
plt.title(f'Resíduos vs. Valores Ajustados para {selected_feature.replace("_", " ")}')
plt.xlabel('Valores Ajustados (Preço de Venda Previsto)')
plt.ylabel('Resíduos')
plt.grid(True, linestyle='--', alpha=0.7)
st.pyplot(fig_res_vs_fit)
st.markdown("### 2.2. Teste Estatístico: Levene's Test")
st.markdown(
"**H0 (Hipótese Nula):** As variâncias dos resíduos são iguais entre os grupos (homocedasticidade).\n"
"**Ha (Hipótese Alternativa):** As variâncias dos resíduos **NÃO** são iguais entre os grupos (heterocedasticidade).\n"
"Um P-valor menor que 0.05 (geralmente) sugere que devemos rejeitar H0, "
"indicando heterocedasticidade."
)
# Agrupamos os resíduos por categoria da característica selecionada
groups_for_levene = [residuals[df_clean[selected_feature] == g] for g in df_clean[selected_feature].unique()]
levene_test_statistic, levene_p_value = stats.levene(*groups_for_levene)
st.write(f"Estatística de Levene: `{levene_test_statistic:.4f}`")
st.write(f"P-valor de Levene: `{levene_p_value:.4f}`")
if levene_p_value < 0.05:
st.warning("Conclusão: Rejeitamos H0. Há evidências de **heterocedasticidade** (variâncias desiguais). Isso pode afetar a validade da ANOVA.")
else:
st.success("Conclusão: Não rejeitamos H0. Há evidências de **homocedasticidade** (variâncias iguais).")
# --- 4.3. Independência dos Erros ---
st.subheader("3. Independência dos Erros")
st.markdown(
"Os erros (resíduos) devem ser independentes uns dos outros. Isso significa que "
"o erro de uma observação não deve estar relacionado ao erro de outra. "
"Este pressuposto é geralmente garantido por um bom desenho experimental e coleta de dados aleatória."
)
st.markdown(
"Para dados de imóveis como este, coletados como um 'snapshot' e não em série temporal, "
"a independência é geralmente uma suposição razoável, a menos que haja alguma ordem "
"espacial ou temporal não modelada. Visualmente, a falta de padrões no gráfico "
"de resíduos vs. valores ajustados também pode sugerir independência."
)
st.markdown("### Estatística Durbin-Watson")
st.markdown(
"A estatística Durbin-Watson testa a autocorrelação dos resíduos. Valores próximos de 2 "
"sugerem que não há autocorrelação. Valores abaixo de 1 ou acima de 3 podem indicar problemas."
)
durbin_watson_stat = sm.stats.durbin_watson(residuals)
st.write(f"Estatística Durbin-Watson: `{durbin_watson_stat:.4f}`")
if durbin_watson_stat > 1.5 and durbin_watson_stat < 2.5:
st.success("Conclusão: A estatística Durbin-Watson sugere que os resíduos são independentes (sem autocorrelação).")
else:
st.warning("Conclusão: A estatística Durbin-Watson pode indicar problemas de independência dos resíduos (autocorrelação).")
st.markdown("---")
st.markdown(
"**Importante:** A violação de um ou mais pressupostos da ANOVA não invalida "
"automaticamente a análise, mas pode exigir cautela na interpretação dos resultados "
"ou o uso de métodos alternativos (e.g., testes não paramétricos, transformações de dados)."
)
# --- 5. Otimização para Deploy no Hugging Face Spaces ---
#st.sidebar.markdown("---")
#st.sidebar.subheader("Informações para Deploy")
#st.sidebar.markdown(
#)"""
# --- Fim do Código ---