ricardoadriano commited on
Commit
6bbdfd0
·
verified ·
1 Parent(s): 8726ddd

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +115 -105
src/streamlit_app.py CHANGED
@@ -4,7 +4,7 @@
4
  # Dataset: Dados/marketing_campaign.csv
5
  # Objetivo: permitir escolher ALVO e PREDITORES e produzir INFERÊNCIA (item e),
6
  # usando Logit (alvo binário) ou OLS (alvo contínuo).
7
- # Inclui engenharia de atributos inspirada no seu notebook.
8
  # -------------------------------------------------------------------
9
  import os
10
  import numpy as np
@@ -13,8 +13,6 @@ import streamlit as st
13
  import altair as alt
14
 
15
  from typing import List, Tuple
16
-
17
- # Pré-processamento e métricas (apoio)
18
  from sklearn.model_selection import train_test_split
19
  from sklearn.compose import ColumnTransformer
20
  from sklearn.preprocessing import OneHotEncoder, StandardScaler
@@ -24,37 +22,27 @@ from sklearn.metrics import (
24
  roc_auc_score, accuracy_score, confusion_matrix, roc_curve,
25
  r2_score, mean_squared_error
26
  )
27
-
28
- # Modelinhos rápidos para métricas (não usados na inferência)
29
  from sklearn.linear_model import LogisticRegression, LinearRegression
30
-
31
- # Inferência estatística
32
  import statsmodels.api as sm
33
 
34
  st.set_page_config(page_title="Inferência Estatística — Marketing Campaign", layout="wide")
35
  st.title("📊 Inferência Estatística — Customer Personality Analysis")
36
- st.caption("Escolha o **alvo** e as **variáveis explicativas** na barra lateral (esquerda) e obtenha inferência (item **e**).")
37
 
38
  DATA_PATH = "Dados/marketing_campaign.csv"
39
 
40
- # -----------------------------
41
- # Utilidades
42
- # -----------------------------
43
  @st.cache_data(show_spinner=False)
44
  def load_csv_try(path: str) -> pd.DataFrame:
45
- """
46
- Lê CSV tentando separadores: vírgula, ponto-e-vírgula e tab.
47
- """
48
  for sep in [",", ";", "\t"]:
49
  try:
50
  df = pd.read_csv(path, sep=sep, encoding="utf-8")
51
- # Heurística: se vier 1 coluna gigantesca, tenta o próximo separador
52
  if sep != "\t" and df.shape[1] == 1:
53
  continue
54
  return df
55
  except Exception:
56
  continue
57
- # última tentativa bruta
58
  return pd.read_csv(path, sep=None, engine="python")
59
 
60
  def split_num_cat(df: pd.DataFrame, exclude: List[str]) -> Tuple[List[str], List[str]]:
@@ -64,26 +52,32 @@ def split_num_cat(df: pd.DataFrame, exclude: List[str]) -> Tuple[List[str], List
64
 
65
  def is_binary_series(s: pd.Series) -> bool:
66
  vals = pd.unique(s.dropna())
67
- if len(vals) == 2:
68
- return True
69
- if s.dtype == bool:
70
- return True
71
- return False
 
 
 
 
 
72
 
73
  def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
74
- """Engenharia mínima inspirada no seu notebook: tenure, totais, shares, ticket, diversidade."""
75
  out = df.copy()
76
- # Datas → Tenure
 
77
  if "Dt_Customer" in out.columns:
78
  out["Dt_Customer"] = pd.to_datetime(out["Dt_Customer"], errors="coerce", dayfirst=True)
79
  out["TenureDays"] = (pd.Timestamp("today").normalize() - out["Dt_Customer"]).dt.days
80
 
81
- # Mnt* somados
82
  mnt_cols = [c for c in out.columns if c.startswith("Mnt")]
83
  if mnt_cols:
84
  out["TotalMnt"] = out[mnt_cols].sum(axis=1)
85
 
86
- # Compras totais e shares
87
  buy_cols = [c for c in ["NumWebPurchases", "NumCatalogPurchases", "NumStorePurchases"] if c in out.columns]
88
  if buy_cols:
89
  out["TotalPurchases"] = out[buy_cols].sum(axis=1)
@@ -96,16 +90,14 @@ def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
96
  if "TotalMnt" in out.columns and "TotalPurchases" in out.columns:
97
  out["AvgTicket"] = out["TotalMnt"] / out["TotalPurchases"].replace(0, np.nan)
98
 
99
- # Diversidade de cesta (quantos Mnt*>0)
100
  if mnt_cols:
101
  out["BasketDiversity"] = (out[mnt_cols] > 0).sum(axis=1)
102
 
103
  return out
104
 
105
  def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransformer:
106
- """
107
- Imputação + padronização (num) e OHE drop='first' (cat) para evitar colinearidade e viabilizar inferência.
108
- """
109
  num_pipe = Pipeline([
110
  ("imp", SimpleImputer(strategy="median")),
111
  ("scaler", StandardScaler())
@@ -114,11 +106,10 @@ def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransf
114
  ("imp", SimpleImputer(strategy="most_frequent")),
115
  ("ohe", OneHotEncoder(handle_unknown="ignore", drop="first", sparse_output=False))
116
  ])
117
- pre = ColumnTransformer([
118
  ("num", num_pipe, num_cols),
119
  ("cat", cat_pipe, cat_cols)
120
  ])
121
- return pre
122
 
123
  def get_feature_names(pre: ColumnTransformer, num_cols: List[str], cat_cols: List[str]) -> List[str]:
124
  names = list(num_cols)
@@ -128,24 +119,18 @@ def get_feature_names(pre: ColumnTransformer, num_cols: List[str], cat_cols: Lis
128
  return names
129
 
130
  def fit_inference(model_type: str, X_design: pd.DataFrame, y: pd.Series):
131
- """
132
- Ajusta modelo de inferência (statsmodels):
133
- - 'logit' → sm.Logit
134
- - 'ols' → sm.OLS
135
- Retorna (resultado_statsmodels, tabela).
136
- """
137
  X_sm = sm.add_constant(X_design, has_constant="add")
138
  if model_type == "logit":
139
  res = sm.Logit(y.values, X_sm).fit(disp=False)
140
- summ = res.summary2().tables[1].copy() # coef, std err, z, P>|z|, [0.025, 0.975]
141
  or_vals = np.exp(res.params)
142
  or_ci = np.exp(res.conf_int())
143
  tbl = pd.DataFrame({
144
- "feature": summ.index,
145
  "coef": res.params.values,
146
  "std_err": res.bse.values,
147
- "z/t": summ["z"].values,
148
- "p_value": summ["P>|z|"].values,
149
  "ci_low": res.conf_int()[0].values,
150
  "ci_high": res.conf_int()[1].values,
151
  "odds_ratio": or_vals.values,
@@ -167,7 +152,7 @@ def fit_inference(model_type: str, X_design: pd.DataFrame, y: pd.Series):
167
  return res, tbl
168
 
169
  def recs_from_inference(tbl: pd.DataFrame, model_type: str, k: int = 5):
170
- """Gera recomendações do item (e) a partir dos efeitos significativos (p<0.05), ignorando o intercepto."""
171
  df = tbl[tbl["feature"] != "const"].copy()
172
  df = df.sort_values(["p_value", "z/t"], ascending=[True, False])
173
  core = df[df["p_value"] < 0.05].head(k)
@@ -178,47 +163,44 @@ def recs_from_inference(tbl: pd.DataFrame, model_type: str, k: int = 5):
178
  if model_type == "logit":
179
  or_txt = f"(OR≈{r['odds_ratio']:.2f}, IC95% {r['or_ci_low']:.2f}–{r['or_ci_high']:.2f}, p={r['p_value']:.3g})"
180
  if sign > 0:
181
- out.append(f"🔧 **Reduzir exposição associada a `{feat}`** {or_txt}, pois aumento nessa variável eleva a probabilidade do evento alvo.")
182
  else:
183
- out.append(f"✅ **Fortalecer fatores ligados a `{feat}`** {or_txt}, pois valores maiores reduzem a probabilidade do evento alvo.")
184
  else:
185
  eff = f"(β≈{r['coef']:.3g}, IC95% {r['ci_low']:.2g}–{r['ci_high']:.2g}, p={r['p_value']:.3g})"
186
  if sign > 0:
187
  out.append(f"🔧 **Mitigar o crescimento de `{feat}`** {eff}, pois contribui positivamente para o aumento do alvo.")
188
  else:
189
  out.append(f"✅ **Aumentar `{feat}`** {eff}, pois está associado à redução do alvo.")
190
- # Trilhas transversais
191
- out.append("🧪 **Testes A/B** nas variáveis mais significativas para validar impacto causal antes de escalar.")
192
- out.append("📞 **Melhorar FCR** e fluxos críticos detectados pelas variáveis top-k (treinamento, scripts, UX).")
193
- out.append("🔁 **Feedback para Produto/Qualidade** baseado nos fatores com evidência estatística robusta.")
194
  return out[:k+3]
195
 
196
- # -----------------------------
197
- # Sidebar (lado esquerdo)
198
- # -----------------------------
199
  with st.sidebar:
200
  st.header("⚙️ Configuração")
201
  if not os.path.exists(DATA_PATH):
202
  st.error(f"Arquivo não encontrado: `{DATA_PATH}`. Suba o CSV em `Dados/`.")
203
  st.stop()
204
 
205
- # Carrega & engenharia
206
  df_raw = load_csv_try(DATA_PATH)
207
  df_eng = engineer_features(df_raw)
208
 
209
  with st.sidebar:
210
- st.markdown("**Escolha o alvo (variável dependente):**")
211
  all_cols = df_eng.columns.tolist()
212
- # Preferências de alvo conforme seu caderno
213
- preferred_targets = ["Response", "Complain", "HasComplained", "Exited", "Churn", "Complaint"]
214
- default_target = next((c for c in preferred_targets if c in all_cols), None)
 
 
 
215
  if default_target is None:
216
- # tenta primeira binária; senão primeira numérica
217
  for c in all_cols:
218
- if is_binary_series(df_eng[c]): default_target = c; break
219
- if default_target is None:
220
- for c in all_cols:
221
- if np.issubdtype(df_eng[c].dtype, np.number): default_target = c; break
222
  target_col = st.selectbox("Alvo (y)", options=all_cols, index=all_cols.index(default_target) if default_target in all_cols else 0)
223
 
224
  # Variáveis explicativas
@@ -227,7 +209,6 @@ num_cols_all, cat_cols_all = split_num_cat(df_eng, exclude=exclude)
227
 
228
  with st.sidebar:
229
  st.markdown("**Variáveis explicativas (X):**")
230
- # sugere alguns engenheirados primeiro
231
  engineered_first = [c for c in ["TenureDays","TotalMnt","TotalPurchases","OnlineShare","PromoShare","AvgTicket","BasketDiversity"] if c in num_cols_all]
232
  base_defaults = engineered_first + [c for c in num_cols_all if c not in engineered_first][:5] + cat_cols_all[:3]
233
  selected_feats = st.multiselect("Selecione X", options=(num_cols_all + cat_cols_all), default=base_defaults)
@@ -239,56 +220,93 @@ if len(selected_feats) == 0:
239
  st.warning("Selecione pelo menos uma variável explicativa.")
240
  st.stop()
241
 
242
- # -----------------------------
243
- # Amostra de dados
244
- # -----------------------------
245
- st.markdown("### 🔎 Amostra")
246
  st.dataframe(df_eng[[target_col] + selected_feats].head(12), use_container_width=True)
247
 
248
- # -----------------------------
249
- # Preparação / Treino
250
- # -----------------------------
251
- df = df_eng.dropna(subset=[target_col]).copy()
252
- y_raw = df[target_col]
253
  is_bin = is_binary_series(y_raw)
254
- model_type = "logit" if is_bin else "ols"
255
 
256
- # Mapear alvo binário não numérico {0,1}
257
- if is_bin and not np.issubdtype(y_raw.dtype, np.number):
258
- uniq = sorted(pd.unique(y_raw.dropna()).tolist(), key=lambda x: str(x))
259
- y = y_raw.replace({uniq[0]: 0, uniq[1]: 1}).astype(int)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  else:
261
- y = y_raw.astype(float if not is_bin else int)
 
 
262
 
263
- X = df[selected_feats].copy()
 
 
 
 
 
 
264
  sel_num = [c for c in selected_feats if np.issubdtype(X[c].dtype, np.number)]
265
  sel_cat = [c for c in selected_feats if (X[c].dtype == "object" or X[c].dtype.name == "category")]
266
 
267
  pre = build_preprocessor(sel_num, sel_cat)
268
- # Pipeline para métricas/predição
269
- quick_est = LogisticRegression(max_iter=200) if is_bin else LinearRegression()
270
  pipe = Pipeline([("pre", pre), ("est", quick_est)])
271
 
272
  X_train, X_test, y_train, y_test = train_test_split(
273
- X, y, test_size=test_size, random_state=random_state, stratify=y if is_bin else None
 
274
  )
275
 
276
  with st.spinner("Treinando e construindo matriz de design..."):
277
  pipe.fit(X_train, y_train)
278
  pre_fit = pipe.named_steps["pre"].fit(X_train, y_train)
279
  X_train_design = pre_fit.transform(X_train)
280
- feat_names = get_feature_names(pre_fit, sel_num, sel_cat)
 
 
 
 
281
  X_train_df = pd.DataFrame(X_train_design, columns=feat_names)
282
 
283
- # -----------------------------
284
- # Inferência estatística (item e)
285
- # -----------------------------
286
  st.markdown("## 📚 Inferência estatística (Item e)")
287
  with st.spinner("Ajustando modelo de inferência (statsmodels)..."):
288
  res, infer_tbl = fit_inference(model_type, X_train_df, y_train)
289
 
290
- if is_bin:
291
- st.caption("Modelo: **Logit** (alvo binário). Coeficientes em log-odds; também exibimos **odds ratios**.")
 
 
 
292
  cols_show = ["feature","coef","std_err","z/t","p_value","ci_low","ci_high","odds_ratio","or_ci_low","or_ci_high"]
293
  else:
294
  st.caption("Modelo: **OLS** (alvo contínuo). Coeficientes, erros-padrão, estatística t e IC 95%.")
@@ -296,11 +314,9 @@ else:
296
 
297
  st.dataframe(infer_tbl[cols_show].round(4), use_container_width=True)
298
 
299
- # -----------------------------
300
- # Métricas de desempenho
301
- # -----------------------------
302
  st.markdown("### 📈 Desempenho do modelo")
303
- if is_bin:
304
  y_proba = pipe.predict_proba(X_test)[:, 1] if hasattr(pipe.named_steps["est"], "predict_proba") else pipe.predict(X_test)
305
  y_pred = (y_proba >= 0.5).astype(int)
306
  auc = roc_auc_score(y_test, y_proba)
@@ -326,9 +342,7 @@ else:
326
  with c1: st.metric("R² (teste)", f"{r2:.3f}")
327
  with c2: st.metric("RMSE (teste)", f"{rmse:.3f}")
328
 
329
- # -----------------------------
330
- # Efeito visual (força por |t/z|)
331
- # -----------------------------
332
  st.markdown("### 🌟 Força dos efeitos (|t/z|)")
333
  eff_df = infer_tbl[infer_tbl["feature"] != "const"].copy()
334
  eff_df["effect_strength"] = eff_df["z/t"].abs()
@@ -338,9 +352,7 @@ eff_chart = alt.Chart(eff_df.sort_values("effect_strength", ascending=False).hea
338
  ).properties(height=420)
339
  st.altair_chart(eff_chart, use_container_width=True)
340
 
341
- # -----------------------------
342
- # Predição interativa
343
- # -----------------------------
344
  st.markdown("## 🔮 Predição interativa")
345
  st.caption("Ajuste valores para X e veja a probabilidade (Logit) ou valor previsto (OLS).")
346
 
@@ -349,7 +361,7 @@ with st.form("pred_form"):
349
  user_inputs = {}
350
  for i, col in enumerate(selected_feats):
351
  with cols[i % 3]:
352
- if col in sel_num:
353
  q1, q5, q95, q99 = X_train[col].quantile([0.01,0.05,0.95,0.99])
354
  default_val = float(np.nan_to_num(X_train[col].median(), nan=0.0))
355
  user_inputs[col] = st.number_input(
@@ -367,17 +379,15 @@ if submitted:
367
  x_new_df = pd.DataFrame(x_new_proc, columns=feat_names)
368
  X_sm_new = sm.add_constant(x_new_df, has_constant="add")
369
  y_hat = float(res.predict(X_sm_new)[0])
370
- if is_bin:
371
- st.success(f"Probabilidade prevista do evento alvo: **{y_hat:.2%}**")
372
  else:
373
  st.success(f"Valor previsto do alvo: **{y_hat:.4g}**")
374
 
375
- # -----------------------------
376
- # Recomendações (Item e)
377
- # -----------------------------
378
  st.markdown("## 🧭 Recomendações estratégicas (Item e)")
379
- for r in recs_from_inference(infer_tbl, model_type="logit" if is_bin else "ols", k=5):
380
  st.markdown("- " + r)
381
 
382
  st.markdown("---")
383
- st.caption("App Streamlit Inferência Estatística • Dados/marketing_campaign.csv • Controles na barra lateral (esquerda).")
 
4
  # Dataset: Dados/marketing_campaign.csv
5
  # Objetivo: permitir escolher ALVO e PREDITORES e produzir INFERÊNCIA (item e),
6
  # usando Logit (alvo binário) ou OLS (alvo contínuo).
7
+ # Se o alvo for categórico com >2 classes, permite one-vs-rest.
8
  # -------------------------------------------------------------------
9
  import os
10
  import numpy as np
 
13
  import altair as alt
14
 
15
  from typing import List, Tuple
 
 
16
  from sklearn.model_selection import train_test_split
17
  from sklearn.compose import ColumnTransformer
18
  from sklearn.preprocessing import OneHotEncoder, StandardScaler
 
22
  roc_auc_score, accuracy_score, confusion_matrix, roc_curve,
23
  r2_score, mean_squared_error
24
  )
 
 
25
  from sklearn.linear_model import LogisticRegression, LinearRegression
 
 
26
  import statsmodels.api as sm
27
 
28
  st.set_page_config(page_title="Inferência Estatística — Marketing Campaign", layout="wide")
29
  st.title("📊 Inferência Estatística — Customer Personality Analysis")
30
+ st.caption("Escolha o **alvo** e as **variáveis explicativas** na barra lateral (esquerda) e obtenha a inferência (item **e**).")
31
 
32
  DATA_PATH = "Dados/marketing_campaign.csv"
33
 
34
+ # ---------- Utilidades ----------
 
 
35
  @st.cache_data(show_spinner=False)
36
  def load_csv_try(path: str) -> pd.DataFrame:
37
+ """Lê CSV tentando separadores: vírgula, ponto-e-vírgula e tab."""
 
 
38
  for sep in [",", ";", "\t"]:
39
  try:
40
  df = pd.read_csv(path, sep=sep, encoding="utf-8")
 
41
  if sep != "\t" and df.shape[1] == 1:
42
  continue
43
  return df
44
  except Exception:
45
  continue
 
46
  return pd.read_csv(path, sep=None, engine="python")
47
 
48
  def split_num_cat(df: pd.DataFrame, exclude: List[str]) -> Tuple[List[str], List[str]]:
 
52
 
53
  def is_binary_series(s: pd.Series) -> bool:
54
  vals = pd.unique(s.dropna())
55
+ return len(vals) == 2 or s.dtype == bool
56
+
57
+ def coerce_numeric_series(s: pd.Series) -> pd.Series:
58
+ """Tenta converter strings numéricas para float (lida com vírgula decimal)."""
59
+ if np.issubdtype(s.dtype, np.number):
60
+ return s.astype(float)
61
+ # troca vírgula decimal por ponto, remove separadores comuns
62
+ tmp = s.astype(str).str.replace(r"[.\s]", "", regex=True).str.replace(",", ".", regex=False)
63
+ coerced = pd.to_numeric(tmp, errors="coerce")
64
+ return coerced
65
 
66
  def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
67
+ """Engenharia minimalista (compatível com o CSV padrão do Kaggle)."""
68
  out = df.copy()
69
+
70
+ # Tenure (dias desde Dt_Customer)
71
  if "Dt_Customer" in out.columns:
72
  out["Dt_Customer"] = pd.to_datetime(out["Dt_Customer"], errors="coerce", dayfirst=True)
73
  out["TenureDays"] = (pd.Timestamp("today").normalize() - out["Dt_Customer"]).dt.days
74
 
75
+ # Total gasto (Mnt*)
76
  mnt_cols = [c for c in out.columns if c.startswith("Mnt")]
77
  if mnt_cols:
78
  out["TotalMnt"] = out[mnt_cols].sum(axis=1)
79
 
80
+ # Compras totais e participações
81
  buy_cols = [c for c in ["NumWebPurchases", "NumCatalogPurchases", "NumStorePurchases"] if c in out.columns]
82
  if buy_cols:
83
  out["TotalPurchases"] = out[buy_cols].sum(axis=1)
 
90
  if "TotalMnt" in out.columns and "TotalPurchases" in out.columns:
91
  out["AvgTicket"] = out["TotalMnt"] / out["TotalPurchases"].replace(0, np.nan)
92
 
93
+ # Diversidade de cesta (quantos tipos Mnt*>0)
94
  if mnt_cols:
95
  out["BasketDiversity"] = (out[mnt_cols] > 0).sum(axis=1)
96
 
97
  return out
98
 
99
  def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransformer:
100
+ """Imputação + padronização (num) e OHE drop='first' (cat) para evitar colinearidade."""
 
 
101
  num_pipe = Pipeline([
102
  ("imp", SimpleImputer(strategy="median")),
103
  ("scaler", StandardScaler())
 
106
  ("imp", SimpleImputer(strategy="most_frequent")),
107
  ("ohe", OneHotEncoder(handle_unknown="ignore", drop="first", sparse_output=False))
108
  ])
109
+ return ColumnTransformer([
110
  ("num", num_pipe, num_cols),
111
  ("cat", cat_pipe, cat_cols)
112
  ])
 
113
 
114
  def get_feature_names(pre: ColumnTransformer, num_cols: List[str], cat_cols: List[str]) -> List[str]:
115
  names = list(num_cols)
 
119
  return names
120
 
121
  def fit_inference(model_type: str, X_design: pd.DataFrame, y: pd.Series):
122
+ """Ajusta a inferência (statsmodels): Logit p/ binário; OLS p/ contínuo."""
 
 
 
 
 
123
  X_sm = sm.add_constant(X_design, has_constant="add")
124
  if model_type == "logit":
125
  res = sm.Logit(y.values, X_sm).fit(disp=False)
 
126
  or_vals = np.exp(res.params)
127
  or_ci = np.exp(res.conf_int())
128
  tbl = pd.DataFrame({
129
+ "feature": res.params.index,
130
  "coef": res.params.values,
131
  "std_err": res.bse.values,
132
+ "z/t": res.tvalues.values if hasattr(res, "tvalues") else res.tvalues,
133
+ "p_value": res.pvalues.values,
134
  "ci_low": res.conf_int()[0].values,
135
  "ci_high": res.conf_int()[1].values,
136
  "odds_ratio": or_vals.values,
 
152
  return res, tbl
153
 
154
  def recs_from_inference(tbl: pd.DataFrame, model_type: str, k: int = 5):
155
+ """Gera recomendações (item e) a partir dos efeitos significativos (p<0.05), ignorando 'const'."""
156
  df = tbl[tbl["feature"] != "const"].copy()
157
  df = df.sort_values(["p_value", "z/t"], ascending=[True, False])
158
  core = df[df["p_value"] < 0.05].head(k)
 
163
  if model_type == "logit":
164
  or_txt = f"(OR≈{r['odds_ratio']:.2f}, IC95% {r['or_ci_low']:.2f}–{r['or_ci_high']:.2f}, p={r['p_value']:.3g})"
165
  if sign > 0:
166
+ out.append(f"🔧 **Reduzir exposição associada a `{feat}`** {or_txt}, pois aumento nessa variável eleva a probabilidade do alvo.")
167
  else:
168
+ out.append(f"✅ **Fortalecer fatores ligados a `{feat}`** {or_txt}, pois valores maiores reduzem a probabilidade do alvo.")
169
  else:
170
  eff = f"(β≈{r['coef']:.3g}, IC95% {r['ci_low']:.2g}–{r['ci_high']:.2g}, p={r['p_value']:.3g})"
171
  if sign > 0:
172
  out.append(f"🔧 **Mitigar o crescimento de `{feat}`** {eff}, pois contribui positivamente para o aumento do alvo.")
173
  else:
174
  out.append(f"✅ **Aumentar `{feat}`** {eff}, pois está associado à redução do alvo.")
175
+ # trilhas transversais
176
+ out.append("🧪 **Testes A/B** nas variáveis mais significativas para validar impacto causal.")
177
+ out.append("📞 **Melhorar FCR/primeiro contato** nas causas evidenciadas pelos top fatores.")
178
+ out.append("🔁 **Feedback a Produto/Qualidade** guiado pelos efeitos com evidência estatística robusta.")
179
  return out[:k+3]
180
 
181
+ # ---------- Sidebar (lado esquerdo) ----------
 
 
182
  with st.sidebar:
183
  st.header("⚙️ Configuração")
184
  if not os.path.exists(DATA_PATH):
185
  st.error(f"Arquivo não encontrado: `{DATA_PATH}`. Suba o CSV em `Dados/`.")
186
  st.stop()
187
 
 
188
  df_raw = load_csv_try(DATA_PATH)
189
  df_eng = engineer_features(df_raw)
190
 
191
  with st.sidebar:
192
+ st.markdown("**Alvo (variável dependente):**")
193
  all_cols = df_eng.columns.tolist()
194
+ # Preferência: 'Response' se existir; senão 1ª binária; senão 1ª numérica
195
+ default_target = "Response" if "Response" in all_cols else None
196
+ if default_target is None:
197
+ for c in all_cols:
198
+ if is_binary_series(df_eng[c]):
199
+ default_target = c; break
200
  if default_target is None:
 
201
  for c in all_cols:
202
+ if np.issubdtype(df_eng[c].dtype, np.number):
203
+ default_target = c; break
 
 
204
  target_col = st.selectbox("Alvo (y)", options=all_cols, index=all_cols.index(default_target) if default_target in all_cols else 0)
205
 
206
  # Variáveis explicativas
 
209
 
210
  with st.sidebar:
211
  st.markdown("**Variáveis explicativas (X):**")
 
212
  engineered_first = [c for c in ["TenureDays","TotalMnt","TotalPurchases","OnlineShare","PromoShare","AvgTicket","BasketDiversity"] if c in num_cols_all]
213
  base_defaults = engineered_first + [c for c in num_cols_all if c not in engineered_first][:5] + cat_cols_all[:3]
214
  selected_feats = st.multiselect("Selecione X", options=(num_cols_all + cat_cols_all), default=base_defaults)
 
220
  st.warning("Selecione pelo menos uma variável explicativa.")
221
  st.stop()
222
 
223
+ # ---------- Amostra ----------
224
+ st.markdown("### 🔎 Amostra dos dados")
 
 
225
  st.dataframe(df_eng[[target_col] + selected_feats].head(12), use_container_width=True)
226
 
227
+ # ---------- Preparação do alvo ----------
228
+ y_raw = df_eng[target_col]
229
+
230
+ # 1) tenta identificar binário diretamente
 
231
  is_bin = is_binary_series(y_raw)
 
232
 
233
+ # 2) se não binário, tenta numérico (coerção segura)
234
+ y_numeric_try = coerce_numeric_series(y_raw) if not is_bin else None
235
+ is_numeric_ok = False
236
+ if not is_bin:
237
+ if y_numeric_try is not None:
238
+ # considera "ok" se pelo menos 80% foram convertidos
239
+ conv_rate = y_numeric_try.notna().mean()
240
+ is_numeric_ok = conv_rate >= 0.8
241
+
242
+ # 3) se não binário e não numérico, vira categórico multi-classe → one-vs-rest
243
+ with st.sidebar:
244
+ positive_class = None
245
+ if not is_bin and not is_numeric_ok:
246
+ uniq_vals = sorted(pd.unique(y_raw.dropna()).tolist(), key=lambda x: str(x))
247
+ st.markdown("**Alvo categórico com múltiplas classes**")
248
+ positive_class = st.selectbox("Classe 'positiva' (one-vs-rest)", options=uniq_vals, index=0)
249
+ st.caption("O modelo fará Logit para a classe escolhida vs. as demais.")
250
+
251
+ # ---------- Montagem de y conforme os casos ----------
252
+ if is_bin:
253
+ # Se binário não-numérico → mapear para {0,1} em ordem alfabética
254
+ if not np.issubdtype(y_raw.dtype, np.number):
255
+ uniq = sorted(pd.unique(y_raw.dropna()).tolist(), key=lambda x: str(x))
256
+ y = y_raw.replace({uniq[0]: 0, uniq[1]: 1}).astype(int)
257
+ else:
258
+ y = y_raw.astype(int)
259
+ model_type = "logit"
260
+
261
+ elif is_numeric_ok:
262
+ y = y_numeric_try.astype(float)
263
+ model_type = "ols"
264
+
265
  else:
266
+ # one-vs-rest
267
+ y = (y_raw == positive_class).astype(int)
268
+ model_type = "logit"
269
 
270
+ # Alinha df aos y válidos
271
+ mask_valid = y.notna()
272
+ df_model = df_eng.loc[mask_valid].copy()
273
+ y = y.loc[mask_valid]
274
+ X = df_model[selected_feats].copy()
275
+
276
+ # ---------- Pré-processamento e treino ----------
277
  sel_num = [c for c in selected_feats if np.issubdtype(X[c].dtype, np.number)]
278
  sel_cat = [c for c in selected_feats if (X[c].dtype == "object" or X[c].dtype.name == "category")]
279
 
280
  pre = build_preprocessor(sel_num, sel_cat)
281
+ quick_est = LogisticRegression(max_iter=200) if model_type == "logit" else LinearRegression()
 
282
  pipe = Pipeline([("pre", pre), ("est", quick_est)])
283
 
284
  X_train, X_test, y_train, y_test = train_test_split(
285
+ X, y, test_size=test_size, random_state=random_state,
286
+ stratify=y if model_type == "logit" else None
287
  )
288
 
289
  with st.spinner("Treinando e construindo matriz de design..."):
290
  pipe.fit(X_train, y_train)
291
  pre_fit = pipe.named_steps["pre"].fit(X_train, y_train)
292
  X_train_design = pre_fit.transform(X_train)
293
+ # nomes das features após OHE
294
+ ohe_names = []
295
+ if sel_cat:
296
+ ohe_names = list(pre_fit.named_transformers_["cat"].named_steps["ohe"].get_feature_names_out(sel_cat))
297
+ feat_names = sel_num + ohe_names
298
  X_train_df = pd.DataFrame(X_train_design, columns=feat_names)
299
 
300
+ # ---------- Inferência (item e) ----------
 
 
301
  st.markdown("## 📚 Inferência estatística (Item e)")
302
  with st.spinner("Ajustando modelo de inferência (statsmodels)..."):
303
  res, infer_tbl = fit_inference(model_type, X_train_df, y_train)
304
 
305
+ if model_type == "logit":
306
+ if positive_class is not None:
307
+ st.caption(f"Modelo: **Logit** (one-vs-rest). Classe positiva: **{positive_class}**.")
308
+ else:
309
+ st.caption("Modelo: **Logit** (alvo binário). Coeficientes em log-odds; exibimos **odds ratios** e IC 95%.")
310
  cols_show = ["feature","coef","std_err","z/t","p_value","ci_low","ci_high","odds_ratio","or_ci_low","or_ci_high"]
311
  else:
312
  st.caption("Modelo: **OLS** (alvo contínuo). Coeficientes, erros-padrão, estatística t e IC 95%.")
 
314
 
315
  st.dataframe(infer_tbl[cols_show].round(4), use_container_width=True)
316
 
317
+ # ---------- Métricas ----------
 
 
318
  st.markdown("### 📈 Desempenho do modelo")
319
+ if model_type == "logit":
320
  y_proba = pipe.predict_proba(X_test)[:, 1] if hasattr(pipe.named_steps["est"], "predict_proba") else pipe.predict(X_test)
321
  y_pred = (y_proba >= 0.5).astype(int)
322
  auc = roc_auc_score(y_test, y_proba)
 
342
  with c1: st.metric("R² (teste)", f"{r2:.3f}")
343
  with c2: st.metric("RMSE (teste)", f"{rmse:.3f}")
344
 
345
+ # ---------- Força dos efeitos ----------
 
 
346
  st.markdown("### 🌟 Força dos efeitos (|t/z|)")
347
  eff_df = infer_tbl[infer_tbl["feature"] != "const"].copy()
348
  eff_df["effect_strength"] = eff_df["z/t"].abs()
 
352
  ).properties(height=420)
353
  st.altair_chart(eff_chart, use_container_width=True)
354
 
355
+ # ---------- Predição interativa ----------
 
 
356
  st.markdown("## 🔮 Predição interativa")
357
  st.caption("Ajuste valores para X e veja a probabilidade (Logit) ou valor previsto (OLS).")
358
 
 
361
  user_inputs = {}
362
  for i, col in enumerate(selected_feats):
363
  with cols[i % 3]:
364
+ if col in [c for c in selected_feats if np.issubdtype(X[c].dtype, np.number)]:
365
  q1, q5, q95, q99 = X_train[col].quantile([0.01,0.05,0.95,0.99])
366
  default_val = float(np.nan_to_num(X_train[col].median(), nan=0.0))
367
  user_inputs[col] = st.number_input(
 
379
  x_new_df = pd.DataFrame(x_new_proc, columns=feat_names)
380
  X_sm_new = sm.add_constant(x_new_df, has_constant="add")
381
  y_hat = float(res.predict(X_sm_new)[0])
382
+ if model_type == "logit":
383
+ st.success(f"Probabilidade prevista do alvo: **{y_hat:.2%}**")
384
  else:
385
  st.success(f"Valor previsto do alvo: **{y_hat:.4g}**")
386
 
387
+ # ---------- Recomendações (item e) ----------
 
 
388
  st.markdown("## 🧭 Recomendações estratégicas (Item e)")
389
+ for r in recs_from_inference(infer_tbl, model_type=model_type, k=5):
390
  st.markdown("- " + r)
391
 
392
  st.markdown("---")
393
+ st.caption("Controles na barra lateral (esquerda) • Dados: `Dados/marketing_campaign.csv`Inferência conforme item (e).")