ricardoadriano commited on
Commit
d5ce1f9
·
verified ·
1 Parent(s): eb89db6

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +233 -378
src/streamlit_app.py CHANGED
@@ -1,391 +1,246 @@
1
- # Dataset: Dados/marketing_campaign.csv
2
-
3
- import os
4
  import numpy as np
5
  import pandas as pd
6
- import streamlit as st
7
  import altair as alt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- from typing import List, Tuple
10
- from sklearn.model_selection import train_test_split
11
- from sklearn.compose import ColumnTransformer
12
- from sklearn.preprocessing import OneHotEncoder, StandardScaler
13
- from sklearn.pipeline import Pipeline
14
- from sklearn.impute import SimpleImputer
15
- from sklearn.metrics import (
16
- roc_auc_score, accuracy_score, confusion_matrix, roc_curve,
17
- r2_score, mean_squared_error
18
- )
19
- from sklearn.linear_model import LogisticRegression, LinearRegression
20
- import statsmodels.api as sm
21
-
22
- st.set_page_config(page_title="Inferência Estatística", layout="wide")
23
- st.title("Inferência Estatística — Reclamações de Clientes")
24
- st.caption("Escolha o **alvo** e as **variáveis explicativas** na barra lateral (esquerda) e obtenha a inferência estatística.")
25
-
26
- DATA_PATH = "Dados/marketing_campaign.csv"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # ---------- Utilidades ----------
29
  @st.cache_data(show_spinner=False)
30
- def load_csv_try(path: str) -> pd.DataFrame:
31
- """Lê CSV tentando separadores: vírgula, ponto-e-vírgula e tab."""
32
- for sep in [",", ";", "\t"]:
33
- try:
34
- df = pd.read_csv(path, sep=sep, encoding="utf-8")
35
- if sep != "\t" and df.shape[1] == 1:
36
- continue
37
- return df
38
- except Exception:
39
- continue
40
- return pd.read_csv(path, sep=None, engine="python")
41
-
42
- def split_num_cat(df: pd.DataFrame, exclude: List[str]) -> Tuple[List[str], List[str]]:
43
- num_cols = [c for c in df.columns if c not in exclude and np.issubdtype(df[c].dtype, np.number)]
44
- cat_cols = [c for c in df.columns if c not in exclude and (df[c].dtype == "object" or df[c].dtype.name == "category")]
45
- return num_cols, cat_cols
46
-
47
- def is_binary_series(s: pd.Series) -> bool:
48
- vals = pd.unique(s.dropna())
49
- return len(vals) == 2 or s.dtype == bool
50
-
51
- def coerce_numeric_series(s: pd.Series) -> pd.Series:
52
- """Tenta converter strings numéricas para float (lida com vírgula decimal)."""
53
- if np.issubdtype(s.dtype, np.number):
54
- return s.astype(float)
55
- tmp = s.astype(str).str.replace(r"[.\s]", "", regex=True).str.replace(",", ".", regex=False)
56
- return pd.to_numeric(tmp, errors="coerce")
57
-
58
- def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
59
- """Engenharia minimalista para o dataset padrão do Kaggle."""
60
- out = df.copy()
61
-
62
- # Tenure (dias desde Dt_Customer)
63
- if "Dt_Customer" in out.columns:
64
- out["Dt_Customer"] = pd.to_datetime(out["Dt_Customer"], errors="coerce", dayfirst=True)
65
- out["TenureDays"] = (pd.Timestamp("today").normalize() - out["Dt_Customer"]).dt.days
66
-
67
- # Total gasto (Mnt*)
68
- mnt_cols = [c for c in out.columns if c.startswith("Mnt")]
69
- if mnt_cols:
70
- out["TotalMnt"] = out[mnt_cols].sum(axis=1)
71
-
72
- # Compras totais e participações
73
- buy_cols = [c for c in ["NumWebPurchases", "NumCatalogPurchases", "NumStorePurchases"] if c in out.columns]
74
- if buy_cols:
75
- out["TotalPurchases"] = out[buy_cols].sum(axis=1)
76
- if "NumWebPurchases" in out.columns:
77
- out["OnlineShare"] = out["NumWebPurchases"] / out["TotalPurchases"].replace(0, np.nan)
78
- if "NumDealsPurchases" in out.columns:
79
- out["PromoShare"] = out["NumDealsPurchases"] / out["TotalPurchases"].replace(0, np.nan)
80
-
81
- # Ticket médio
82
- if "TotalMnt" in out.columns and "TotalPurchases" in out.columns:
83
- out["AvgTicket"] = out["TotalMnt"] / out["TotalPurchases"].replace(0, np.nan)
84
-
85
- # Diversidade de cesta (quantos tipos Mnt*>0)
86
- if mnt_cols:
87
- out["BasketDiversity"] = (out[mnt_cols] > 0).sum(axis=1)
88
-
89
- return out
90
-
91
- def build_preprocessor(num_cols: List[str], cat_cols: List[str]) -> ColumnTransformer:
92
- """Imputação + padronização (num) e OHE drop='first' (cat) para evitar colinearidade."""
93
- num_pipe = Pipeline([
94
- ("imp", SimpleImputer(strategy="median")),
95
- ("scaler", StandardScaler())
96
- ])
97
- cat_pipe = Pipeline([
98
- ("imp", SimpleImputer(strategy="most_frequent")),
99
- ("ohe", OneHotEncoder(handle_unknown="ignore", drop="first", sparse_output=False))
100
- ])
101
- return ColumnTransformer([
102
- ("num", num_pipe, num_cols),
103
- ("cat", cat_pipe, cat_cols)
104
- ])
105
-
106
- def get_feature_names(pre: ColumnTransformer, num_cols: List[str], cat_cols: List[str]) -> List[str]:
107
- names = list(num_cols)
108
- if cat_cols:
109
- ohe = pre.named_transformers_["cat"].named_steps["ohe"]
110
- names.extend(list(ohe.get_feature_names_out(cat_cols)))
111
- return names
112
-
113
- def fit_inference(model_type: str, X_design: pd.DataFrame, y: pd.Series):
114
- """Ajusta a inferência (statsmodels): Logit p/ binário; OLS p/ contínuo."""
115
- X_sm = sm.add_constant(X_design, has_constant="add")
116
- if model_type == "logit":
117
- res = sm.Logit(y.values, X_sm).fit(disp=False)
118
- or_vals = np.exp(res.params)
119
- or_ci = np.exp(res.conf_int())
120
- tbl = pd.DataFrame({
121
- "feature": res.params.index,
122
- "coef": res.params.values,
123
- "std_err": res.bse.values,
124
- "z/t": res.tvalues.values if hasattr(res, "tvalues") else res.tvalues,
125
- "p_value": res.pvalues.values,
126
- "ci_low": res.conf_int()[0].values,
127
- "ci_high": res.conf_int()[1].values,
128
- "odds_ratio": or_vals.values,
129
- "or_ci_low": or_ci[0].values,
130
- "or_ci_high": or_ci[1].values
131
  })
 
 
 
 
 
 
 
 
 
 
 
