ricardoadriano commited on
Commit
5189ebb
·
verified ·
1 Parent(s): 7d4c7e1

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +205 -181
src/streamlit_app.py CHANGED
@@ -1,196 +1,220 @@
1
  #!/usr/bin/env python
2
  # coding: utf-8
3
-
4
- # =====================================================
5
- # Dashboard - AmesHousing (Tarefa 4) - versão compacta
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.stats import shapiro, levene, kruskal
16
- from statsmodels.formula.api import ols
17
- import statsmodels.api as sm
18
- from statsmodels.stats.diagnostic import het_breuschpagan
19
- from statsmodels.stats.outliers_influence import variance_inflation_factor
20
  from sklearn.model_selection import train_test_split
21
- from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
22
-
23
- # -----------------------------------------------------
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
- st.query_params.clear()
32
-
33
- st.markdown("<h1 style='text-align:center;color:#003366;'>Análise do Dataset AmesHousing</h1>", unsafe_allow_html=True)
34
- st.markdown("---")
35
 
36
- # -----------------------------------------------------
37
- # Leitura do CSV
38
- # -----------------------------------------------------
 
 
 
 
 
 
 
39
  @st.cache_data
40
- def carregar_dados():
41
- paths_tentativa = [
42
- "AmesHousing.csv",
43
- "/mnt/data/AmesHousing.csv",
44
- "../Dados/AmesHousing.csv",
 
 
45
  ]
46
- for p in paths_tentativa:
 
47
  try:
48
  df = pd.read_csv(p)
49
- return df
50
- except:
 
51
  continue
52
- return pd.DataFrame()
 
 
 
 
 
 
 
53
 
54
- casa_data = carregar_dados()
55
- casa_data.columns = casa_data.columns.str.strip().str.replace(" ", "_")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  # -----------------------------
58
- # Sidebar Filtros + Seleção de Variáveis (Regressão)
59
  # -----------------------------
