Spaces:
Sleeping
Sleeping
| import re | |
| import numpy as np | |
| import pandas as pd | |
| import altair as alt | |
| import streamlit as st | |
| from pathlib import Path | |
| st.set_page_config(page_title="Simulação Monte Carlo (Dirichlet–Multinomial)", layout="wide") | |
| # ===================== Sidebar: parâmetros ===================== | |
| st.sidebar.title("Parâmetros da Simulação") | |
| N_SIM = st.sidebar.number_input("Número de simulações", min_value=1000, max_value=200_000, value=10_000, step=1000) | |
| META_APROV = st.sidebar.slider("Meta de aprovação (≥)", 0.50, 0.95, 0.80, 0.01) | |
| MAX_EVASAO = st.sidebar.slider("Limite de evasão (≤)", 0.00, 0.40, 0.15, 0.01) | |
| ADD_K = st.sidebar.select_slider("Suavização add-k", options=[0.5, 1.0, 2.0], value=1.0) | |
| N_MULT = st.sidebar.select_slider("Cenário do tamanho da turma (n ×)", options=[0.9, 1.0, 1.1], value=1.0) | |
| SEED = st.sidebar.number_input("Semente aleatória", min_value=0, value=42, step=1) | |
| # ===================== Helpers ===================== | |
| def _norm_cols(cols): | |
| return [re.sub(r"\s+", " ", str(c)).strip().replace("%", "pct") for c in cols] | |
| def _pick(col, pats): | |
| return any(re.search(p, col, re.I) for p in pats) | |
| def _to_num(s): | |
| return pd.to_numeric( | |
| s.astype(str) | |
| .str.replace("%", "", regex=False) | |
| .str.replace(",", ".", regex=False) | |
| .str.strip(), | |
| errors="coerce" | |
| ) | |
| def _try_read_csv(path: Path): | |
| """Lê Dados/levantamentoTurmas.csv tentando separadores e encodings comuns.""" | |
| if not path.exists(): | |
| return None, f"Arquivo esperado não encontrado: {path}" | |
| last_err = None | |
| for enc in ("utf-8-sig", "utf-8", "latin1"): | |
| for sep in (None, ",", ";", "\t"): # None = autodetect | |
| try: | |
| df = pd.read_csv(path, sep=sep, engine="python", encoding=enc) | |
| if df.shape[1] == 1 and sep is None: | |
| df = pd.read_csv(path, sep=";", engine="python", encoding=enc) | |
| return df, {"source": str(path), "sep": sep if sep is not None else "auto", "encoding": enc} | |
| except Exception as e: | |
| last_err = e | |
| continue | |
| return None, f"Falha ao ler {path}: {last_err}" | |
| def load_dataframe_from_dados(): | |
| csv_path = Path("Dados/levantamentoTurmas.csv") | |
| df, meta = _try_read_csv(csv_path) | |
| if df is None: | |
| return None, meta # mensagem de erro | |
| # Normalização de cabeçalhos | |
| df.columns = _norm_cols(df.columns) | |
| # Renomeação inteligente | |
| ren = {} | |
| for c in df.columns: | |
| lc = c.lower() | |
| if _pick(c, [r"^turma"]): ren[c] = "Turma" | |
| elif _pick(c, [r"matriculado"]): ren[c] = "Matriculados" | |
| elif _pick(c, [r"\baprov"]): ren[c] = "Aprovados" if "pct" not in lc else "pct_Aprov" | |
| elif _pick(c, [r"reprov"]): ren[c] = "Reprovados" if "pct" not in lc else "pct_Reprov" | |
| elif _pick(c, [r"desistent|evas"]): ren[c] = "Desistentes" if "pct" not in lc else "pct_Desist" | |
| df = df.rename(columns=ren) | |
| # Converte números/percentuais | |
| for c in ["Matriculados","Aprovados","Reprovados","Desistentes","pct_Aprov","pct_Reprov","pct_Desist"]: | |
| if c in df.columns: | |
| df[c] = _to_num(df[c]) | |
| # Reconstrói contagens quando vierem apenas em % | |
| if "Aprovados" not in df.columns and "pct_Aprov" in df.columns: | |
| df["Aprovados"] = (df["pct_Aprov"]/100 * df["Matriculados"]).round() | |
| if "Reprovados" not in df.columns and "pct_Reprov" in df.columns: | |
| df["Reprovados"] = (df["pct_Reprov"]/100 * df["Matriculados"]).round() | |
| if "Desistentes" not in df.columns and "pct_Desist" in df.columns: | |
| df["Desistentes"] = (df["pct_Desist"]/100 * df["Matriculados"]).round() | |
| need = ["Turma","Matriculados","Aprovados","Reprovados","Desistentes"] | |
| miss = [c for c in need if c not in df.columns] | |
| if miss: | |
| return None, f"Colunas ausentes no CSV ({csv_path}): {miss}" | |
| base = df[need].copy() | |
| for c in need[1:]: | |
| base[c] = pd.to_numeric(base[c], errors="coerce").fillna(0).astype(int) | |
| base = base[base["Matriculados"] > 0].copy() | |
| base["Turma"] = base["Turma"].astype(str).str.strip() | |
| # Ajuste de soma | |
| soma = base[["Aprovados","Reprovados","Desistentes"]].sum(axis=1) | |
| diff = soma != base["Matriculados"] | |
| base.loc[diff, "Aprovados"] = ( | |
| base.loc[diff, "Matriculados"] - base.loc[diff, ["Reprovados","Desistentes"]].sum(axis=1) | |
| ).clip(lower=0) | |
| if len(base) == 0: | |
| return None, "Após limpeza, não restaram turmas válidas." | |
| return base.reset_index(drop=True), None | |
| def simulate_dirichlet_multinomial(base: pd.DataFrame, n_sim: int, meta_aprov: float, max_evasao: float, add_k: float, n_mult: float, seed: int): | |
| rng = np.random.default_rng(seed) | |
| rows = [] | |
| for _, r in base.iterrows(): | |
| turma = r["Turma"] | |
| n0 = int(r["Matriculados"]) | |
| n = max(1, int(round(n0 * n_mult))) | |
| a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"]) | |
| alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float) | |
| P = rng.dirichlet(alpha, size=n_sim) | |
| counts = np.vstack([rng.multinomial(n, p) for p in P]) | |
| t_ap = counts[:, 0] / n | |
| t_dz = counts[:, 2] / n | |
| rows.append({ | |
| "Turma": turma, | |
| "Matriculados": n, | |
| "Média_Aprov": t_ap.mean(), | |
| "P5_Aprov": np.percentile(t_ap, 5), | |
| "P50_Aprov": np.percentile(t_ap, 50), | |
| "P95_Aprov": np.percentile(t_ap, 95), | |
| "Média_Desist": t_dz.mean(), | |
| "P5_Desist": np.percentile(t_dz, 5), | |
| "P50_Desist": np.percentile(t_dz, 50), | |
| "P95_Desist": np.percentile(t_dz, 95), | |
| "Prob_Meta": ((t_ap >= meta_aprov) & (t_dz <= max_evasao)).mean() | |
| }) | |
| return pd.DataFrame(rows).sort_values("Prob_Meta", ascending=False).reset_index(drop=True) | |
| def sample_turma(base: pd.DataFrame, turma_label: str, n_sim: int, add_k: float, n_mult: float, seed: int): | |
| turma_label = str(turma_label).strip() | |
| m = base["Turma"] == turma_label | |
| if not m.any(): | |
| mc = base["Turma"].str.contains(re.escape(turma_label), case=False, na=False) | |
| if not mc.any(): | |
| return None, None | |
| idx = base.index[mc][0] | |
| else: | |
| idx = base.index[m][0] | |
| r = base.loc[idx] | |
| n0 = int(r["Matriculados"]) | |
| n = max(1, int(round(n0 * n_mult))) | |
| a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"]) | |
| alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float) | |
| rng = np.random.default_rng(seed) | |
| P = rng.dirichlet(alpha, size=n_sim) | |
| C = np.vstack([rng.multinomial(n, p) for p in P]) | |
| return C[:, 0] / n, C[:, 2] / n | |
| # ===================== App ===================== | |
| st.title("Simulação de Monte Carlo — Dirichlet–Multinomial") | |
| st.caption("O app lê **Dados/levantamentoTurmas.csv**. Ajuste os parâmetros na lateral e simule.") | |
| base, err = load_dataframe_from_dados() | |
| if err: | |
| st.error(err) | |
| st.stop() | |
| with st.expander("Ver dados utilizados (base limpa)", expanded=False): | |
| st.dataframe(base) | |
| sim_df = simulate_dirichlet_multinomial( | |
| base=base, | |
| n_sim=int(N_SIM), | |
| meta_aprov=float(META_APROV), | |
| max_evasao=float(MAX_EVASAO), | |
| add_k=float(ADD_K), | |
| n_mult=float(N_MULT), | |
| seed=int(SEED) | |
| ) | |
| st.subheader("Resultados por turma") | |
| st.dataframe(sim_df.style.format({ | |
| "Média_Aprov": "{:.3f}", "P5_Aprov": "{:.3f}", "P50_Aprov": "{:.3f}", "P95_Aprov": "{:.3f}", | |
| "Média_Desist": "{:.3f}", "P5_Desist": "{:.3f}", "P50_Desist": "{:.3f}", "P95_Desist": "{:.3f}", | |
| "Prob_Meta": "{:.3f}" | |
| })) | |
| st.download_button( | |
| label="Baixar resultados (CSV)", | |
| data=sim_df.to_csv(index=False).encode("utf-8"), | |
| file_name="resultados_simulacao.csv", | |
| mime="text/csv" | |
| ) | |
| st.subheader("Probabilidade de bater a meta (ordenado)") | |
| chart_prob = ( | |
| alt.Chart(sim_df.sort_values("Prob_Meta", ascending=True)) | |
| .mark_bar() | |
| .encode( | |
| x=alt.X("Prob_Meta:Q", title=f"Prob. (aprovação ≥ {META_APROV:.0%} & evasão ≤ {MAX_EVASAO:.0%})"), | |
| y=alt.Y("Turma:N", sort="-x", title="Turma"), | |
| tooltip=[ | |
| alt.Tooltip("Turma:N"), | |
| alt.Tooltip("Prob_Meta:Q", format=".3f"), | |
| alt.Tooltip("Média_Aprov:Q", format=".3f"), | |
| alt.Tooltip("Média_Desist:Q", format=".3f"), | |
| ], | |
| ).properties(height=400) | |
| ) | |
| st.altair_chart(chart_prob, use_container_width=True) | |
| st.subheader("Distribuições simuladas (detalhe por turma)") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| turma_sel = st.selectbox("Escolha uma turma", options=sim_df["Turma"].tolist(), index=0) | |
| with col2: | |
| st.write(f"Meta de aprovação ≥ **{META_APROV:.0%}** | Evasão ≤ **{MAX_EVASAO:.0%}**") | |
| st.write(f"add-k = **{ADD_K}** · n × = **{N_MULT}** · simulações = **{N_SIM}**") | |
| t_ap, t_dz = sample_turma(base, turma_sel, int(N_SIM), float(ADD_K), float(N_MULT), int(SEED)) | |
| if t_ap is None: | |
| st.warning("Turma não encontrada após normalização.") | |
| else: | |
| h_ap = ( | |
| alt.Chart(pd.DataFrame({"taxa_aprov": t_ap})) | |
| .mark_bar() | |
| .encode(x=alt.X("taxa_aprov:Q", bin=alt.Bin(maxbins=30), title="Taxa de aprovação"), | |
| y=alt.Y("count()", title="Frequência")) | |
| .properties(height=300) | |
| ) | |
| linha_meta = alt.Chart(pd.DataFrame({"x": [META_APROV]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q") | |
| st.altair_chart(h_ap + linha_meta, use_container_width=True) | |
| h_dz = ( | |
| alt.Chart(pd.DataFrame({"taxa_evasao": t_dz})) | |
| .mark_bar() | |
| .encode(x=alt.X("taxa_evasao:Q", bin=alt.Bin(maxbins=30), title="Taxa de evasão"), | |
| y=alt.Y("count()", title="Frequência")) | |
| .properties(height=300) | |
| ) | |
| linha_lim = alt.Chart(pd.DataFrame({"x": [MAX_EVASAO]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q") | |
| st.altair_chart(h_dz + linha_lim, use_container_width=True) |