132
  else:
133
- res = sm.OLS(y.values, X_sm).fit()
134
- conf = res.conf_int()
135
- tbl = pd.DataFrame({
136
- "feature": res.params.index,
137
- "coef": res.params.values,
138
- "std_err": res.bse.values,
139
- "z/t": res.tvalues.values,
140
- "p_value": res.pvalues.values,
141
- "ci_low": conf[0].values,
142
- "ci_high": conf[1].values
143
- })
144
- return res, tbl
145
-
146
- def recs_from_inference(tbl: pd.DataFrame, model_type: str, k: int = 5):
147
- """Gera recomendações (item e) a partir dos efeitos significativos (p<0.05), ignorando 'const'."""
148
- df = tbl[tbl["feature"] != "const"].copy()
149
- df = df.sort_values(["p_value", "z/t"], ascending=[True, False])
150
- core = df[df["p_value"] < 0.05].head(k)
151
- out = []
152
- for _, r in core.iterrows():
153
- feat = r["feature"]
154
- sign = np.sign(r["coef"])
155
- if model_type == "logit":
156
- or_txt = f"(OR≈{r['odds_ratio']:.2f}, IC95% {r['or_ci_low']:.2f}–{r['or_ci_high']:.2f}, p={r['p_value']:.3g})"
157
- if sign > 0:
158
- out.append(f" **Reduzir exposição associada a `{feat}`** {or_txt}, pois aumento nessa variável eleva a probabilidade do alvo.")
159
- else:
160
- out.append(f" **Fortalecer fatores ligados a `{feat}`** {or_txt}, pois valores maiores reduzem a probabilidade do alvo.")
161
- else:
162
- eff = f"(β≈{r['coef']:.3g}, IC95% {r['ci_low']:.2g}–{r['ci_high']:.2g}, p={r['p_value']:.3g})"
163
- if sign > 0:
164
- out.append(f" **Mitigar o crescimento de `{feat}`** {eff}, pois contribui positivamente para o aumento do alvo.")
165
- else:
166
- out.append(f" **Aumentar `{feat}`** {eff}, pois está associado à redução do alvo.")
167
- # trilhas transversais
168
- out.append(" **Testes A/B** nas variáveis mais significativas para validar impacto causal.")
169
- out.append(" **Melhorar FCR/primeiro contato** nas causas evidenciadas pelos top fatores.")
170
- out.append(" **Feedback a Produto/Qualidade** guiado pelos efeitos com evidência estatística robusta.")
171
- return out[:k+3]
172
-
173
- # ---------- Sidebar (lado esquerdo) ----------
174
- with st.sidebar:
175
- st.header("Configuração")
176
- if not os.path.exists(DATA_PATH):
177
- st.error(f"Arquivo não encontrado: `{DATA_PATH}`. Suba o CSV em `Dados/`.")
178
- st.stop()
179
-
180
- df_raw = load_csv_try(DATA_PATH)
181
- df_eng = engineer_features(df_raw)
182
-
183
- with st.sidebar:
184
- st.markdown("**Alvo (variável dependente):**")
185
- all_cols = df_eng.columns.tolist()
186
- # Alvo padrão fixo: Response (se existir). Caso contrário, mesma lógica de fallback.
187
- if "Response" in all_cols:
188
- default_target = "Response"
189
- else:
190
- default_target = None
191
- for c in all_cols:
192
- if is_binary_series(df_eng[c]): default_target = c; break
193
- if default_target is None:
194
- for c in all_cols:
195
- if np.issubdtype(df_eng[c].dtype, np.number): default_target = c; break
196
- target_col = st.selectbox("Alvo (y)", options=all_cols, index=all_cols.index(default_target) if default_target in all_cols else 0)
197
-
198
- # Variáveis explicativas
199
- exclude = [target_col]
200
- num_cols_all, cat_cols_all = split_num_cat(df_eng, exclude=exclude)
201
-
202
- # X padrão alinhado ao Colab (só incluir se existir na base)
203
- preferred_defaults = [
204
- "Income", "Recency", "Education", "Marital_Status",
205
- "TenureDays", "TotalMnt", "TotalPurchases",
206
- "OnlineShare", "PromoShare", "AvgTicket", "BasketDiversity",
207
- "NumWebVisitsMonth"
208
- ]
209
- default_X = [c for c in preferred_defaults if c in (num_cols_all + cat_cols_all)]
210
-
211
- with st.sidebar:
212
- st.markdown("**Variáveis explicativas (X):**")
213
- # Se nada dos preferidos existir, cai no fallback antigo (algumas num + categ)
214
- if not default_X:
215
- engineered_first = [c for c in ["TenureDays","TotalMnt","TotalPurchases","OnlineShare","PromoShare","AvgTicket","BasketDiversity"] if c in num_cols_all]
216
- default_X = engineered_first + [c for c in num_cols_all if c not in engineered_first][:5] + cat_cols_all[:3]
217
-
218
- selected_feats = st.multiselect("Selecione X", options=(num_cols_all + cat_cols_all), default=default_X)
219
-
220
- test_size = st.slider("Proporção de teste", 0.1, 0.4, 0.2, 0.05)
221
- random_state = st.number_input("Random seed", value=42, step=1)
222
-
223
- if len(selected_feats) == 0:
224
- st.warning("Selecione pelo menos uma variável explicativa.")
225
  st.stop()