60
- st.sidebar.markdown("### Filtros AmesHousing")
61
-
62
- bairros = st.sidebar.multiselect(
63
- "Selecione bairros",
64
- options=sorted(casa_data["Neighborhood"].dropna().unique()),
65
- default=None
66
- )
67
-
68
- dados_filtrados = casa_data.copy()
69
- if bairros:
70
- dados_filtrados = dados_filtrados[dados_filtrados["Neighborhood"].isin(bairros)]
71
-
72
- st.sidebar.markdown("---")
73
- st.sidebar.subheader("Regressão Linear — Tarefa 4 (PPCA/UnB)")
74
- st.sidebar.markdown("Selecione variáveis para modelagem do **SalePrice** (alvo).")
75
-
76
- candidatos_numericas = [
77
- c for c in [
78
- 'Bedroom_AbvGr','Full_Bath','Half_Bath','TotRms_AbvGrd','Gr_Liv_Area',
79
- 'Garage_Cars','Garage_Area','Overall_Qual','Overall_Cond','Year_Built','Lot_Area',
80
- 'Fireplaces'
81
- ] if c in dados_filtrados.columns
82
- ]
83
- candidatos_categoricas = [c for c in ['Neighborhood','House_Style','Bldg_Type','Garage_Type','Kitchen_Qual'] if c in dados_filtrados.columns]
84
-
85
- 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])
86
- feats_cat = st.sidebar.multiselect("Categóricas", options=candidatos_categoricas, default=['Neighborhood'] if 'Neighborhood' in candidatos_categoricas else [])
87
-
88
- interagir = st.sidebar.checkbox("Adicionar interação entre duas variáveis", value=False)
89
- inter_1 = inter_2 = None
90
- if interagir:
91
- inter_1 = st.sidebar.selectbox("Variável 1 (para interação)", options=feats_num + feats_cat, index=0 if feats_num else 0)
92
- 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)
93
-
94
- usar_logy = st.sidebar.checkbox("Aplicar transformação log(SalePrice) caso pressupostos sejam violados", value=False)
95
- teste_size = st.sidebar.slider("Proporção de teste (holdout)", 0.1, 0.5, 0.2, 0.05)
96
- alpha_reg = st.sidebar.slider("Nível de significância (α) — Regressão", 0.01, 0.10, 0.05, 0.01)
97
-
98
- # >>> botão agora na sidebar <<<
99
- ajustar = st.sidebar.button("Ajustar modelo")
100
-
101
- # -------------------------------------------------
102
- # Conteúdo principal — Análises
103
- # -------------------------------------------------
104
-
105
- # Distribuição de Preço de Venda
106
- st.subheader("Distribuição do Preço de Venda")
107
- if not dados_filtrados.empty:
108
- fig, ax = plt.subplots(figsize=(5,3.5))
109
- sns.histplot(dados_filtrados['SalePrice'], kde=True, ax=ax)
110
- st.pyplot(fig, clear_figure=True, use_container_width=False)
111
-
112
- # Boxplots
113
- st.subheader("Boxplots das Variáveis Selecionadas")
114
- variavel = st.selectbox(
115
- "Escolha a variável categórica para comparar preços:",
116
- ["Neighborhood","Garage_Type","Fireplaces"]
117
- )
118
- if not dados_filtrados.empty and len(dados_filtrados[variavel].dropna().unique()) > 1:
119
- fig2, ax2 = plt.subplots(figsize=(5,3.5))
120
- sns.boxplot(x=variavel, y="SalePrice", data=dados_filtrados, ax=ax2)
121
- plt.xticks(rotation=60)
122
- st.pyplot(fig2, clear_figure=True, use_container_width=False)
123
-
124
- # Scatter interativo
125
- st.subheader("Preço Médio de Venda por Bairro")
126
- if not dados_filtrados.empty:
127
- bairro_grouped = dados_filtrados.groupby('Neighborhood').agg(
128
- count=('SalePrice','size'),
129
- mean_price=('SalePrice','mean')
130
- ).reset_index()
131
- bairro_filtered = bairro_grouped[bairro_grouped['count'] >= 5]
132
- if not bairro_filtered.empty:
133
- fig3 = px.scatter(
134
- bairro_filtered,
135
- x='mean_price', y='Neighborhood',
136
- size='count', color='Neighborhood',
137
- labels={'mean_price': 'Preço Médio de Venda', 'Neighborhood':'Bairro'}
138
- )
139
- fig3.update_layout(width=550, height=320)
140
- st.plotly_chart(fig3, use_container_width=False)
141
-
142
- # =================================================
143
- # Regressão Linear Tarefa 4
144
- # =================================================
145
- def construir_formula(y, feats_num, feats_cat, inter_1=None, inter_2=None):
146
- termos = feats_num + [f"C({c})" for c in feats_cat]
147
- if inter_1 and inter_2:
148
- a = f"C({inter_1})" if inter_1 in feats_cat else inter_1
149
- b = f"C({inter_2})" if inter_2 in feats_cat else inter_2
150
- termos.append(f"{a}:{b}")
151
- rhs = " + ".join(termos) if termos else "1"
152
- return f"{y} ~ {rhs}"
153
-
154
- if ajustar: # botão na sidebar
155
- cols_necessarias = ['SalePrice'] + feats_num + feats_cat
156
- if interagir and inter_1 and inter_2:
157
- cols_necessarias += [inter_1, inter_2]
158
- df_modelo = dados_filtrados[cols_necessarias].dropna().copy()
159
- if not df_modelo.empty:
160
- y_col = 'SalePrice'
161
- if usar_logy:
162
- df_modelo['SalePrice'] = np.log(df_modelo['SalePrice'])
163
- formula = construir_formula(y_col, feats_num, feats_cat, inter_1 if interagir else None, inter_2 if interagir else None)
164
- df_treino, df_teste = train_test_split(df_modelo, test_size=teste_size, random_state=42)
165
- model = ols(formula, data=df_treino).fit()
166
-
167
- st.code(formula)
168
- st.dataframe(model.summary2().tables[1], use_container_width=False, height=200)
169
-
170
- y_true, y_pred = df_teste['SalePrice'], model.predict(df_teste)
171
- if usar_logy:
172
- y_true, y_pred = np.exp(y_true), np.exp(y_pred)
173
- metrics = pd.DataFrame({
174
- 'Métrica': ['R²','RMSE','MAE'],
175
- 'Valor': [r2_score(y_true, y_pred),
176
- mean_squared_error(y_true, y_pred, squared=False),
177
- mean_absolute_error(y_true, y_pred)]
178
- })
179
- st.dataframe(metrics, use_container_width=False, height=120)
180
-
181
- # Gráficos diagnósticos compactos
182
- residuos, fitted = model.resid, model.fittedvalues
183
- cols = st.columns(3)
184
- with cols[0]:
185
- fig_r, ax_r = plt.subplots(figsize=(3,3))
186
- ax_r.scatter(fitted, residuos, alpha=0.5)
187
- ax_r.axhline(0, color='red', linestyle='--')
188
- st.pyplot(fig_r, clear_figure=True, use_container_width=False)
189
- with cols[1]:
190
- fig_q, ax_q = plt.subplots(figsize=(3,3))
191
- sm.qqplot(residuos, line='45', fit=True, ax=ax_q)
192
- st.pyplot(fig_q, clear_figure=True, use_container_width=False)
193
- with cols[2]:
194
- fig_h, ax_h = plt.subplots(figsize=(3,3))
195
- sns.histplot(residuos, kde=True, ax=ax_h)
196
- st.pyplot(fig_h, clear_figure=True, use_container_width=False)
 
