ricardoadriano commited on
Commit
3331850
·
verified ·
1 Parent(s): d3051a3

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +295 -291
src/streamlit_app.py CHANGED
@@ -1,6 +1,11 @@
1
- # App: Análise de Reclamações de Consumidores
2
- # Dataset esperado: Dados/marketing_campaign.csv (Kaggle - Customer Personality Analysis)
3
-
 
 
 
 
 
4
  import os
5
  import numpy as np
6
  import pandas as pd
@@ -9,67 +14,105 @@ import altair as alt
9
 
10
  from typing import List, Tuple
11
 
12
- # Sci-kit / stats
13
  from sklearn.model_selection import train_test_split
14
  from sklearn.compose import ColumnTransformer
15
  from sklearn.preprocessing import OneHotEncoder, StandardScaler
16
  from sklearn.pipeline import Pipeline
17
  from sklearn.impute import SimpleImputer
18
- from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix, RocCurveDisplay
19
- from sklearn.inspection import permutation_importance
 
 
20
 
21
- from sklearn.linear_model import LogisticRegression
22
- from sklearn.ensemble import RandomForestClassifier
23
 
 
24
  import statsmodels.api as sm
25
 
26
- # -----------------------------
27
- # Configurações gerais
28
- # -----------------------------
29
- st.set_page_config(
30
- page_title="Reclamações de Consumidores — PPCA/UnB",
31
- layout="wide",
32
- )
33
-
34
- st.title("Reclamações de Consumidores — Predição & Explicação")
35
- st.caption("Com base no conjunto **Customer Personality Analysis** (marketing_campaign.csv)")
36
 
37
  DATA_PATH = "Dados/marketing_campaign.csv"
38
 
39
  # -----------------------------
40
- # Utilitários
41
  # -----------------------------
42
  @st.cache_data(show_spinner=False)
43
- def load_data(path: str) -> pd.DataFrame:
44
- df = pd.read_csv(path, sep=",", encoding="utf-8")
45
- # Alguns CSVs deste dataset vêm com separador ';'. Se falhar, tenta novamente:
46
- if df.shape[1] == 1:
47
- df = pd.read_csv(path, sep=";", encoding="utf-8")
48
- return df
49
-
50
- def infer_target_column(df: pd.DataFrame) -> str:
51
- # No dataset da Kaggle, a variável é "Complain" (0/1).
52
- # Se não existir, tenta nomes comuns.
53
- candidates = ["Complain", "complain", "Complaint", "has_complaint", "has_complain"]
54
- for c in candidates:
55
- if c in df.columns:
56
- return c
57
- # fallback: se não achou, cria guiagem
58
- return None
59
-
60
- def split_features(df: pd.DataFrame, y_col: str) -> Tuple[List[str], List[str]]:
61
- cat_cols = [c for c in df.columns if (df[c].dtype == "object" or df[c].dtype.name == "category") and c != y_col]
62
- num_cols = [c for c in df.columns if (np.issubdtype(df[c].dtype, np.number)) and c != y_col]
63
  return num_cols, cat_cols
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransformer:
 
 
 
66
  num_pipe = Pipeline([
67
- ("imputer", SimpleImputer(strategy="median")),
68
  ("scaler", StandardScaler())
69
  ])
