Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +44 -92
src/streamlit_app.py
CHANGED
|
@@ -3,12 +3,11 @@ import numpy as np
|
|
| 3 |
import pandas as pd
|
| 4 |
import altair as alt
|
| 5 |
import streamlit as st
|
| 6 |
-
from io import BytesIO
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
st.set_page_config(page_title="Simulação Monte Carlo (Dirichlet–Multinomial)", layout="wide")
|
| 10 |
|
| 11 |
-
#
|
| 12 |
st.sidebar.title("Parâmetros da Simulação")
|
| 13 |
N_SIM = st.sidebar.number_input("Número de simulações", min_value=1000, max_value=200_000, value=10_000, step=1000)
|
| 14 |
META_APROV = st.sidebar.slider("Meta de aprovação (≥)", 0.50, 0.95, 0.80, 0.01)
|
|
@@ -17,11 +16,7 @@ ADD_K = st.sidebar.select_slider("Suavização add-k", options=[0.5, 1.0, 2.0],
|
|
| 17 |
N_MULT = st.sidebar.select_slider("Cenário do tamanho da turma (n ×)", options=[0.9, 1.0, 1.1], value=1.0)
|
| 18 |
SEED = st.sidebar.number_input("Semente aleatória", min_value=0, value=42, step=1)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
uploaded = st.sidebar.file_uploader("Carregar CSV (opcional)", type=["csv"])
|
| 22 |
-
st.sidebar.caption("Se não enviar, o app lê automaticamente **Dados/levantamentoTurmas.csv** do repositório.")
|
| 23 |
-
|
| 24 |
-
# --------------------- Funções de limpeza/modelo ---------------------
|
| 25 |
def _norm_cols(cols):
|
| 26 |
return [re.sub(r"\s+", " ", str(c)).strip().replace("%", "pct") for c in cols]
|
| 27 |
|
|
@@ -37,78 +32,54 @@ def _to_num(s):
|
|
| 37 |
errors="coerce"
|
| 38 |
)
|
| 39 |
|
| 40 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
last_err = None
|
| 42 |
-
for enc in
|
| 43 |
-
for sep in (None,
|
| 44 |
try:
|
| 45 |
-
|
| 46 |
-
df = pd.read_csv(path_or_bytes, sep=sep, engine="python", encoding=enc)
|
| 47 |
-
else:
|
| 48 |
-
path_or_bytes.seek(0)
|
| 49 |
-
df = pd.read_csv(path_or_bytes, sep=sep, engine="python", encoding=enc)
|
| 50 |
if df.shape[1] == 1 and sep is None:
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
df = pd.read_csv(path_or_bytes, sep=";", engine="python", encoding=enc)
|
| 54 |
-
else:
|
| 55 |
-
path_or_bytes.seek(0)
|
| 56 |
-
df = pd.read_csv(path_or_bytes, sep=";", engine="python", encoding=enc)
|
| 57 |
-
return df, {"sep": sep if sep is not None else "auto", "encoding": enc}
|
| 58 |
except Exception as e:
|
| 59 |
last_err = e
|
| 60 |
continue
|
| 61 |
-
return None, last_err
|
| 62 |
|
| 63 |
@st.cache_data(show_spinner=False)
|
| 64 |
-
def
|
| 65 |
-
""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
#
|
| 70 |
-
if file_obj is not None:
|
| 71 |
-
raw = file_obj.read()
|
| 72 |
-
bio = BytesIO(raw)
|
| 73 |
-
df, meta = _try_read_csv(bio)
|
| 74 |
-
if df is None:
|
| 75 |
-
return None, f"Erro ao ler o CSV enviado: {meta}"
|
| 76 |
-
source = "upload"
|
| 77 |
-
else:
|
| 78 |
-
# 2) Caminho oficial no Space
|
| 79 |
-
primary = Path("Dados/levantamentoTurmas.csv")
|
| 80 |
-
if primary.exists():
|
| 81 |
-
df, meta = _try_read_csv(str(primary))
|
| 82 |
-
if df is None:
|
| 83 |
-
return None, f"Falha ao ler {primary} ({meta})."
|
| 84 |
-
source = str(primary)
|
| 85 |
-
else:
|
| 86 |
-
# 3) Fallback raiz
|
| 87 |
-
fallback = Path("levantamentoTurmas.csv")
|
| 88 |
-
if fallback.exists():
|
| 89 |
-
df, meta = _try_read_csv(str(fallback))
|
| 90 |
-
if df is None:
|
| 91 |
-
return None, f"Falha ao ler {fallback} ({meta})."
|
| 92 |
-
source = str(fallback)
|
| 93 |
-
else:
|
| 94 |
-
return None, "Arquivo não encontrado. Coloque o CSV em **Dados/levantamentoTurmas.csv** ou faça upload."
|
| 95 |
-
|
| 96 |
-
# ---------------- Normalização ----------------
|
| 97 |
df.columns = _norm_cols(df.columns)
|
|
|
|
|
|
|
| 98 |
ren = {}
|
| 99 |
for c in df.columns:
|
| 100 |
lc = c.lower()
|
| 101 |
-
if _pick(c, [r"^turma"]):
|
| 102 |
-
elif _pick(c, [r"matriculado"]):
|
| 103 |
-
elif _pick(c, [r"\baprov"]):
|
| 104 |
-
elif _pick(c, [r"reprov"]):
|
| 105 |
-
elif _pick(c, [r"desistent|evas"]):
|
| 106 |
df = df.rename(columns=ren)
|
| 107 |
|
|
|
|
| 108 |
for c in ["Matriculados","Aprovados","Reprovados","Desistentes","pct_Aprov","pct_Reprov","pct_Desist"]:
|
| 109 |
-
if c in df.columns:
|
|
|
|
| 110 |
|
| 111 |
-
# Reconstrói contagens se
|
| 112 |
if "Aprovados" not in df.columns and "pct_Aprov" in df.columns:
|
| 113 |
df["Aprovados"] = (df["pct_Aprov"]/100 * df["Matriculados"]).round()
|
| 114 |
if "Reprovados" not in df.columns and "pct_Reprov" in df.columns:
|
|
@@ -119,7 +90,7 @@ def load_dataframe(file_obj):
|
|
| 119 |
need = ["Turma","Matriculados","Aprovados","Reprovados","Desistentes"]
|
| 120 |
miss = [c for c in need if c not in df.columns]
|
| 121 |
if miss:
|
| 122 |
-
return None, f"Colunas ausentes no CSV
|
| 123 |
|
| 124 |
base = df[need].copy()
|
| 125 |
for c in need[1:]:
|
|
@@ -136,7 +107,7 @@ def load_dataframe(file_obj):
|
|
| 136 |
).clip(lower=0)
|
| 137 |
|
| 138 |
if len(base) == 0:
|
| 139 |
-
return None,
|
| 140 |
|
| 141 |
return base.reset_index(drop=True), None
|
| 142 |
|
|
@@ -151,8 +122,8 @@ def simulate_dirichlet_multinomial(base: pd.DataFrame, n_sim: int, meta_aprov: f
|
|
| 151 |
a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"])
|
| 152 |
alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float)
|
| 153 |
|
| 154 |
-
P = rng.dirichlet(alpha, size=n_sim)
|
| 155 |
-
counts = np.vstack([rng.multinomial(n, p) for p in P])
|
| 156 |
t_ap = counts[:, 0] / n
|
| 157 |
t_dz = counts[:, 2] / n
|
| 158 |
|
|
@@ -169,8 +140,7 @@ def simulate_dirichlet_multinomial(base: pd.DataFrame, n_sim: int, meta_aprov: f
|
|
| 169 |
"P95_Desist": np.percentile(t_dz, 95),
|
| 170 |
"Prob_Meta": ((t_ap >= meta_aprov) & (t_dz <= max_evasao)).mean()
|
| 171 |
})
|
| 172 |
-
|
| 173 |
-
return out
|
| 174 |
|
| 175 |
@st.cache_data(show_spinner=False)
|
| 176 |
def sample_turma(base: pd.DataFrame, turma_label: str, n_sim: int, add_k: float, n_mult: float, seed: int):
|
|
@@ -194,13 +164,11 @@ def sample_turma(base: pd.DataFrame, turma_label: str, n_sim: int, add_k: float,
|
|
| 194 |
C = np.vstack([rng.multinomial(n, p) for p in P])
|
| 195 |
return C[:, 0] / n, C[:, 2] / n
|
| 196 |
|
| 197 |
-
#
|
| 198 |
-
file_obj = uploaded if uploaded is not None else None
|
| 199 |
-
base, err = load_dataframe(file_obj)
|
| 200 |
-
|
| 201 |
st.title("Simulação de Monte Carlo — Dirichlet–Multinomial")
|
| 202 |
-
st.caption("
|
| 203 |
|
|
|
|
| 204 |
if err:
|
| 205 |
st.error(err)
|
| 206 |
st.stop()
|
|
@@ -208,7 +176,6 @@ if err:
|
|
| 208 |
with st.expander("Ver dados utilizados (base limpa)", expanded=False):
|
| 209 |
st.dataframe(base)
|
| 210 |
|
| 211 |
-
# -------------------------- Rodar simulação --------------------------
|
| 212 |
sim_df = simulate_dirichlet_multinomial(
|
| 213 |
base=base,
|
| 214 |
n_sim=int(N_SIM),
|
|
@@ -226,7 +193,6 @@ st.dataframe(sim_df.style.format({
|
|
| 226 |
"Prob_Meta": "{:.3f}"
|
| 227 |
}))
|
| 228 |
|
| 229 |
-
# Download CSV
|
| 230 |
st.download_button(
|
| 231 |
label="Baixar resultados (CSV)",
|
| 232 |
data=sim_df.to_csv(index=False).encode("utf-8"),
|
|
@@ -234,7 +200,6 @@ st.download_button(
|
|
| 234 |
mime="text/csv"
|
| 235 |
)
|
| 236 |
|
| 237 |
-
# -------------------------- Gráfico: Prob_Meta por turma --------------------------
|
| 238 |
st.subheader("Probabilidade de bater a meta (ordenado)")
|
| 239 |
chart_prob = (
|
| 240 |
alt.Chart(sim_df.sort_values("Prob_Meta", ascending=True))
|
|
@@ -248,12 +213,10 @@ chart_prob = (
|
|
| 248 |
alt.Tooltip("Média_Aprov:Q", format=".3f"),
|
| 249 |
alt.Tooltip("Média_Desist:Q", format=".3f"),
|
| 250 |
],
|
| 251 |
-
)
|
| 252 |
-
.properties(height=400)
|
| 253 |
)
|
| 254 |
st.altair_chart(chart_prob, use_container_width=True)
|
| 255 |
|
| 256 |
-
# -------------------------- Detalhe: Histogramas de turmas --------------------------
|
| 257 |
st.subheader("Distribuições simuladas (detalhe por turma)")
|
| 258 |
col1, col2 = st.columns(2)
|
| 259 |
with col1:
|
|
@@ -266,7 +229,6 @@ t_ap, t_dz = sample_turma(base, turma_sel, int(N_SIM), float(ADD_K), float(N_MUL
|
|
| 266 |
if t_ap is None:
|
| 267 |
st.warning("Turma não encontrada após normalização.")
|
| 268 |
else:
|
| 269 |
-
# Histograma aprovação
|
| 270 |
h_ap = (
|
| 271 |
alt.Chart(pd.DataFrame({"taxa_aprov": t_ap}))
|
| 272 |
.mark_bar()
|
|
@@ -277,7 +239,6 @@ else:
|
|
| 277 |
linha_meta = alt.Chart(pd.DataFrame({"x": [META_APROV]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
|
| 278 |
st.altair_chart(h_ap + linha_meta, use_container_width=True)
|
| 279 |
|
| 280 |
-
# Histograma evasão
|
| 281 |
h_dz = (
|
| 282 |
alt.Chart(pd.DataFrame({"taxa_evasao": t_dz}))
|
| 283 |
.mark_bar()
|
|
@@ -286,13 +247,4 @@ else:
|
|
| 286 |
.properties(height=300)
|
| 287 |
)
|
| 288 |
linha_lim = alt.Chart(pd.DataFrame({"x": [MAX_EVASAO]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
|
| 289 |
-
st.altair_chart(h_dz + linha_lim, use_container_width=True)
|
| 290 |
-
|
| 291 |
-
st.markdown(
|
| 292 |
-
f"""
|
| 293 |
-
**Notas metodológicas**
|
| 294 |
-
- Modelo: \\(\\boldsymbol{{\\pi}}\\sim\\mathrm{{Dirichlet}}(A+k, R+k, D+k)\\), \\(\\mathbf{{X}}\\mid\\boldsymbol{{\\pi}}\\sim\\mathrm{{Multinomial}}(n,\\boldsymbol{{\\pi}})\\).
|
| 295 |
-
- Parâmetros atuais: add-\\(k={ADD_K}\\), \\(n\\) escalado por \\({N_MULT}\\).
|
| 296 |
-
- A **Prob_Meta** é a fração de simulações com aprovação ≥ {META_APROV:.0%} e evasão ≤ {MAX_EVASAO:.0%}.
|
| 297 |
-
"""
|
| 298 |
-
)
|
|
|
|
| 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)
|
|
|
|
| 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 |
|
|
|
|
| 32 |
errors="coerce"
|
| 33 |
)
|
| 34 |
|
| 35 |
+
def _try_read_csv_only_root(path="levantamentoTurmas.csv"):
|
| 36 |
+
"""
|
| 37 |
+
Lê exclusivamente ./levantamentoTurmas.csv na raiz do Space.
|
| 38 |
+
Tenta múltiplos separadores e encodings. Não faz upload, nem busca em outras pastas.
|
| 39 |
+
"""
|
| 40 |
+
p = Path(path)
|
| 41 |
+
if not p.exists():
|
| 42 |
+
return None, f"Arquivo esperado não encontrado: {path} (coloque na raiz do Space)."
|
| 43 |
+
|
| 44 |
last_err = None
|
| 45 |
+
for enc in ("utf-8-sig", "utf-8", "latin1"):
|
| 46 |
+
for sep in (None, ",", ";", "\t"): # None = autodetect
|
| 47 |
try:
|
| 48 |
+
df = pd.read_csv(p, sep=sep, engine="python", encoding=enc)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
if df.shape[1] == 1 and sep is None:
|
| 50 |
+
df = pd.read_csv(p, sep=";", engine="python", encoding=enc)
|
| 51 |
+
return df, {"source": str(p), "sep": sep if sep is not None else "auto", "encoding": enc}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
except Exception as e:
|
| 53 |
last_err = e
|
| 54 |
continue
|
| 55 |
+
return None, f"Falha ao ler {path}: {last_err}"
|
| 56 |
|
| 57 |
@st.cache_data(show_spinner=False)
|
| 58 |
+
def load_dataframe_root():
|
| 59 |
+
df, meta = _try_read_csv_only_root("levantamentoTurmas.csv")
|
| 60 |
+
if df is None:
|
| 61 |
+
return None, meta # mensagem de erro
|
| 62 |
+
|
| 63 |
+
# Normalização de cabeçalhos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
df.columns = _norm_cols(df.columns)
|
| 65 |
+
|
| 66 |
+
# Renomeio inteligente
|
| 67 |
ren = {}
|
| 68 |
for c in df.columns:
|
| 69 |
lc = c.lower()
|
| 70 |
+
if _pick(c, [r"^turma"]): ren[c] = "Turma"
|
| 71 |
+
elif _pick(c, [r"matriculado"]): ren[c] = "Matriculados"
|
| 72 |
+
elif _pick(c, [r"\baprov"]): ren[c] = "Aprovados" if "pct" not in lc else "pct_Aprov"
|
| 73 |
+
elif _pick(c, [r"reprov"]): ren[c] = "Reprovados" if "pct" not in lc else "pct_Reprov"
|
| 74 |
+
elif _pick(c, [r"desistent|evas"]): ren[c] = "Desistentes" if "pct" not in lc else "pct_Desist"
|
| 75 |
df = df.rename(columns=ren)
|
| 76 |
|
| 77 |
+
# Converte números/percentuais
|
| 78 |
for c in ["Matriculados","Aprovados","Reprovados","Desistentes","pct_Aprov","pct_Reprov","pct_Desist"]:
|
| 79 |
+
if c in df.columns:
|
| 80 |
+
df[c] = _to_num(df[c])
|
| 81 |
|
| 82 |
+
# Reconstrói contagens se vierem em %
|
| 83 |
if "Aprovados" not in df.columns and "pct_Aprov" in df.columns:
|
| 84 |
df["Aprovados"] = (df["pct_Aprov"]/100 * df["Matriculados"]).round()
|
| 85 |
if "Reprovados" not in df.columns and "pct_Reprov" in df.columns:
|
|
|
|
| 90 |
need = ["Turma","Matriculados","Aprovados","Reprovados","Desistentes"]
|
| 91 |
miss = [c for c in need if c not in df.columns]
|
| 92 |
if miss:
|
| 93 |
+
return None, f"Colunas ausentes no CSV raiz: {miss}"
|
| 94 |
|
| 95 |
base = df[need].copy()
|
| 96 |
for c in need[1:]:
|
|
|
|
| 107 |
).clip(lower=0)
|
| 108 |
|
| 109 |
if len(base) == 0:
|
| 110 |
+
return None, "Após limpeza, não restaram turmas válidas."
|
| 111 |
|
| 112 |
return base.reset_index(drop=True), None
|
| 113 |
|
|
|
|
| 122 |
a, rp, dz = int(r["Aprovados"]), int(r["Reprovados"]), int(r["Desistentes"])
|
| 123 |
alpha = np.array([a + add_k, rp + add_k, dz + add_k], dtype=float)
|
| 124 |
|
| 125 |
+
P = rng.dirichlet(alpha, size=n_sim)
|
| 126 |
+
counts = np.vstack([rng.multinomial(n, p) for p in P])
|
| 127 |
t_ap = counts[:, 0] / n
|
| 128 |
t_dz = counts[:, 2] / n
|
| 129 |
|
|
|
|
| 140 |
"P95_Desist": np.percentile(t_dz, 95),
|
| 141 |
"Prob_Meta": ((t_ap >= meta_aprov) & (t_dz <= max_evasao)).mean()
|
| 142 |
})
|
| 143 |
+
return pd.DataFrame(rows).sort_values("Prob_Meta", ascending=False).reset_index(drop=True)
|
|
|
|
| 144 |
|
| 145 |
@st.cache_data(show_spinner=False)
|
| 146 |
def sample_turma(base: pd.DataFrame, turma_label: str, n_sim: int, add_k: float, n_mult: float, seed: int):
|
|
|
|
| 164 |
C = np.vstack([rng.multinomial(n, p) for p in P])
|
| 165 |
return C[:, 0] / n, C[:, 2] / n
|
| 166 |
|
| 167 |
+
# ===================== App =====================
|
|
|
|
|
|
|
|
|
|
| 168 |
st.title("Simulação de Monte Carlo — Dirichlet–Multinomial")
|
| 169 |
+
st.caption("O app lê **./levantamentoTurmas.csv** (raiz do Space). Ajuste os parâmetros na lateral e simule.")
|
| 170 |
|
| 171 |
+
base, err = load_dataframe_root()
|
| 172 |
if err:
|
| 173 |
st.error(err)
|
| 174 |
st.stop()
|
|
|
|
| 176 |
with st.expander("Ver dados utilizados (base limpa)", expanded=False):
|
| 177 |
st.dataframe(base)
|
| 178 |
|
|
|
|
| 179 |
sim_df = simulate_dirichlet_multinomial(
|
| 180 |
base=base,
|
| 181 |
n_sim=int(N_SIM),
|
|
|
|
| 193 |
"Prob_Meta": "{:.3f}"
|
| 194 |
}))
|
| 195 |
|
|
|
|
| 196 |
st.download_button(
|
| 197 |
label="Baixar resultados (CSV)",
|
| 198 |
data=sim_df.to_csv(index=False).encode("utf-8"),
|
|
|
|
| 200 |
mime="text/csv"
|
| 201 |
)
|
| 202 |
|
|
|
|
| 203 |
st.subheader("Probabilidade de bater a meta (ordenado)")
|
| 204 |
chart_prob = (
|
| 205 |
alt.Chart(sim_df.sort_values("Prob_Meta", ascending=True))
|
|
|
|
| 213 |
alt.Tooltip("Média_Aprov:Q", format=".3f"),
|
| 214 |
alt.Tooltip("Média_Desist:Q", format=".3f"),
|
| 215 |
],
|
| 216 |
+
).properties(height=400)
|
|
|
|
| 217 |
)
|
| 218 |
st.altair_chart(chart_prob, use_container_width=True)
|
| 219 |
|
|
|
|
| 220 |
st.subheader("Distribuições simuladas (detalhe por turma)")
|
| 221 |
col1, col2 = st.columns(2)
|
| 222 |
with col1:
|
|
|
|
| 229 |
if t_ap is None:
|
| 230 |
st.warning("Turma não encontrada após normalização.")
|
| 231 |
else:
|
|
|
|
| 232 |
h_ap = (
|
| 233 |
alt.Chart(pd.DataFrame({"taxa_aprov": t_ap}))
|
| 234 |
.mark_bar()
|
|
|
|
| 239 |
linha_meta = alt.Chart(pd.DataFrame({"x": [META_APROV]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
|
| 240 |
st.altair_chart(h_ap + linha_meta, use_container_width=True)
|
| 241 |
|
|
|
|
| 242 |
h_dz = (
|
| 243 |
alt.Chart(pd.DataFrame({"taxa_evasao": t_dz}))
|
| 244 |
.mark_bar()
|
|
|
|
| 247 |
.properties(height=300)
|
| 248 |
)
|
| 249 |
linha_lim = alt.Chart(pd.DataFrame({"x": [MAX_EVASAO]})).mark_rule(strokeDash=[6,4]).encode(x="x:Q")
|
| 250 |
+
st.altair_chart(h_dz + linha_lim, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|