1
  #!/usr/bin/env python
2
  # coding: utf-8
3
+ """
4
+ Streamlit – Churn (Logistic Regression) for Hugging Face Spaces
5
+ ---------------------------------------------------------------
6
+ - Loads "Dados/Churn_Modelling.csv"
7
+ - Lets the user choose features (numeric / categorical)
8
+ - Trains Logistic Regression
9
+ - Shows coefficients, odds ratios, and quick interpretations
10
+ - Provides an interactive control panel to simulate a customer's probability of churn
11
+
12
+ Obs.: Esta versão atende ao item (a) da Tarefa 5: modelagem com Regressão Logística
13
+ e interpretação dos coeficientes/odds ratio.
14
+ """
15
+
16
+ import os
17
  import numpy as np
18
+ import pandas as pd
19
+ import streamlit as st
20
 
 
 
 
 
 
21
  from sklearn.model_selection import train_test_split
22
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
23
+ from sklearn.compose import ColumnTransformer
24
+ from sklearn.linear_model import LogisticRegression
25
+ from sklearn.pipeline import Pipeline
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # -----------------------------
28
+ # Page config
29
+ # -----------------------------
30
+ st.set_page_config(page_title="Churn – Regressão Logística (PPCA/UnB)", layout="wide", initial_sidebar_state="expanded")
31
+ st.title("Churn – Regressão Logística (PPCA/UnB)")
32
+ st.caption("Item (a) – Modelagem da Retenção de Clientes e interpretação de coeficientes/odds ratio.")
33
+
34
+ # -----------------------------
35
+ # Data loader (cache)
36
+ # -----------------------------
37
  @st.cache_data
38
+ def load_data():
39
+ tried = [
40
+ "Dados/Churn_Modelling.csv",
41
+ "./Dados/Churn_Modelling.csv",
42
+ "/mnt/data/Dados/Churn_Modelling.csv",
43
+ "Churn_Modelling.csv",
44
+ "./Churn_Modelling.csv"
45
  ]
46
+ last_err = None
47
+ for p in tried:
48
  try:
49
  df = pd.read_csv(p)
50
+ return df, p
51
+ except Exception as e:
52
+ last_err = e
53
  continue
54
+ return pd.DataFrame(), str(last_err)
55
+
56
+ df, data_info = load_data()
57
+
58
+ if df.empty:
59
+ st.error("Não foi possível carregar o arquivo **Churn_Modelling.csv**. "
60
+ "Certifique-se de que ele está em `Dados/Churn_Modelling.csv` dentro do Space.")
61
+ st.stop()
62
 