70
  cat_pipe = Pipeline([
71
- ("imputer", SimpleImputer(strategy="most_frequent")),
72
- ("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
73
  ])
74
  pre = ColumnTransformer([
75
  ("num", num_pipe, num_cols),
@@ -77,303 +120,264 @@ def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransf
77
  ])
78
  return pre
79
 
80
- def get_model(name: str):
81
- if name == "Regressão Logística":
82
- return LogisticRegression(max_iter=200, n_jobs=None) # n_jobs só no liblinear/saga; usamos solver padrão (lbfgs)
83
- elif name == "Random Forest":
84
- return RandomForestClassifier(
85
- n_estimators=300,
86
- max_depth=None,
87
- random_state=42,
88
- n_jobs=-1
89
- )
90
- else:
91
- raise ValueError("Modelo desconhecido.")
92
-
93
- def coefficient_table_for_logit(statsmodels_result, feature_names):
94
- # Retorna DataFrame com OR e IC 95%
95
- params = statsmodels_result.params
96
- conf = statsmodels_result.conf_int(alpha=0.05)
97
- df_coef = pd.DataFrame({
98
- "feature": ["Intercept"] + feature_names,
99
- "coef": params.values
100
- })
101
- conf = pd.DataFrame(conf.values, columns=["ci_low", "ci_high"])
102
- df_coef["ci_low"] = conf["ci_low"].values
103
- df_coef["ci_high"] = conf["ci_high"].values
104
-
105
- # Odds ratios
106
- df_coef["odds_ratio"] = np.exp(df_coef["coef"])
107
- df_coef["or_ci_low"] = np.exp(df_coef["ci_low"])
108
- df_coef["or_ci_high"] = np.exp(df_coef["ci_high"])
109
- return df_coef
110
-
111
- def make_recommendations(imp_df: pd.DataFrame, top_k: int = 5) -> List[str]:
112
  """
113
- Gera recomendações de alto nível com base nas variáveis mais importantes.
114
- imp_df precisa ter colunas: feature, importance, sign (para regressão logística; senão assume neutro).
 
 
115
  """
116
- recs = []
117
- # Pega top_k
118
- core = imp_df.sort_values("importance", ascending=False).head(top_k)
119
- for _, row in core.iterrows():
120
- feat = row["feature"]
121
- sign = row.get("sign", 0)
122
- if sign > 0:
123
- recs.append(
124
- f"🔧 **Reduzir a exposição associada a `{feat}`**, pois aumento nessa variável eleva a probabilidade de reclamação. "
125
- f"Considere políticas específicas (p.ex., comunicação proativa, revisão de políticas de entrega/atendimento, "
126
- f"ou benefícios segmentados para o grupo impactado por `{feat}`)."
127
- )
128
- elif sign < 0:
129
- recs.append(
130
- f" **Ampliar ações relacionadas a `{feat}`**, já que maior valor nessa variável tende a reduzir reclamações. "
131
- f"Ex.: expandir programas de fidelidade ou incentivos que reforcem o comportamento ligado a `{feat}`."
132
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  else:
134
- recs.append(
135
- f" **Monitorar `{feat}`** de perto: é importante, ainda que a direção do efeito varie entre segmentos. "
136
- f"Teste intervenções com experimentos A/B e avalie impacto nas métricas de reclamação."
137
- )
138
- # Recomendações genéricas de processo:
139
- recs.append(" **Implantar testes A/B** para validar intervenções nas variáveis-chave e medir impacto em taxa de reclamação.")
140
- recs.append(" **Aprimorar o 1º contato (FCR)**: reduzir transferência/recontato; scripts e treinamentos focados nas causas top-1/2.")
141
- recs.append(" **Feedback loop**: alimentar o time de Produto/Qualidade com causas de reclamação mais preditivas para correções upstream.")
142
- return recs
 
143
 
144
  # -----------------------------
145
- # Carregamento
146
  # -----------------------------
147
  with st.sidebar:
148
- st.header("Dados")
149
- st.write("Esperado: `Dados/marketing_campaign.csv`")
150
  if not os.path.exists(DATA_PATH):
151
- st.error(f"Arquivo não encontrado em `{DATA_PATH}`. Suba o CSV na pasta `Dados/` do Space.")
152
- else:
153
- st.success("Arquivo encontrado")
154
 
155
- try:
156
- df_raw = load_data(DATA_PATH)
157
- except Exception as e:
158
- st.stop()
159
 
160
- target_col = infer_target_column(df_raw)
161
- if target_col is None:
162
- st.error("Não encontrei a coluna alvo (ex.: `Complain`). Confirme o nome no CSV.")
163
- st.dataframe(df_raw.head())
164
- st.stop()
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- # -----------------------------
167
- # Sidebar — Configuração
168
- # -----------------------------
169
  with st.sidebar:
170
- st.header("Configuração do Modelo")
171
- st.caption("**Item (c)** Definição & Seleção de Modelos")
172
- model_name = st.selectbox("Modelo preditivo", ["Regressão Logística", "Random Forest"], index=0)
173
-
174
- # Seleção de variáveis explicativas
175
- st.subheader("Variáveis explicativas")
176
- num_cols, cat_cols = split_features(df_raw, target_col)
177
- all_feats = num_cols + cat_cols
178
- default_feats = [c for c in all_feats if c != target_col]
179
- selected_feats = st.multiselect(
180
- "Selecione as variáveis de entrada",
181
- options=default_feats,
182
- default=default_feats[: min(12, len(default_feats))]
183
- )
184
 
185
  test_size = st.slider("Proporção de teste", 0.1, 0.4, 0.2, 0.05)
186
  random_state = st.number_input("Random seed", value=42, step=1)
187
 
188
- st.markdown("### Visão geral dos dados")
189
- st.write(f"Linhas: **{df_raw.shape[0]}**, Colunas: **{df_raw.shape[1]}**")
190
- st.dataframe(df_raw[[c for c in [target_col] + selected_feats if c in df_raw.columns]].head(10))
191
 
192
  # -----------------------------
193
- # Preparação
194
  # -----------------------------
195
- df = df_raw.dropna(subset=[target_col]).copy()
196
- y = df[target_col].astype(int)
197
- X = df[selected_feats].copy()
198
 
199
- # Tipos
200
- sel_num = [c for c in selected_feats if c in X.columns and np.issubdtype(X[c].dtype, np.number)]
201
- sel_cat = [c for c in selected_feats if c in X.columns and (X[c].dtype == "object" or X[c].dtype.name == "category")]
 
 
 
 
 
 
 
 
 
 
 
202
 
203
- pre = build_preprocessor(sel_num, sel_cat)
204
- model = get_model(model_name)
 
205
 
206
- pipe = Pipeline([
207
- ("pre", pre),
208
- ("clf", model)
209
- ])
210
 
211
  X_train, X_test, y_train, y_test = train_test_split(
212
- X, y, test_size=test_size, random_state=random_state, stratify=y
213
  )
214
 
 
 
 
 
 
 
 
215
  # -----------------------------
216
- # Treinamento
217
  # -----------------------------
218
- with st.spinner("Treinando modelo..."):
219
- pipe.fit(X_train, y_train)
 
220
 
221
- # Predições e métricas
222
- proba_test = pipe.predict_proba(X_test)[:, 1]
223
- pred_test = (proba_test >= 0.5).astype(int)
224
- auc = roc_auc_score(y_test, proba_test)
225
- acc = accuracy_score(y_test, pred_test)
226
- cm = confusion_matrix(y_test, pred_test)
227
-
228
- met1, met2 = st.columns(2)
229
- with met1:
230
- st.metric("AUC (ROC)", f"{auc:.3f}")
231
- with met2:
232
- st.metric("Acurácia", f"{acc:.3f}")
233
-
234
- st.markdown("#### Matriz de confusão")
235
- cm_df = pd.DataFrame(cm, index=["Real 0", "Real 1"], columns=["Pred 0", "Pred 1"])
236
- st.dataframe(cm_df)
237
-
238
- # Curva ROC (usando altair simples)
239
- roc_points = []
240
- fpr_list = np.linspace(0, 1, 101)
241
- # Calcular TPR para limiares aproximados
242
- from sklearn.metrics import roc_curve
243
- fpr, tpr, thr = roc_curve(y_test, proba_test)
244
- roc_data = pd.DataFrame({"fpr": fpr, "tpr": tpr})
245
- roc_chart = alt.Chart(roc_data).mark_line().encode(x="fpr:Q", y="tpr:Q").properties(
246
- height=250, width=380
247
- )
248
- diag = alt.Chart(pd.DataFrame({"x":[0,1],"y":[0,1]})).mark_line(strokeDash=[4,4]).encode(x="x", y="y")
249
- st.altair_chart(roc_chart + diag, use_container_width=True)
 
 
 
 
 
 
 
 
 
250
 
251
  # -----------------------------
252
- # Importância das variáveis
253
  # -----------------------------
254
- st.markdown("### Importância das variáveis")
255
- with st.spinner("Calculando importância (permutation importance)..."):
256
- perm = permutation_importance(pipe, X_test, y_test, n_repeats=10, random_state=42, scoring="roc_auc")
257
- # Nomear features após o preprocessamento:
258
- # Recupera nomes one-hot para categoricas
259
- ohe = pipe.named_steps["pre"].named_transformers_.get("cat")
260
- ohe_feat_names = []
261
- if ohe is not None and hasattr(ohe, "named_steps"):
262
- onehot = ohe.named_steps["onehot"]
263
- if hasattr(onehot, "get_feature_names_out"):
264
- ohe_feat_names = list(onehot.get_feature_names_out(sel_cat))
265
- # Nomes finais
266
- feat_names = sel_num + ohe_feat_names
267
- imp_df = pd.DataFrame({
268
- "feature": feat_names,
269
- "importance": perm.importances_mean[:len(feat_names)]
270
- }).sort_values("importance", ascending=False)
271
-
272
- # Para regressão logística, calcular sinal aproximado por coeficientes
273
- sign_map = {}
274
- if model_name == "Regressão Logística":
275
- # Reconstruir coeficientes no espaço expandido:
276
- # Ajusta novamente em X_train pretransformado para extrair coef
277
- X_train_proc = pipe.named_steps["pre"].fit_transform(X_train)
278
- clf = LogisticRegression(max_iter=200)
279
- clf.fit(X_train_proc, y_train)
280
- coefs = clf.coef_.ravel()
281
- # Alinha tamanho; pode haver diferença por features descartadas
282
- k = min(len(coefs), len(feat_names))
283
- for i in range(k):
284
- sign_map[feat_names[i]] = np.sign(coefs[i])
285
-
286
- imp_df["sign"] = imp_df["feature"].map(lambda f: sign_map.get(f, 0))
287
- st.dataframe(imp_df.head(15))
288
-
289
- # Chart
290
- bar = alt.Chart(imp_df.head(20)).mark_bar().encode(
291
- x=alt.X("importance:Q", title="Perm. importance (AUC)"),
292
- y=alt.Y("feature:N", sort='-x', title="Feature"),
293
- color=alt.value("#3165d4")
294
- ).properties(height=450)
295
- st.altair_chart(bar, use_container_width=True)
296
 
297
  # -----------------------------
298
  # Predição interativa
299
  # -----------------------------
300
- st.markdown("## Predição interativa (probabilidade de reclamação)")
301
- st.caption("Ajuste os valores no painel e veja a probabilidade prevista pelo modelo.")
302
 
303
- # Constrói um dicionário de entradas
304
  with st.form("pred_form"):
305
  cols = st.columns(3)
306
- inputs = {}
307
- for idx, col in enumerate(selected_feats):
308
- col_container = cols[idx % 3]
309
- with col_container:
310
  if col in sel_num:
311
- # Usa faixa baseada nos quantis do treino
312
- q1, q5, q95, q99 = X_train[col].quantile([0.01, 0.05, 0.95, 0.99])
313
- val = st.number_input(
314
- f"{col}",
315
- value=float(np.nan_to_num(X_train[col].median(), nan=0.0)),
316
  help=f"Faixa típica ~ {q5:.2f}–{q95:.2f} (1–99%: {q1:.2f}–{q99:.2f})"
317
  )
318
- inputs[col] = val
319
  else:
320
- opts = sorted([str(x) for x in X_train[col].dropna().unique().tolist()])[:30]
321
- default = opts[0] if opts else ""
322
- val = st.selectbox(f"{col}", options=opts if opts else [""], index=0 if opts else 0)
323
- inputs[col] = val
324
- submitted = st.form_submit_button("Calcular probabilidade")
325
 
326
  if submitted:
327
- x_new = pd.DataFrame([inputs])
328
- prob = pipe.predict_proba(x_new)[0, 1]
329
- st.success(f"Probabilidade de registrar reclamação (Complain=1): **{prob:.2%}**")
330
-
331
- # -----------------------------
332
- # Inferência estatística (Logística)
333
- # -----------------------------
334
- st.markdown("## Inferência estatística (para mitigação)")
335
- st.caption("Quando o modelo selecionado é Regressão Logística, mostramos *odds ratios* com IC 95% (explicabilidade estatística).")
336
-
337
- if model_name == "Regressão Logística":
338
- try:
339
- # Reconstruir design matrix com OHE + padronização (para statsmodels, manter padronização ajuda numérica)
340
- pre_fit = pipe.named_steps["pre"].fit(X_train, y_train)
341
- X_train_proc = pre_fit.transform(X_train)
342
- feature_names = sel_num + (
343
- list(pre_fit.named_transformers_["cat"].named_steps["onehot"].get_feature_names_out(sel_cat))
344
- if sel_cat else []
345
- )
346
- X_sm = sm.add_constant(pd.DataFrame(X_train_proc, columns=feature_names))
347
- y_sm = y_train.values
348
- logit = sm.Logit(y_sm, X_sm).fit(disp=False)
349
- or_table = coefficient_table_for_logit(logit, feature_names)
350
- st.dataframe(or_table[["feature", "odds_ratio", "or_ci_low", "or_ci_high"]].round(3))
351
-
352
- st.info(
353
- "Interpretação: valores de *odds ratio* > 1 aumentam a chance de reclamação; "
354
- "< 1 reduzem. Use os IC para priorizar intervenções mais robustas."
355
- )
356
- except Exception as e:
357
- st.warning(f"Não foi possível calcular os intervalos de confiança: {e}")
358
-
359
- # -----------------------------
360
- # Item (c): Definição & Seleção de Modelos
361
- # -----------------------------
362
- st.markdown("## Item (c) — Definição & Seleção de Modelos")
363
- st.write("""
364
- **Regressão Logística** foi escolhida por sua interpretabilidade (odds ratios) e por modelar diretamente a probabilidade de `Complain=1`.
365
- Em paralelo, **Random Forest** foi incluída como baseline não linear robusto a interações e efeitos não lineares. A escolha final pode ser
366
- guiada por **AUC/ROC**, **acurácia** e capacidade de **explicação** necessária ao negócio. Para variáveis mistas (numéricas/categóricas),
367
- aplicamos *imputação*, *padronização* (numéricas) e *one-hot* (categóricas) para garantir comparabilidade e estabilidade do treinamento.
368
- """)
369
 
370
  # -----------------------------
371
- # Item (e): Recomendações estratégicas
372
  # -----------------------------
373
- st.markdown("## Item (e) — Recomendações para a Tomada de Decisão")
374
- recs = make_recommendations(imp_df, top_k=5)
375
- for r in recs:
376
  st.markdown("- " + r)
377
 
378
  st.markdown("---")
379
- st.caption("PPCA/UnB Tarefa 6 Modelos SupervisionadosApp em Streamlit para Hugging Face Spaces")
 
1
+ # -*- coding: utf-8 -*-
2
+ # -------------------------------------------------------------------
3
+ # App: Inferência Estatística — Customer Personality Analysis
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
11
  import pandas as pd
 
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
21
  from sklearn.pipeline import Pipeline
22
  from sklearn.impute import SimpleImputer
23
+ 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
+ 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]]:
61
+ num_cols = [c for c in df.columns if c not in exclude and np.issubdtype(df[c].dtype, np.number)]
62
+ cat_cols = [c for c in df.columns if c not in exclude and (df[c].dtype == "object" or df[c].dtype.name == "category")]
 
