ricardoadriano commited on
Commit
00a2070
·
verified ·
1 Parent(s): 18661d8

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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
- # -------------------------- UI: Sidebar --------------------------
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
- st.sidebar.markdown("---")
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 _try_read_csv(path_or_bytes, *, try_seps=(",", ";", "\t"), try_encodings=("utf-8-sig", "utf-8", "latin1")):
 
 
 
 
 
 
 
 
41
  last_err = None
42
- for enc in try_encodings:
43
- for sep in (None,) + try_seps: # None = autodetect
44
  try:
45
- if isinstance(path_or_bytes, (str, Path)):
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
- # força ';' se veio uma coluna gigante
52
- if isinstance(path_or_bytes, (str, Path)):
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 load_dataframe(file_obj):
65
- """
66
- Prioriza: 'Dados/levantamentoTurmas.csv' no repositório.
67
- Se não houver, tenta upload (se fornecido) e depois 'levantamentoTurmas.csv' na raiz.
68
- """
69
- # 1) Upload (se houver)
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"]): ren[c] = "Turma"
102
- elif _pick(c, [r"matriculado"]): ren[c] = "Matriculados"
103
- elif _pick(c, [r"\baprov"]): ren[c] = "Aprovados" if "pct" not in lc else "pct_Aprov"
104
- elif _pick(c, [r"reprov"]): ren[c] = "Reprovados" if "pct" not in lc else "pct_Reprov"
105
- elif _pick(c, [r"desistent|evas"]): ren[c] = "Desistentes" if "pct" not in lc else "pct_Desist"
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: df[c] = _to_num(df[c])
 
110
 
111
- # Reconstrói contagens se vier em %
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 ({source}): {miss}"
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, f"Após limpeza, não restaram turmas válidas. Origem: {source}"
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) # (n_sim, 3)
155
- counts = np.vstack([rng.multinomial(n, p) for p in P]) # (n_sim, 3)
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
- out = pd.DataFrame(rows).sort_values("Prob_Meta", ascending=False).reset_index(drop=True)
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
- # -------------------------- Carrega dados --------------------------
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("Aprovação, Reprovação e Desistência por turma. Ajuste os parâmetros na lateral e simule.")
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 **./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)