63
+ st.success(f"Dataset carregado de: `{data_info}`")
64
+
65
+ # Normalizar nomes de colunas
66
+ df.columns = [c.strip() for c in df.columns]
67
+
68
+ # -----------------------------
69
+ # Target and candidate features (dataset padrão do Kaggle)
70
+ # -----------------------------
71
+ TARGET = "Exited" # 1 = saiu, 0 = permaneceu
72
+ candidates_num = [c for c in ["CreditScore","Age","Tenure","Balance","NumOfProducts","HasCrCard","IsActiveMember","EstimatedSalary"] if c in df.columns]
73
+ candidates_cat = [c for c in ["Geography","Gender"] if c in df.columns]
74
+
75
+ # Sidebar: feature selection & model hyperparams
76
+ st.sidebar.header("Configuração do Modelo")
77
+ use_num = st.sidebar.multiselect("Variáveis numéricas", options=candidates_num, default=[c for c in ["Age","Balance","NumOfProducts","IsActiveMember"] if c in candidates_num])
78
+ use_cat = st.sidebar.multiselect("Variáveis categóricas", options=candidates_cat, default=[c for c in ["Geography","Gender"] if c in candidates_cat])
79
+
80
+ test_size = st.sidebar.slider("Proporção de teste", 0.1, 0.4, 0.2, 0.05)
81
+ reg_strength = st.sidebar.slider("Força de regularização (C)", 0.05, 5.0, 1.0, 0.05)
82
+ class_balanced = st.sidebar.checkbox("Class weight = 'balanced' (útil se desbalanceado)", value=True)
83
+ max_iter = st.sidebar.slider("Max iter", 200, 2000, 1000, 100)
84
+
85
+ train_btn = st.sidebar.button("Treinar modelo")
86
 
87
  # -----------------------------
88
+ # Quick EDA block (compact)
89
  # -----------------------------