226
 
227
- # ---------- Amostra ----------
228
- st.markdown("### Amostra dos dados")
229
- st.dataframe(df_eng[[target_col] + selected_feats].head(12), use_container_width=True)
230
-
231
- # ---------- Preparação do alvo ----------
232
- y_raw = df_eng[target_col]
233
-
234
- # 1) tenta identificar binário diretamente
235
- is_bin = is_binary_series(y_raw)
236
-
237
- # 2) se não binário, tenta numérico (coerção segura)
238
- y_numeric_try = coerce_numeric_series(y_raw) if not is_bin else None
239
- is_numeric_ok = False
240
- if not is_bin and y_numeric_try is not None:
241
- conv_rate = y_numeric_try.notna().mean()
242
- is_numeric_ok = conv_rate >= 0.8
243
-
244
- # 3) se não binário e não numérico, vira categórico multi-classe → one-vs-rest
245
- with st.sidebar:
246
- positive_class = None
247
- if not is_bin and not is_numeric_ok:
248
- uniq_vals = sorted(pd.unique(y_raw.dropna()).tolist(), key=lambda x: str(x))
249
- st.markdown("**Alvo categórico com múltiplas classes**")
250
- positive_class = st.selectbox("Classe 'positiva' (one-vs-rest)", options=uniq_vals, index=0)
251
- st.caption("O modelo fará Logit para a classe escolhida vs. as demais.")
252
-
253
- # ---------- Montagem de y conforme os casos ----------
254
- if is_bin:
255
- if not np.issubdtype(y_raw.dtype, np.number):
256
- uniq = sorted(pd.unique(y_raw.dropna()).tolist(), key=lambda x: str(x))
257
- y = y_raw.replace({uniq[0]: 0, uniq[1]: 1}).astype(int)
258
- else:
259
- y = y_raw.astype(int)
260
- model_type = "logit"
261
- elif is_numeric_ok:
262
- y = y_numeric_try.astype(float)
263
- model_type = "ols"
264
- else:
265
- y = (y_raw == positive_class).astype(int)
266
- model_type = "logit"
267
-
268
- # Alinha df aos y válidos
269
- mask_valid = y.notna()
270
- df_model = df_eng.loc[mask_valid].copy()
271
- y = y.loc[mask_valid]
272
- X = df_model[selected_feats].copy()
273
-
274
- # ---------- Pré-processamento e treino ----------
275
- sel_num = [c for c in selected_feats if np.issubdtype(X[c].dtype, np.number)]
276
- sel_cat = [c for c in selected_feats if (X[c].dtype == "object" or X[c].dtype.name == "category")]
277
-
278
- pre = build_preprocessor(sel_num, sel_cat)
279
- quick_est = LogisticRegression(max_iter=200) if model_type == "logit" else LinearRegression()
280
- pipe = Pipeline([("pre", pre), ("est", quick_est)])
281
-
282
- X_train, X_test, y_train, y_test = train_test_split(
283
- X, y, test_size=test_size, random_state=random_state,
284
- stratify=y if model_type == "logit" else None
285
  )