63
  return num_cols, cat_cols
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)
90
+ if "NumWebPurchases" in out.columns:
91
+ out["OnlineShare"] = out["NumWebPurchases"] / out["TotalPurchases"].replace(0, np.nan)
92
+ if "NumDealsPurchases" in out.columns:
93
+ out["PromoShare"] = out["NumDealsPurchases"] / out["TotalPurchases"].replace(0, np.nan)
94
+
95
+ # Ticket médio
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())
112
  ])
113
  cat_pipe = Pipeline([
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),
 
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)
125
+ if cat_cols:
126
+ ohe = pre.named_transformers_["cat"].named_steps["ohe"]
127
+ names.extend(list(ohe.get_feature_names_out(cat_cols)))
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,
152
+ "or_ci_low": or_ci[0].values,
153
+ "or_ci_high": or_ci[1].values
154
+ })
155
+ else:
156
+ res = sm.OLS(y.values, X_sm).fit()
157
+ conf = res.conf_int()
158
+ tbl = pd.DataFrame({
159
+ "feature": res.params.index,
160
+ "coef": res.params.values,
161
+ "std_err": res.bse.values,
162
+ "z/t": res.tvalues.values,
163
+ "p_value": res.pvalues.values,
164
+ "ci_low": conf[0].values,
165
+ "ci_high": conf[1].values
166
+ })
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)
174
+ out = []
175
+ for _, r in core.iterrows():
176
+ feat = r["feature"]
177
+ sign = np.sign(r["coef"])
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
225
+ exclude = [target_col]
226
+ 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)
 
 
 
 
 
 
 
 
 