90
+ st.subheader("Visão rápida do conjunto de dados")
91
+ col_a, col_b = st.columns([2,1])
92
+ with col_a:
93
+ st.dataframe(df.sample(min(10, len(df))), use_container_width=True)
94
+ with col_b:
95
+ if TARGET in df.columns:
96
+ n1 = int(df[TARGET].sum())
97
+ n0 = int((1 - df[TARGET]).sum())
98
+ st.metric("Clientes que saíram (1)", n1)
99
+ st.metric("Clientes que ficaram (0)", n0)
100
+
101
+ # -----------------------------
102
+ # Training
103
+ # -----------------------------
104
+ def build_pipeline(num_cols, cat_cols, C=1.0, class_weight=None, max_iter=1000):
105
+ preprocess = ColumnTransformer(
106
+ transformers=[
107
+ ("num", StandardScaler(with_mean=True, with_std=True), num_cols),
108
+ ("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), cat_cols),
109
+ ],
110
+ remainder="drop"
111
+ )
112
+ lr = LogisticRegression(C=C, penalty="l2", solver="lbfgs", max_iter=max_iter, class_weight=class_weight, n_jobs=None)
113
+ pipe = Pipeline(steps=[("prep", preprocess), ("clf", lr)])
114
+ return pipe
115
+
116
+ def get_feature_names(preprocess, num_cols, cat_cols):
117
+ names = []
118
+ if num_cols:
119
+ names.extend(num_cols)
120
+ if cat_cols:
121
+ ohe = preprocess.named_transformers_["cat"]
122
+ cat_names = ohe.get_feature_names_out(cat_cols).tolist()
123
+ names.extend(cat_names)
124
+ return names
125
+
126
+ if train_btn:
127
+ if not use_num and not use_cat:
128
+ st.warning("Selecione pelo menos **uma** variável explicativa (numérica ou categórica).")
129
+ st.stop()
130
+
131
+ cols_needed = [TARGET] + use_num + use_cat
132
+ df_model = df[cols_needed].dropna().copy()
133
+
134
+ X = df_model[use_num + use_cat]
135
+ y = df_model[TARGET]
136
+
137
+ cw = "balanced" if class_balanced else None
138
+ pipe = build_pipeline(use_num, use_cat, C=reg_strength, class_weight=cw, max_iter=max_iter)
139
+
140
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42, stratify=y)
141
+ pipe.fit(X_train, y_train)
142
+
143
+ # -------------------------
144
+ # Coefficients & Odds Ratios
145
+ # -------------------------
146
+ lr = pipe.named_steps["clf"]
147
+ preprocess = pipe.named_steps["prep"]
148
+
149
+ feat_names = get_feature_names(preprocess, use_num, use_cat)
150
+ coefs = lr.coef_.ravel()
151
+ odds = np.exp(coefs)
152
+
153
+ coef_table = pd.DataFrame({
154
+ "Variável": feat_names,
155
+ "Coeficiente (β)": coefs,
156
+ "Odds Ratio (e^β)": odds
157
+ }).sort_values(by="Odds Ratio (e^β)", ascending=False)
158
+
159
+ st.subheader("Coeficientes e Odds Ratio")
160
+ st.write(
161
+ "Interpretação: mantendo as demais variáveis constantes, um aumento de uma unidade na variável (ou mudança para a categoria indicada) "
162
+ "multiplica as *odds* de churn por `e^β`. Se `e^β > 1`, o risco de churn aumenta; se `< 1`, diminui."
163
+ )
164
+ st.dataframe(coef_table, use_container_width=True, height=380)
165
+
166
+ # Acurácia simples (para referência rápida no item a)
167
+ acc = pipe.score(X_test, y_test)
168
+ st.info(f"**Acurácia (holdout)**: {acc:.3f} | Amostras de treino: {len(X_train)} | Amostras de teste: {len(X_test)}")
169
+
170
+ # -------------------------
171
+ # Interactive prediction
172
+ # -------------------------
173
+ st.subheader("Simulação: probabilidade de churn para um perfil de cliente")
174
+ with st.expander("Abrir painel de controle do cliente", expanded=True):
175
+ # Build controls dynamically from current selections
176
+ inputs = {}
177
+ cols = st.columns(2)
178
+
179
+ # Numeric controls
180
+ for i, col in enumerate(use_num):
181
+ with cols[i % 2]:
182
+ vmin = float(np.nanmin(df[col])) if np.isfinite(df[col]).all() else 0.0
183
+ vmax = float(np.nanmax(df[col])) if np.isfinite(df[col]).all() else 1.0
184
+ vmean = float(np.nanmean(df[col])) if np.isfinite(df[col]).all() else (vmin + vmax)/2.0
185
+ step = (vmax - vmin) / 100.0 if vmax > vmin else 1.0
186
+ inputs[col] = st.number_input(f"{col}", value=round(vmean, 2), step=step, min_value=vmin, max_value=vmax, format="%.2f")
187
+
188
+ # Categorical controls
189
+ for i, col in enumerate(use_cat):
190
+ with cols[i % 2]:
191
+ opts = sorted([o for o in df[col].dropna().unique().tolist()])
192
+ default_idx = 0 if opts else None
193
+ inputs[col] = st.selectbox(f"{col}", options=opts, index=default_idx if default_idx is not None else 0)
194
+
195
+ # Compose a single-row DataFrame
196
+ if inputs:
197
+ row = pd.DataFrame([inputs])
198
+ proba = float(pipe.predict_proba(row)[0, 1])
199
+ st.metric("Probabilidade de churn (sair do banco)", f"{proba:.1%}")
200
+ st.caption("Dica: ajuste os controles e observe como a probabilidade muda.")
201
+
202
+ # -------------------------
203
+ # Textual help / interpretation
204
+ # -------------------------
205
+ st.subheader("Como interpretar os coeficientes")
206
+ st.markdown("""
207
+ - **Sinal de β**: positivo ⇒ aumenta as *odds* de churn; negativo ⇒ reduz.
208
+ - **Magnitude**: valores maiores em módulo indicam maior impacto, dado o mesmo escalonamento.
209
+ - **Odds Ratio `e^β`**: fator multiplicativo nas *odds*. Ex.: `e^β = 1.30` ⇒ as *odds* aumentam **30%**.
210
+ - Em variáveis **categóricas**, o β refere-se à **categoria de referência vs. a categoria exibida** (depois do one-hot com `drop='first'`).
211
+ """)
212
+
213
+ else:
214
+ st.info("Selecione as variáveis na barra lateral e clique em **Treinar modelo** para começar.")
215
+
216
+ # -----------------------------
217
+ # Footer
218
+ # -----------------------------
219
+ st.markdown("---")
220
+ st.caption("PPCA/UnB Tarefa 5 – Item (a) • Regressão Logística + Odds Ratio • Feito para rodar em Hugging Face Spaces (Streamlit).")