286
 
287
- with st.spinner("Treinando e construindo matriz de design..."):
288
- pipe.fit(X_train, y_train)
289
- pre_fit = pipe.named_steps["pre"].fit(X_train, y_train)
290
- X_train_design = pre_fit.transform(X_train)
291
- # nomes das features após OHE
292
- ohe_names = []
293
- if sel_cat:
294
- ohe_names = list(pre_fit.named_transformers_["cat"].named_steps["ohe"].get_feature_names_out(sel_cat))
295
- feat_names = sel_num + ohe_names
296
- X_train_df = pd.DataFrame(X_train_design, columns=feat_names)
297
-
298
- # ---------- Inferência (item e) ----------
299
- st.markdown("## Inferência estatística")
300
- with st.spinner("Ajustando modelo de inferência (statsmodels)..."):
301
- res, infer_tbl = fit_inference(model_type, X_train_df, y_train)
302
-
303
- if model_type == "logit":
304
- if positive_class is not None:
305
- st.caption(f"Modelo: **Logit** (one-vs-rest). Classe positiva: **{positive_class}**.")
306
- else:
307
- st.caption("Modelo: **Logit** (alvo binário). Coeficientes em log-odds; exibimos **odds ratios** e IC 95%.")
308
- cols_show = ["feature","coef","std_err","z/t","p_value","ci_low","ci_high","odds_ratio","or_ci_low","or_ci_high"]
309
- else:
310
- st.caption("Modelo: **OLS** (alvo contínuo). Coeficientes, erros-padrão, estatística t e IC 95%.")
311
- cols_show = ["feature","coef","std_err","z/t","p_value","ci_low","ci_high"]
312
-
313
- st.dataframe(infer_tbl[cols_show].round(4), use_container_width=True)
314
-
315
- # ---------- Métricas ----------
316
- st.markdown("### Desempenho do modelo")
317
- if model_type == "logit":
318
- y_proba = pipe.predict_proba(X_test)[:, 1] if hasattr(pipe.named_steps["est"], "predict_proba") else pipe.predict(X_test)
319
- y_pred = (y_proba >= 0.5).astype(int)
320
- auc = roc_auc_score(y_test, y_proba)
321
- acc = accuracy_score(y_test, y_pred)
322
- c1, c2 = st.columns(2)
323
- with c1: st.metric("AUC (ROC)", f"{auc:.3f}")
324
- with c2: st.metric("Acurácia (0.5)", f"{acc:.3f}")
325
-
326
- cm = confusion_matrix(y_test, y_pred)
327
- st.markdown("**Matriz de confusão (teste)**")
328
- st.dataframe(pd.DataFrame(cm, index=["Real 0","Real 1"], columns=["Pred 0","Pred 1"]), use_container_width=True)
329
-
330
- fpr, tpr, _ = roc_curve(y_test, y_proba)
331
- roc_data = pd.DataFrame({"fpr": fpr, "tpr": tpr})
332
- roc_chart = alt.Chart(roc_data).mark_line().encode(x="fpr:Q", y="tpr:Q").properties(height=250, width=380)
333
- diag = alt.Chart(pd.DataFrame({"x":[0,1],"y":[0,1]})).mark_line(strokeDash=[4,4]).encode(x="x", y="y")
334
- st.altair_chart(roc_chart + diag, use_container_width=True)
335
- else:
336
- y_pred = pipe.predict(X_test)
337
- r2 = r2_score(y_test, y_pred)
338
- rmse = mean_squared_error(y_test, y_pred, squared=False)
339
- c1, c2 = st.columns(2)
340
- with c1: st.metric("R² (teste)", f"{r2:.3f}")
341
- with c2: st.metric("RMSE (teste)", f"{rmse:.3f}")
342
-
343
- # ---------- Força dos efeitos ----------
344
- st.markdown("### Força dos efeitos (|t/z|)")
345
- eff_df = infer_tbl[infer_tbl["feature"] != "const"].copy()
346
- eff_df["effect_strength"] = eff_df["z/t"].abs()
347
- eff_chart = alt.Chart(eff_df.sort_values("effect_strength", ascending=False).head(20)).mark_bar().encode(
348
- x=alt.X("effect_strength:Q", title="|estatística t/z|"),
349
- y=alt.Y("feature:N", sort='-x', title="Variável")
350
- ).properties(height=420)
351
- st.altair_chart(eff_chart, use_container_width=True)
352
-
353
- # ---------- Predição interativa ----------
354
- st.markdown("## Predição interativa")
355
- st.caption("Ajuste valores para X e veja a probabilidade (Logit) ou valor previsto (OLS).")
356
-
357
- with st.form("pred_form"):
358
- cols = st.columns(3)
359
- user_inputs = {}
360
- for i, col in enumerate(selected_feats):
361
- with cols[i % 3]:
362
- if col in [c for c in selected_feats if np.issubdtype(X[c].dtype, np.number)]:
363
- q1, q5, q95, q99 = X_train[col].quantile([0.01,0.05,0.95,0.99])
364
- default_val = float(np.nan_to_num(X_train[col].median(), nan=0.0))
365
- user_inputs[col] = st.number_input(
366
- f"{col}", value=default_val,
367
- help=f"Faixa típica ~ {q5:.2f}–{q95:.2f} (1–99%: {q1:.2f}–{q99:.2f})"
368
- )
369
- else:
370
- opts = sorted([str(x) for x in X_train[col].dropna().unique().tolist()])[:50]
371
- user_inputs[col] = st.selectbox(f"{col}", options=opts if opts else [""], index=0 if opts else 0)
372
- submitted = st.form_submit_button("Calcular")
373
-
374
- if submitted:
375
- x_new = pd.DataFrame([user_inputs])
376
- x_new_proc = pre_fit.transform(x_new)
377
- x_new_df = pd.DataFrame(x_new_proc, columns=feat_names)
378
- X_sm_new = sm.add_constant(x_new_df, has_constant="add")
379
- y_hat = float(res.predict(X_sm_new)[0])
380
- if model_type == "logit":
381
- st.success(f"Probabilidade prevista do alvo: **{y_hat:.2%}**")
382
- else:
383
- st.success(f"Valor previsto do alvo: **{y_hat:.4g}**")
384
-
385
- # ---------- Recomendações (item e) ----------
386
- st.markdown("## Recomendações estratégicas (Item e)")
387
- for r in recs_from_inference(infer_tbl, model_type=model_type, k=5):
388
- st.markdown("- " + r)
389
 
