ricardoadriano commited on
Commit
6a8a483
·
verified ·
1 Parent(s): 5189ebb

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +145 -32
src/streamlit_app.py CHANGED
@@ -17,6 +17,7 @@ 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
@@ -27,38 +28,128 @@ from sklearn.pipeline import Pipeline
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
 
@@ -69,13 +160,26 @@ df.columns = [c.strip() for c in df.columns]
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)
@@ -88,7 +192,7 @@ train_btn = st.sidebar.button("Treinar modelo")
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:
@@ -109,7 +213,10 @@ def build_pipeline(num_cols, cat_cols, C=1.0, class_weight=None, max_iter=1000):
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
 
@@ -137,7 +244,9 @@ if train_btn:
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
  # -------------------------
@@ -158,8 +267,9 @@ if train_btn:
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
 
@@ -172,7 +282,6 @@ if train_btn:
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
 
@@ -181,9 +290,12 @@ if train_btn:
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):
@@ -206,8 +318,9 @@ if train_btn:
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:
 
17
  import numpy as np
18
  import pandas as pd
19
  import streamlit as st
20
+ from pathlib import Path
21
 
22
  from sklearn.model_selection import train_test_split
23
  from sklearn.preprocessing import OneHotEncoder, StandardScaler
 
28
  # -----------------------------
29
  # Page config
30
  # -----------------------------
31
+ st.set_page_config(
32
+ page_title="Churn – Regressão Logística (PPCA/UnB)",
33
+ layout="wide",
34
+ initial_sidebar_state="expanded"
35
+ )
36
  st.title("Churn – Regressão Logística (PPCA/UnB)")
37
  st.caption("Item (a) – Modelagem da Retenção de Clientes e interpretação de coeficientes/odds ratio.")
38
 
39
  # -----------------------------
40
+ # Data loader (cache) – robusto para HF Spaces
41
  # -----------------------------
42
  @st.cache_data
43
  def load_data():
44
+ from pathlib import Path
45
+ import pandas as _pd
46
+
47
+ # Candidate roots a varrer
48
+ roots = []
49
+ try:
50
+ roots.append(Path(__file__).parent)
51
+ except Exception:
52
+ pass
53
+ roots += [Path.cwd(), Path("."), Path("/home/user/app")]
54
+
55
+ # Caminhos explícitos rápidos
56
+ fast_candidates = [
57
+ Path("Dados/Churn_Modelling.csv"),
58
+ Path("./Dados/Churn_Modelling.csv"),
59
+ Path("/mnt/data/Dados/Churn_Modelling.csv"),
60
+ Path("Churn_Modelling.csv"),
61
+ Path("./Churn_Modelling.csv"),
62
  ]
63
+
64
+ # Função simples de "sniff" de delimitador
65
+ def _detect_sep(sample_lines):
66
+ if any(";" in line for line in sample_lines):
67
+ return ";"
68
+ if any("\t" in line for line in sample_lines):
69
+ return "\t"
70
+ return ","
71
+
72
+ # 1) Tentar candidatos explícitos
73
+ for pth in fast_candidates:
74
  try:
75
+ if pth.exists():
76
+ text = pth.read_text(encoding="utf-8", errors="ignore")
77
+ sample = text.splitlines()[:5]
78
+ sep = _detect_sep(sample)
79
+ df_ = _pd.read_csv(pth, sep=sep)
80
+ return df_, str(pth)
81
+ except Exception:
82
+ pass
83
+
84
+ # 2) Busca recursiva case-insensitive pelo nome
85
+ targets = []
86
+ for root in roots:
87
+ if root.exists():
88
+ for p in root.rglob("*"):
89
+ try:
90
+ if p.is_file() and p.name.lower() == "churn_modelling.csv":
91
+ targets.append(p)
92
+ except Exception:
93
+ continue
94
+
95
+ # Preferir caminho dentro de 'Dados/'
96
+ targets.sort(
97
+ key=lambda p: (
98
+ 0 if ("Dados" in str(p.parent) or "dados" in str(p.parent)) else 1,
99
+ len(str(p))
100
+ )
101
+ )
102
+
103
+ for pth in targets:
104
+ try:
105
+ text = pth.read_text(encoding="utf-8", errors="ignore")
106
+ sample = text.splitlines()[:5]
107
+ sep = _detect_sep(sample)
108
+ df_ = _pd.read_csv(pth, sep=sep)
109
+ return df_, str(pth)
110
+ except Exception:
111
  continue
112
+
113
+ # Não achou
114
+ return _pd.DataFrame(), "caminhos não encontrados"
115
 
116
  df, data_info = load_data()
117
 
118
  if df.empty:
119
+ st.error("Não foi possível carregar **Churn_Modelling.csv** nos caminhos padrão.")
120
+ with st.expander("Diagnóstico rápido", expanded=True):
121
+ st.write("**Caminho de trabalho atual (cwd):**", os.getcwd())
122
+ try:
123
+ st.write("**Arquivos na raiz:**", os.listdir("."))
124
+ except Exception as e:
125
+ st.write("Falha ao listar raiz:", e)
126
+ dados_dir = Path("Dados")
127
+ if dados_dir.exists():
128
+ try:
129
+ st.write("**Arquivos em `Dados/`:**", os.listdir(dados_dir))
130
+ except Exception as e:
131
+ st.write("Falha ao listar `Dados/`:", e)
132
+ st.caption(
133
+ "Se `Dados/Churn_Modelling.csv` não aparecer acima, suba o CSV para o repositório do Space "
134
+ "com exatamente esse caminho e nome (case-sensitive)."
135
+ )
136
+
137
+ st.info("**Alternativa:** faça upload do CSV abaixo para testar agora (não persiste no repositório).")
138
+ up = st.file_uploader("Envie Churn_Modelling.csv", type=["csv"])
139
+ if up is not None:
140
+ # Tentar separar por vírgula, depois ponto-e-vírgula e tab, se necessário
141
+ try:
142
+ df = pd.read_csv(up)
143
+ except Exception:
144
+ up.seek(0)
145
+ try:
146
+ df = pd.read_csv(up, sep=";")
147
+ except Exception:
148
+ up.seek(0)
149
+ df = pd.read_csv(up, sep="\t")
150
+ data_info = "via upload do usuário"
151
+ else:
152
+ st.stop()
153
 