234
 
235
  test_size = st.slider("Proporção de teste", 0.1, 0.4, 0.2, 0.05)
236
  random_state = st.number_input("Random seed", value=42, step=1)
237
 
238
+ 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 só 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%.")
295
+ cols_show = ["feature","coef","std_err","z/t","p_value","ci_low","ci_high"]
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)
307
+ acc = accuracy_score(y_test, y_pred)
308
+ c1, c2 = st.columns(2)
309
+ with c1: st.metric("AUC (ROC)", f"{auc:.3f}")
310
+ with c2: st.metric("Acurácia (0.5)", f"{acc:.3f}")
311
+
312
+ cm = confusion_matrix(y_test, y_pred)
313
+ st.markdown("**Matriz de confusão (teste)**")
314
+ st.dataframe(pd.DataFrame(cm, index=["Real 0","Real 1"], columns=["Pred 0","Pred 1"]), use_container_width=True)
315
+
316
+ fpr, tpr, _ = roc_curve(y_test, y_proba)
317
+ roc_data = pd.DataFrame({"fpr": fpr, "tpr": tpr})
318
+ roc_chart = alt.Chart(roc_data).mark_line().encode(x="fpr:Q", y="tpr:Q").properties(height=250, width=380)
319
+ diag = alt.Chart(pd.DataFrame({"x":[0,1],"y":[0,1]})).mark_line(strokeDash=[4,4]).encode(x="x", y="y")
320
+ st.altair_chart(roc_chart + diag, use_container_width=True)
321
+ else:
322
+ y_pred = pipe.predict(X_test)
323
+ r2 = r2_score(y_test, y_pred)
324
+ rmse = mean_squared_error(y_test, y_pred, squared=False)
325
+ c1, c2 = st.columns(2)
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()
335
+ eff_chart = alt.Chart(eff_df.sort_values("effect_strength", ascending=False).head(20)).mark_bar().encode(
336
+ x=alt.X("effect_strength:Q", title="|estatística t/z|"),
337
+ y=alt.Y("feature:N", sort='-x', title="Variável")
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
 
 
347
  with st.form("pred_form"):
348
  cols = st.columns(3)
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(
356
+ f"{col}", value=default_val,
 
357
  help=f"Faixa típica ~ {q5:.2f}–{q95:.2f} (1–99%: {q1:.2f}–{q99:.2f})"
358
  )
 
359
  else:
360
+ opts = sorted([str(x) for x in X_train[col].dropna().unique().tolist()])[:50]
361
+ user_inputs[col] = st.selectbox(f"{col}", options=opts if opts else [""], index=0 if opts else 0)
362
+ submitted = st.form_submit_button("Calcular")
 
 
363
 
364
  if submitted:
365
+ x_new = pd.DataFrame([user_inputs])
366
+ x_new_proc = pre_fit.transform(x_new)
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 StreamlitInferência EstatísticaDados/marketing_campaign.csv Controles na barra lateral (esquerda).")