390
- st.markdown("---")
391
- st.caption("Controles na barra lateral (esquerda) • Dados: `Dados/marketing_campaign.csv` • Inferência conforme item (e).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
 
 
2
  import numpy as np
3
  import pandas as pd
 
4
  import altair as alt
5
+ import streamlit as st
6
+ from pathlib import Path
7
+
8
+ st.set_page_config(page_title="Simulação Monte Carlo (Dirichlet–Multinomial)", layout="wide")
9
+
10
+ # ===================== Sidebar: parâmetros =====================
11
+ st.sidebar.title("Parâmetros da Simulação")
12
+ N_SIM = st.sidebar.number_input("Número de simulações", min_value=1000, max_value=200_000, value=10_000, step=1000)
13
+ META_APROV = st.sidebar.slider("Meta de aprovação (≥)", 0.50, 0.95, 0.80, 0.01)
14
+ MAX_EVASAO = st.sidebar.slider("Limite de evasão (≤)", 0.00, 0.40, 0.15, 0.01)
15
+ ADD_K = st.sidebar.select_slider("Suavização add-k", options=[0.5, 1.0, 2.0], value=1.0)
16
+ N_MULT = st.sidebar.select_slider("Cenário do tamanho da turma (n ×)", options=[0.9, 1.0, 1.1], value=1.0)
17
+ SEED = st.sidebar.number_input("Semente aleatória", min_value=0, value=42, step=1)
18
+
19
+ # ===================== Helpers =====================
20
+ def _norm_cols(cols):
21
+ return [re.sub(r"\s+", " ", str(c)).strip().replace("%", "pct") for c in cols]
22
+
23
+ def _pick(col, pats):
24
+ return any(re.search(p, col, re.I) for p in pats)
25
+
26
+ def _to_num(s):
27
+ return pd.to_numeric(
28
+ s.astype(str)
29
+ .str.replace("%", "", regex=False)
30
+ .str.replace(",", ".", regex=False)
31
+ .str.strip(),
32
+ errors="coerce"
33
+ )
34
+
35
+ def _try_read_csv(path: Path):
36
+ """Lê Dados/levantamentoTurmas.csv tentando separadores e encodings comuns."""
37
+ if not path.exists():
38
+ return None, f"Arquivo esperado não encontrado: {path}"
39
+ last_err = None
40
+ for enc in ("utf-8-sig", "utf-8", "latin1"):
41
+ for sep in (None, ",", ";", "\t"): # None = autodetect
42
+ try:
43
+ df = pd.read_csv(path, sep=sep, engine="python", encoding=enc)
44
+ if df.shape[1] == 1 and sep is None:
45
+ df = pd.read_csv(path, sep=";", engine="python", encoding=enc)
46
+ return df, {"source": str(path), "sep": sep if sep is not None else "auto", "encoding": enc}
47
+ except Exception as e:
48
+ last_err = e
49
+ continue
50
+ return None, f"Falha ao ler {path}: {last_err}"
51
 
52
+ @st.cache_data(show_spinner=False)
53
+ def load_dataframe_from_dados():
54
+ csv_path = Path("Dados/levantamentoTurmas.csv")
55
+ df, meta = _try_read_csv(csv_path)
56
+ if df is None:
57
+ return None, meta # mensagem de erro
58
+
59
+ # Normalização de cabeçalhos
60
+ df.columns = _norm_cols(df.columns)
61
+
62
+ # Renomeação inteligente
63
+ ren = {}
64
+ for c in df.columns:
65
+ lc = c.lower()
66
+ if _pick(c, [r"^turma"]): ren[c] = "Turma"
67
+ elif _pick(c, [r"matriculado"]): ren[c] = "Matriculados"
68
+ elif _pick(c, [r"\baprov"]): ren[c] = "Aprovados" if "pct" not in lc else "pct_Aprov"
69
+ elif _pick(c, [r"reprov"]): ren[c] = "Reprovados" if "pct" not in lc else "pct_Reprov"
70
+ elif _pick(c, [r"desistent|evas"]): ren[c] = "Desistentes" if "pct" not in lc else "pct_Desist"
71
+ df = df.rename(columns=ren)
72
+
73
+ # Converte números/percentuais
74
+ for c in ["Matriculados","Aprovados","Reprovados","Desistentes","pct_Aprov","pct_Reprov","pct_Desist"]:
75
+ if c in df.columns:
76
+ df[c] = _to_num(df[c])
77
+
78
+ # Reconstrói contagens quando vierem apenas em %
79
+ if "Aprovados" not in df.columns and "pct_Aprov" in df.columns:
80
+ df["Aprovados"] = (df["pct_Aprov"]/100 * df["Matriculados"]).round()
81
+ if "Reprovados" not in df.columns and "pct_Reprov" in df.columns:
82
+ df["Reprovados"] = (df["pct_Reprov"]/100 * df["Matriculados"]).round()
83
+ if "Desistentes" not in df.columns and "pct_Desist" in df.columns:
84
+ df["Desistentes"] = (df["pct_Desist"]/100 * df["Matriculados"]).round()
85
+
86
+ need = ["Turma","Matriculados","Aprovados","Reprovados","Desistentes"]
87
+ miss = [c for c in need if c not in df.columns]
88
+ if miss:
89
+ return None, f"Colunas ausentes no CSV ({csv_path}): {miss}"
90
+
91
+ base = df[need].copy()
92
+ for c in need[1:]:
93
+ base[c] = pd.to_numeric(base[c], errors="coerce").fillna(0).astype(int)
94
+
95
+ base = base[base["Matriculados"] > 0].copy()
96
+ base["Turma"] = base["Turma"].astype(str).str.strip()
97
+
98
+ # Ajuste de soma
99
+ soma = base[["Aprovados","Reprovados","Desistentes"]].sum(axis=1)
100
+ diff = soma != base["Matriculados"]
101
+ base.loc[diff, "Aprovados"] = (
102
+ base.loc[diff, "Matriculados"] - base.loc[diff, ["Reprovados","Desistentes"]].sum(axis=1)
103
+ ).clip(lower=0)
104
+
105
+ if len(base) == 0:
106
+ return None, "Após limpeza, não restaram turmas válidas."
107
+
108
+ return base.reset_index(drop=True), None
109
 
 
110
  @st.cache_data(show_spinner=False)
111
+ def simulate_dirichlet_multinomial(base: pd.DataFrame, n_sim: int, meta_aprov: float, max_evasao: float, add_k: float, n_mult: float, seed: int):
112
+ rng = np.random.default_rng(seed)
113
+ rows = []
114
+ for _, r in base.iterrows():
115
+ turma = r["Turma"]
116
+ n0 = int(r["Matriculados"])
117
+ n = max(1, int(round(n0 * n_mult)))
118
+ a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"])
119
+ alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float)
120
+
121
+ P = rng.dirichlet(alpha, size=n_sim)
122
+ counts = np.vstack([rng.multinomial(n, p) for p in P])
123
+ t_ap = counts[:, 0] / n
124
+ t_dz = counts[:, 2] / n
125
+
126
+ rows.append({
127
+ "Turma": turma,
128
+ "Matriculados": n,
129
+ "Média_Aprov": t_ap.mean(),
130
+ "P5_Aprov": np.percentile(t_ap, 5),
131
+ "P50_Aprov": np.percentile(t_ap, 50),
132
+ "P95_Aprov": np.percentile(t_ap, 95),
133
+ "Média_Desist": t_dz.mean(),
134
+ "P5_Desist": np.percentile(t_dz, 5),
135
+ "P50_Desist": np.percentile(t_dz, 50),
136
+ "P95_Desist": np.percentile(t_dz, 95),
137
+ "Prob_Meta": ((t_ap >= meta_aprov) & (t_dz <= max_evasao)).mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  })
139
+ return pd.DataFrame(rows).sort_values("Prob_Meta", ascending=False).reset_index(drop=True)
140
+
141
+ @st.cache_data(show_spinner=False)
142
+ def sample_turma(base: pd.DataFrame, turma_label: str, n_sim: int, add_k: float, n_mult: float, seed: int):
143
+ turma_label = str(turma_label).strip()
144
+ m = base["Turma"] == turma_label
145
+ if not m.any():
146
+ mc = base["Turma"].str.contains(re.escape(turma_label), case=False, na=False)
147
+ if not mc.any():
148
+ return None, None
149
+ idx = base.index[mc][0]
150
  else:
151
+ idx = base.index[m][0]
152
+
153
+ r = base.loc[idx]
154
+ n0 = int(r["Matriculados"])
155
+ n = max(1, int(round(n0 * n_mult)))
156
+ a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"])
157
+ alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float)
158
+ rng = np.random.default_rng(seed)
159
+ P = rng.dirichlet(alpha, size=n_sim)
160
+ C = np.vstack([rng.multinomial(n, p) for p in P])
161
+ return C[:, 0] / n, C[:, 2] / n
162
+
163
+ # ===================== App =====================
164
+ st.title("Simulação de Monte Carlo Dirichlet–Multinomial")
165
+ st.caption("O app **Dados/levantamentoTurmas.csv**. Ajuste os parâmetros na lateral e simule.")
166
+
167
+ base, err = load_dataframe_from_dados()
168
+ if err:
169
+ st.error(err)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  st.stop()
171
 
172
+ with st.expander("Ver dados utilizados (base limpa)", expanded=False):
173
+ st.dataframe(base)
174
+
175
+ sim_df = simulate_dirichlet_multinomial(
176
+ base=base,
177
+ n_sim=int(N_SIM),
178
+ meta_aprov=float(META_APROV),
179
+ max_evasao=float(MAX_EVASAO),
180
+ add_k=float(ADD_K),
181
+ n_mult=float(N_MULT),
182
+ seed=int(SEED)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  )
184
 
185
+ st.subheader("Resultados por turma")
186
+ st.dataframe(sim_df.style.format({
187
+ "Média_Aprov": "{:.3f}", "P5_Aprov": "{:.3f}", "P50_Aprov": "{:.3f}", "P95_Aprov": "{:.3f}",
188
+ "Média_Desist": "{:.3f}", "P5_Desist": "{:.3f}", "P50_Desist": "{:.3f}", "P95_Desist": "{:.3f}",
189
+ "Prob_Meta": "{:.3f}"
190
+ }))
191
+
192
+ st.download_button(
193
+ label="Baixar resultados (CSV)",
194
+ data=sim_df.to_csv(index=False).encode("utf-8"),
195
+ file_name="resultados_simulacao.csv",
196
+ mime="text/csv"
197
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ st.subheader("Probabilidade de bater a meta (ordenado)")
200
+ chart_prob = (
201
+ alt.Chart(sim_df.sort_values("Prob_Meta", ascending=True))
202
+ .mark_bar()
203
+ .encode(
204
+ x=alt.X("Prob_Meta:Q", title=f"Prob. (aprovação ≥ {META_APROV:.0%} & evasão ≤ {MAX_EVASAO:.0%})"),
205
+ y=alt.Y("Turma:N", sort="-x", title="Turma"),
206
+ tooltip=[
207
+ alt.Tooltip("Turma:N"),
208
+ alt.Tooltip("Prob_Meta:Q", format=".3f"),
209
+ alt.Tooltip("Média_Aprov:Q", format=".3f"),
210
+ alt.Tooltip("Média_Desist:Q", format=".3f"),
211
+ ],
212
+ ).properties(height=400)
213
+ )
214
+ st.altair_chart(chart_prob, use_container_width=True)
215
+
216
+ st.subheader("Distribuições simuladas (detalhe por turma)")
217
+ col1, col2 = st.columns(2)
218
+ with col1:
219
+ turma_sel = st.selectbox("Escolha uma turma", options=sim_df["Turma"].tolist(), index=0)
220
+ with col2:
221
+ st.write(f"Meta de aprovação ≥ **{META_APROV:.0%}** | Evasão ≤ **{MAX_EVASAO:.0%}**")
222
+ st.write(f"add-k = **{ADD_K}** · n × = **{N_MULT}** · simulações = **{N_SIM}**")
223
+
224
+ t_ap, t_dz = sample_turma(base, turma_sel, int(N_SIM), float(ADD_K), float(N_MULT), int(SEED))
225
+ if t_ap is None:
226
+ st.warning("Turma não encontrada após normalização.")
227
+ else:
228
+ h_ap = (
229
+ alt.Chart(pd.DataFrame({"taxa_aprov": t_ap}))
230
+ .mark_bar()
231
+ .encode(x=alt.X("taxa_aprov:Q", bin=alt.Bin(maxbins=30), title="Taxa de aprovação"),
232
+ y=alt.Y("count()", title="Frequência"))
233
+ .properties(height=300)
234
+ )
235
+ linha_meta = alt.Chart(pd.DataFrame({"x": [META_APROV]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
236
+ st.altair_chart(h_ap + linha_meta, use_container_width=True)
237
+
238
+ h_dz = (
239
+ alt.Chart(pd.DataFrame({"taxa_evasao": t_dz}))
240
+ .mark_bar()
241
+ .encode(x=alt.X("taxa_evasao:Q", bin=alt.Bin(maxbins=30), title="Taxa de evasão"),
242
+ y=alt.Y("count()", title="Frequência"))
243
+ .properties(height=300)
244
+ )
245
+ linha_lim = alt.Chart(pd.DataFrame({"x": [MAX_EVASAO]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
246
+ st.altair_chart(h_dz + linha_lim, use_container_width=True)