154
  st.success(f"Dataset carregado de: `{data_info}`")
155
 
 
160
  # Target and candidate features (dataset padrão do Kaggle)
161
  # -----------------------------
162
  TARGET = "Exited" # 1 = saiu, 0 = permaneceu
163
+ candidates_num = [
164
+ c for c in [
165
+ "CreditScore", "Age", "Tenure", "Balance", "NumOfProducts",
166
+ "HasCrCard", "IsActiveMember", "EstimatedSalary"
167
+ ] if c in df.columns
168
+ ]
169
+ candidates_cat = [c for c in ["Geography", "Gender"] if c in df.columns]
170
 
171
  # Sidebar: feature selection & model hyperparams
172
  st.sidebar.header("Configuração do Modelo")
173
+ use_num = st.sidebar.multiselect(
174
+ "Variáveis numéricas",
175
+ options=candidates_num,
176
+ default=[c for c in ["Age", "Balance", "NumOfProducts", "IsActiveMember"] if c in candidates_num]
177
+ )
178
+ use_cat = st.sidebar.multiselect(
179
+ "Variáveis categóricas",
180
+ options=candidates_cat,
181
+ default=[c for c in ["Geography", "Gender"] if c in candidates_cat]
182
+ )
183
 
184
  test_size = st.sidebar.slider("Proporção de teste", 0.1, 0.4, 0.2, 0.05)
185
  reg_strength = st.sidebar.slider("Força de regularização (C)", 0.05, 5.0, 1.0, 0.05)
 
192
  # Quick EDA block (compact)
193
  # -----------------------------
194
  st.subheader("Visão rápida do conjunto de dados")
195
+ col_a, col_b = st.columns([2, 1])
196
  with col_a:
197
  st.dataframe(df.sample(min(10, len(df))), use_container_width=True)
198
  with col_b:
 
213
  ],
214
  remainder="drop"
215
  )
216
+ lr = LogisticRegression(
217
+ C=C, penalty="l2", solver="lbfgs",
218
+ max_iter=max_iter, class_weight=class_weight, n_jobs=None
219
+ )
220
  pipe = Pipeline(steps=[("prep", preprocess), ("clf", lr)])
221
  return pipe
222
 
 
244
  cw = "balanced" if class_balanced else None
245
  pipe = build_pipeline(use_num, use_cat, C=reg_strength, class_weight=cw, max_iter=max_iter)
246
 
247
+ X_train, X_test, y_train, y_test = train_test_split(
248
+ X, y, test_size=test_size, random_state=42, stratify=y
249
+ )
250
  pipe.fit(X_train, y_train)
251
 
252
  # -------------------------
 
267
 
268
  st.subheader("Coeficientes e Odds Ratio")
269
  st.write(
270
+ "Interpretação: mantendo as demais variáveis constantes, um aumento de uma unidade na variável "
271
+ "(ou mudança para a categoria indicada) multiplica as *odds* de churn por `e^β`. "
272
+ "Se `e^β > 1`, o risco de churn aumenta; se `< 1`, diminui."
273
  )
274
  st.dataframe(coef_table, use_container_width=True, height=380)
275
 
 
282
  # -------------------------
283
  st.subheader("Simulação: probabilidade de churn para um perfil de cliente")
284
  with st.expander("Abrir painel de controle do cliente", expanded=True):
 
285
  inputs = {}
286
  cols = st.columns(2)
287
 
 
290
  with cols[i % 2]:
291
  vmin = float(np.nanmin(df[col])) if np.isfinite(df[col]).all() else 0.0
292
  vmax = float(np.nanmax(df[col])) if np.isfinite(df[col]).all() else 1.0
293
+ vmean = float(np.nanmean(df[col])) if np.isfinite(df[col]).all() else (vmin + vmax) / 2.0
294
  step = (vmax - vmin) / 100.0 if vmax > vmin else 1.0
295
+ inputs[col] = st.number_input(
296
+ f"{col}", value=round(vmean, 2), step=step,
297
+ min_value=vmin, max_value=vmax, format="%.2f"
298
+ )
299
 
300
  # Categorical controls
301
  for i, col in enumerate(use_cat):
 
318
  st.markdown("""
319
  - **Sinal de β**: positivo ⇒ aumenta as *odds* de churn; negativo ⇒ reduz.
320
  - **Magnitude**: valores maiores em módulo indicam maior impacto, dado o mesmo escalonamento.
321
+ - **Odds Ratio `e^β`**: fator multiplicativo nas *odds*. Ex.: `e^β = 1,30` ⇒ as *odds* aumentam **30%**.
322
+ - Em variáveis **categóricas**, o β refere-se à **categoria de referência vs. a categoria exibida**
323
+ (depois do one-hot com `drop='first'`).
324
  """)
325
 
326
  else: