Tesneem commited on
Commit
4d52461
·
verified ·
1 Parent(s): 96ad3da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -187
app.py CHANGED
@@ -2,7 +2,6 @@
2
  import os
3
  from datetime import date
4
  from typing import Dict, List
5
-
6
  import numpy as np
7
  import pandas as pd
8
  import plotly.graph_objects as go
@@ -14,19 +13,11 @@ st.set_page_config(page_title="Student Skill Radar", layout="wide")
14
 
15
  # ------------------- Constants -------------------
16
  SKILLS = [
17
- "Problem-Solving",
18
- "Critical Thinking",
19
- "Analytical Reasoning",
20
- "Adaptability",
21
- "Continuous Learning",
22
- "Creativity",
23
- "Communication",
24
- "Collaboration",
25
- "Community Engagement",
26
- "Emotional Intelligence",
27
- "Ethical Decision-Making",
28
- "Time Management",
29
- "Tech Aptitude",
30
  ]
31
 
32
  SKILL_GROUPS = {
@@ -43,10 +34,9 @@ SKILL_GROUPS = {
43
  "Emotional Intelligence, Ethical Decision Making": [
44
  "Emotional Intelligence", "Ethical Decision-Making"
45
  ],
46
- "Tech Aptitude": ["Tech Aptitude"],
47
  }
48
 
49
- # Map responses "source" → Likert stage
50
  SOURCE_TO_STAGE = {
51
  "onboarding_responses": "onboarding",
52
  "closing_responses": "closing",
@@ -72,22 +62,7 @@ def aggregate_groups_row(row: pd.Series) -> Dict[str, float]:
72
  for g, members in SKILL_GROUPS.items()
73
  }
74
 
75
- def summarize(records: List[dict], level: str = "student") -> pd.DataFrame:
76
- df = pd.DataFrame(records) if records else pd.DataFrame()
77
- if df.empty:
78
- return df
79
- if level == "student+source":
80
- df["label"] = df["student"].astype(str) + " — " + df["source"].astype(str)
81
- else:
82
- df["label"] = df["student"].astype(str)
83
- # groupby mean skips NaNs by default
84
- return df.groupby("label", dropna=False)[SKILLS].mean().reset_index()
85
-
86
  def df_to_grouped(df_in: pd.DataFrame) -> pd.DataFrame:
87
- """
88
- Convert a base-skill df with a 'label' column into grouped columns so it
89
- matches SKILL_GROUPS exactly (one row per label).
90
- """
91
  if df_in.empty:
92
  return df_in
93
  rows = []
@@ -105,29 +80,15 @@ def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
105
  return go.Figure()
106
 
107
  traces = []
108
- if grouped:
109
- labels = list(SKILL_GROUPS.keys())
110
- for _, r in df.iterrows():
111
- values = [0.0 if pd.isna(r.get(k)) else float(r.get(k)) for k in labels]
112
- traces.append(go.Scatterpolar(
113
- r=values + [values[0]],
114
- theta=labels + [labels[0]],
115
- name=r["label"],
116
- fill="toself",
117
- ))
118
- else:
119
- labels = SKILLS
120
- for _, r in df.iterrows():
121
- values = []
122
- for k in SKILLS:
123
- v = r.get(k, np.nan)
124
- values.append(0.0 if pd.isna(v) else float(v))
125
- traces.append(go.Scatterpolar(
126
- r=values + [values[0]],
127
- theta=labels + [labels[0]],
128
- name=r["label"],
129
- fill="toself",
130
- ))
131
 
132
  fig = go.Figure(traces)
133
  fig.update_layout(
@@ -135,21 +96,15 @@ def plot_radar(df: pd.DataFrame, grouped: bool, title: str):
135
  showlegend=True,
136
  polar=dict(
137
  radialaxis=dict(
138
- autorange=False,
139
- range=[0, 1],
140
- tick0=0,
141
- dtick=0.2,
142
- ticks="outside",
143
- showline=True,
144
- showgrid=True,
145
- visible=True,
146
  )
147
  ),
148
  margin=dict(l=30, r=30, t=60, b=30),
149
  )
150
  return fig
151
 
152
- # ------------------- Mongo Access (secrets-only) -------------------
153
  def _get_secret(name: str) -> str | None:
154
  try:
155
  val = st.secrets.get(name)
@@ -165,40 +120,21 @@ def _build_uri(db_name: str | None) -> str | None:
165
  cluster = _get_secret("MONGO_CLUSTER")
166
  if not (user and pw and cluster):
167
  return None
168
- user_q = quote_plus(user)
169
- pw_q = quote_plus(pw)
170
- db_path = f"/{db_name}" if db_name else ""
171
- return (
172
- f"mongodb+srv://{user_q}:{pw_q}@{cluster}{db_path}"
173
- f"?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
174
- )
175
 
176
  @st.cache_resource(show_spinner=False)
177
  def _client(uri: str):
178
  return MongoClient(uri, serverSelectionTimeoutMS=10000)
179
 
180
- # @st.cache_data(show_spinner=False)
181
  def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
182
  if not uri:
183
  return []
184
  try:
185
- c = _client(uri)
186
- vals = c[db][coll].distinct(field)
187
- return sorted([v for v in vals if isinstance(v, str) and v.strip()])
188
  except Exception:
189
  return []
190
 
191
- # @st.cache_data(show_spinner=False)
192
- def mongo_records(
193
- uri: str,
194
- db: str,
195
- coll: str,
196
- student: str | None,
197
- source: str | None,
198
- start: str | None,
199
- end: str | None,
200
- ) -> List[dict]:
201
- """Return flat rows with one column per skill; missing skills -> NaN (ignored in means)."""
202
  if not uri:
203
  return []
204
  q = {}
@@ -208,57 +144,32 @@ def mongo_records(
208
  q["source"] = source
209
  if start or end:
210
  q["date"] = {}
211
- if start:
212
- q["date"]["$gte"] = start
213
- if end:
214
- q["date"]["$lte"] = end
215
  try:
216
- c = _client(uri)
217
- proj = {"_id": 0, "student": 1, "source": 1, "date": 1, "skills": 1}
218
- docs = list(c[db][coll].find(q, proj))
219
  rows = []
220
  for d in docs:
221
- base = {
222
- "student": str(d.get("student", "")),
223
- "source": str(d.get("source", "")),
224
- "date": str(d.get("date", "")),
225
- }
226
- sd = d.get("skills") or {}
227
  for k in SKILLS:
228
- base[k] = to_01_or_nan(sd.get(k, np.nan))
229
  rows.append(base)
230
  return rows
231
  except Exception:
232
  return []
233
 
234
- # ---------- Likert helpers (fetch + normalize 0..1) ----------
235
  def _norm_01(v):
236
- if v is None:
237
- return None
238
  try:
239
- v = float(v)
240
  except Exception:
241
  return None
242
- return max(0.0, min(1.0, v / 5.0 if v > 1.0 else v))
243
-
244
- def mongo_get_likert_grouped(
245
- uri: str,
246
- db: str,
247
- coll: str,
248
- student: str,
249
- stage: str
250
- ) -> dict:
251
- """
252
- Returns {group_label: score_0_1} from likert_summaries for a student+stage, or {} if missing.
253
- """
254
  if not (uri and student and stage):
255
  return {}
256
  try:
257
- c = _client(uri)
258
- doc = c[db][coll].find_one(
259
- {"student_name": student, "stage": stage},
260
- {"_id": 0, "average_skill_scores": 1}
261
- )
262
  avg = (doc or {}).get("average_skill_scores") or {}
263
  return {g: _norm_01(avg.get(g)) for g in SKILL_GROUPS.keys()}
264
  except Exception:
@@ -268,107 +179,73 @@ def mongo_get_likert_grouped(
268
  st.title("📊 Student Skill Radar")
269
 
270
  with st.sidebar:
271
- st.subheader("MongoDB Settings")
272
  db_name = st.text_input("Database name", value="student_skills")
273
  coll_name = st.text_input("Collection name", value="responses_IFE_2025")
274
  summaries_coll = st.text_input("Likert summaries collection", value="likert_summaries_IFE_2025")
275
 
276
  mongo_uri = _build_uri(db_name)
277
-
278
- if not mongo_uri:
279
- st.warning("Missing MONGO_USER, MONGO_PASS, or MONGO_CLUSTER in secrets/env.")
280
- else:
281
- try:
282
- _client(mongo_uri).admin.command("ping")
283
- st.success("Connected via secrets ✅")
284
- except Exception as e:
285
- st.error(f"Mongo connection failed: {e}")
286
-
287
- # Filters
288
  students = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "student") if mongo_uri else [])
289
  sources = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "source") if mongo_uri else [])
290
 
291
  student_choice = st.selectbox("Select student", students)
292
  source_choice = st.selectbox("Select source/week", sources)
293
-
294
- c1, c2 = st.columns(2)
295
- start_dt = c1.date_input("Start date", value=None)
296
- end_dt = c2.date_input("End date", value=None)
297
-
298
- agg_level = st.selectbox("Aggregation level", ["student", "student+source"], index=0)
299
- grouped = st.toggle("Grouped skills (skill clusters)", value=True)
300
  overlay_sources = st.toggle("Overlay all sources when '(All)' selected", value=False)
301
  chart_title = st.text_input("Chart title", value="")
302
 
303
- # Convert dates to strings (YYYY-MM-DD)
304
  start_str = start_dt.strftime("%Y-%m-%d") if isinstance(start_dt, date) else None
305
  end_str = end_dt.strftime("%Y-%m-%d") if isinstance(end_dt, date) else None
306
 
307
- # ------------------- Fetch + aggregate -------------------
308
  records = mongo_records(mongo_uri, db_name, coll_name, student_choice, source_choice, start_str, end_str) if mongo_uri else []
309
  df_raw = pd.DataFrame(records) if records else pd.DataFrame()
310
 
311
- # Build label per agg_level; aggregate means across rows
312
  if not df_raw.empty:
313
- if agg_level == "student+source":
314
- df_raw["label"] = df_raw["student"].astype(str) + " — " + df_raw["source"].astype(str)
315
- else:
316
- df_raw["label"] = df_raw["student"].astype(str)
317
  df_resp = df_raw.groupby("label", dropna=False)[SKILLS].mean().reset_index()
 
 
318
  else:
319
  df_resp = pd.DataFrame()
320
 
321
- # If grouped view, convert responses to grouped columns
322
- if grouped and not df_resp.empty:
323
- df_resp = df_to_grouped(df_resp)
324
-
325
- # Merge in Likert grouped scores (average) for onboarding/closing sources
326
- df_final = df_resp.copy()
327
- if grouped and not df_final.empty and agg_level == "student+source" and summaries_coll:
328
  merged_rows = []
329
- for _, r in df_final.iterrows():
330
  label = str(r["label"])
331
- # Expect "Studentsource"
332
- if " " in label:
333
- student, src = label.split(" ", 1)
334
- stage = SOURCE_TO_STAGE.get(src.strip())
335
- else:
336
- student, src, stage = label, None, None
337
-
338
- if stage in ("onboarding", "closing") and mongo_uri:
339
- likert = mongo_get_likert_grouped(mongo_uri, db_name, summaries_coll, student=student, stage=stage)
340
- out = {"label": label}
341
- for glabel in SKILL_GROUPS.keys():
342
- resp_val = r.get(glabel, np.nan)
343
- resp_val = None if pd.isna(resp_val) else float(resp_val)
344
- likert_val = likert.get(glabel, None)
345
- if resp_val is not None and likert_val is not None:
346
- out[glabel] = (resp_val + likert_val) / 2.0 # merge rule
347
- elif resp_val is not None:
348
- out[glabel] = resp_val
349
- elif likert_val is not None:
350
- out[glabel] = likert_val
351
- else:
352
- out[glabel] = np.nan
353
- merged_rows.append(out)
354
- else:
355
- merged_rows.append(dict(r))
356
-
357
  df_final = pd.DataFrame(merged_rows, columns=["label"] + list(SKILL_GROUPS.keys()))
358
  else:
359
  df_final = df_resp
360
 
361
- # If overlay is OFF and source is (All), collapse to one trace per student by averaging grouped vals
362
- if grouped and not df_final.empty and source_choice == "(All)" and not overlay_sources:
363
- df_final["_student"] = df_final["label"].apply(lambda s: s.split(" — ", 1)[0] if " — " in str(s) else str(s))
364
- group_cols = list(SKILL_GROUPS.keys())
365
- df_final = df_final.groupby("_student", dropna=False)[group_cols].mean().reset_index()
366
  df_final = df_final.rename(columns={"_student": "label"})
367
 
368
  # ------------------- Output -------------------
369
- fig = plot_radar(df_final if not df_final.empty else pd.DataFrame(), grouped, chart_title)
370
  st.plotly_chart(fig, use_container_width=True)
371
- st.caption(f"{len(df_final)} line(s) aggregated." if not df_final.empty else "No data. Adjust filters or check Mongo connection.")
 
372
 
373
  # # app.py — Student Skill Radar (MongoDB, secrets-based, no CSV)
374
  # import os
 
2
  import os
3
  from datetime import date
4
  from typing import Dict, List
 
5
  import numpy as np
6
  import pandas as pd
7
  import plotly.graph_objects as go
 
13
 
14
  # ------------------- Constants -------------------
15
  SKILLS = [
16
+ "Problem-Solving", "Critical Thinking", "Analytical Reasoning",
17
+ "Adaptability", "Continuous Learning", "Creativity",
18
+ "Communication", "Collaboration", "Community Engagement",
19
+ "Emotional Intelligence", "Ethical Decision-Making",
20
+ "Time Management", "Tech Aptitude"
 
 
 
 
 
 
 
 
21
  ]
22
 
23
  SKILL_GROUPS = {
 
34
  "Emotional Intelligence, Ethical Decision Making": [
35
  "Emotional Intelligence", "Ethical Decision-Making"
36
  ],
37
+ "Tech Aptitude": ["Tech Aptitude"]
38
  }
39
 
 
40
  SOURCE_TO_STAGE = {
41
  "onboarding_responses": "onboarding",
42
  "closing_responses": "closing",
 
62
  for g, members in SKILL_GROUPS.items()
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
65
  def df_to_grouped(df_in: pd.DataFrame) -> pd.DataFrame:
 
 
 
 
66
  if df_in.empty:
67
  return df_in
68
  rows = []
 
80
  return go.Figure()
81
 
82
  traces = []
83
+ labels = list(SKILL_GROUPS.keys()) if grouped else SKILLS
84
+ for _, r in df.iterrows():
85
+ values = [0.0 if pd.isna(r.get(k)) else float(r.get(k)) for k in labels]
86
+ traces.append(go.Scatterpolar(
87
+ r=values + [values[0]],
88
+ theta=labels + [labels[0]],
89
+ name=r["label"],
90
+ fill="toself",
91
+ ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  fig = go.Figure(traces)
94
  fig.update_layout(
 
96
  showlegend=True,
97
  polar=dict(
98
  radialaxis=dict(
99
+ autorange=False, range=[0, 1], tick0=0, dtick=0.2,
100
+ ticks="outside", showline=True, showgrid=True, visible=True
 
 
 
 
 
 
101
  )
102
  ),
103
  margin=dict(l=30, r=30, t=60, b=30),
104
  )
105
  return fig
106
 
107
+ # ------------------- Mongo -------------------
108
  def _get_secret(name: str) -> str | None:
109
  try:
110
  val = st.secrets.get(name)
 
120
  cluster = _get_secret("MONGO_CLUSTER")
121
  if not (user and pw and cluster):
122
  return None
123
+ return f"mongodb+srv://{quote_plus(user)}:{quote_plus(pw)}@{cluster}/{db_name}?retryWrites=true&w=majority&tls=true&tlsAllowInvalidCertificates=true"
 
 
 
 
 
 
124
 
125
  @st.cache_resource(show_spinner=False)
126
  def _client(uri: str):
127
  return MongoClient(uri, serverSelectionTimeoutMS=10000)
128
 
 
129
  def mongo_distinct(uri: str, db: str, coll: str, field: str) -> List[str]:
130
  if not uri:
131
  return []
132
  try:
133
+ return sorted([v for v in _client(uri)[db][coll].distinct(field) if isinstance(v, str) and v.strip()])
 
 
134
  except Exception:
135
  return []
136
 
137
+ def mongo_records(uri: str, db: str, coll: str, student: str | None, source: str | None, start: str | None, end: str | None) -> List[dict]:
 
 
 
 
 
 
 
 
 
 
138
  if not uri:
139
  return []
140
  q = {}
 
144
  q["source"] = source
145
  if start or end:
146
  q["date"] = {}
147
+ if start: q["date"]["$gte"] = start
148
+ if end: q["date"]["$lte"] = end
 
 
149
  try:
150
+ docs = list(_client(uri)[db][coll].find(q, {"_id": 0, "student": 1, "source": 1, "skills": 1}))
 
 
151
  rows = []
152
  for d in docs:
153
+ base = {"student": str(d.get("student", "")), "source": str(d.get("source", ""))}
 
 
 
 
 
154
  for k in SKILLS:
155
+ base[k] = to_01_or_nan((d.get("skills") or {}).get(k, np.nan))
156
  rows.append(base)
157
  return rows
158
  except Exception:
159
  return []
160
 
161
+ # ---------- Likert helpers ----------
162
  def _norm_01(v):
 
 
163
  try:
164
+ return max(0.0, min(1.0, float(v) / 5.0 if float(v) > 1 else float(v)))
165
  except Exception:
166
  return None
167
+
168
+ def mongo_get_likert_grouped(uri: str, db: str, coll: str, student: str, stage: str) -> dict:
 
 
 
 
 
 
 
 
 
 
169
  if not (uri and student and stage):
170
  return {}
171
  try:
172
+ doc = _client(uri)[db][coll].find_one({"student_name": student, "stage": stage}, {"_id": 0, "average_skill_scores": 1})
 
 
 
 
173
  avg = (doc or {}).get("average_skill_scores") or {}
174
  return {g: _norm_01(avg.get(g)) for g in SKILL_GROUPS.keys()}
175
  except Exception:
 
179
  st.title("📊 Student Skill Radar")
180
 
181
  with st.sidebar:
 
182
  db_name = st.text_input("Database name", value="student_skills")
183
  coll_name = st.text_input("Collection name", value="responses_IFE_2025")
184
  summaries_coll = st.text_input("Likert summaries collection", value="likert_summaries_IFE_2025")
185
 
186
  mongo_uri = _build_uri(db_name)
 
 
 
 
 
 
 
 
 
 
 
187
  students = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "student") if mongo_uri else [])
188
  sources = ["(All)"] + (mongo_distinct(mongo_uri, db_name, coll_name, "source") if mongo_uri else [])
189
 
190
  student_choice = st.selectbox("Select student", students)
191
  source_choice = st.selectbox("Select source/week", sources)
192
+ start_dt = st.date_input("Start date", value=None)
193
+ end_dt = st.date_input("End date", value=None)
194
+ grouped = st.toggle("Grouped skills", value=True)
 
 
 
 
195
  overlay_sources = st.toggle("Overlay all sources when '(All)' selected", value=False)
196
  chart_title = st.text_input("Chart title", value="")
197
 
 
198
  start_str = start_dt.strftime("%Y-%m-%d") if isinstance(start_dt, date) else None
199
  end_str = end_dt.strftime("%Y-%m-%d") if isinstance(end_dt, date) else None
200
 
201
+ # ------------------- Fetch + merge -------------------
202
  records = mongo_records(mongo_uri, db_name, coll_name, student_choice, source_choice, start_str, end_str) if mongo_uri else []
203
  df_raw = pd.DataFrame(records) if records else pd.DataFrame()
204
 
 
205
  if not df_raw.empty:
206
+ df_raw["label"] = df_raw["student"].astype(str) + " — " + df_raw["source"].astype(str)
 
 
 
207
  df_resp = df_raw.groupby("label", dropna=False)[SKILLS].mean().reset_index()
208
+ if grouped:
209
+ df_resp = df_to_grouped(df_resp)
210
  else:
211
  df_resp = pd.DataFrame()
212
 
213
+ # Merge Likert scores
214
+ if grouped and not df_resp.empty and summaries_coll:
 
 
 
 
 
215
  merged_rows = []
216
+ for _, r in df_resp.iterrows():
217
  label = str(r["label"])
218
+ student, stage = label.split(" — ", 1) if " — " in label else (label, None)
219
+ stage = SOURCE_TO_STAGE.get(stage.strip()) if stage else None
220
+ likert = mongo_get_likert_grouped(mongo_uri, db_name, summaries_coll, student.strip(), stage) if stage in ("onboarding", "closing") else {}
221
+ out = {"label": label}
222
+ for g in SKILL_GROUPS.keys():
223
+ resp_val = None if pd.isna(r.get(g)) else float(r.get(g))
224
+ likert_val = likert.get(g, None)
225
+ if resp_val is not None and likert_val is not None:
226
+ out[g] = (resp_val + likert_val) / 2.0
227
+ elif resp_val is not None:
228
+ out[g] = resp_val
229
+ elif likert_val is not None:
230
+ out[g] = likert_val
231
+ else:
232
+ out[g] = np.nan
233
+ merged_rows.append(out)
 
 
 
 
 
 
 
 
 
 
234
  df_final = pd.DataFrame(merged_rows, columns=["label"] + list(SKILL_GROUPS.keys()))
235
  else:
236
  df_final = df_resp
237
 
238
+ # Overlay mode
239
+ if grouped and not df_final.empty and source_choice == "(All)" and overlay_sources:
240
+ df_final["_student"] = df_final["label"].apply(lambda s: s.split(" — ", 1)[0])
241
+ df_final = df_final.groupby("_student", dropna=False)[list(SKILL_GROUPS.keys())].mean().reset_index()
 
242
  df_final = df_final.rename(columns={"_student": "label"})
243
 
244
  # ------------------- Output -------------------
245
+ fig = plot_radar(df_final, grouped, chart_title)
246
  st.plotly_chart(fig, use_container_width=True)
247
+ st.caption(f"{len(df_final)} line(s) aggregated." if not df_final.empty else "No data.")
248
+
249
 
250
  # # app.py — Student Skill Radar (MongoDB, secrets-based, no CSV)
251
  # import os