Tesneem commited on
Commit
8cbfb34
·
verified ·
1 Parent(s): b481089

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -0
app.py CHANGED
@@ -112,6 +112,54 @@ def plot_radar(df: pd.DataFrame, grouped: bool, title: str, avg_label: str = Non
112
  margin=dict(l=30, r=30, t=60, b=30),
113
  )
114
  return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  # ------------------- Mongo -------------------
116
  def _get_secret(name: str) -> str | None:
117
  try:
@@ -343,6 +391,120 @@ if not df_final.empty and source_choice == "(All)":
343
  # ------------------- Output -------------------
344
  # fig = plot_radar(df_final, grouped, chart_title)
345
  # st.plotly_chart(fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  df_plot = df_final.copy()
347
  avg_label = None
348
 
@@ -662,6 +824,36 @@ with tab_analyses:
662
  if idx:
663
  st.caption("Available analyses:")
664
  st.write(", ".join(sorted({name.title() for name in idx.keys()})))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
 
666
  # # app.py — Student Skill Radar (MongoDB, secrets-based, no CSV)
667
  # import os
 
112
  margin=dict(l=30, r=30, t=60, b=30),
113
  )
114
  return fig
115
+
116
+ def _vector_from_row(row: pd.Series, cols: list[str]) -> dict:
117
+ return {k: (None if pd.isna(row.get(k)) else float(row.get(k))) for k in cols}
118
+
119
+ def _percent_change(new: float | None, old: float | None) -> float | None:
120
+ if new is None or old is None:
121
+ return None
122
+ if old == 0:
123
+ return None # avoid div-by-zero; you can choose to show 100% if new>0
124
+ return (new - old) / old * 100.0
125
+
126
+ def _merge_resp_and_likert_vector(resp_vec: dict, likert_grouped_vec: dict | None, grouped: bool, SKILL_TO_GROUPS: dict[str, list[str]], SKILL_GROUPS: dict[str, list[str]]) -> dict:
127
+ """
128
+ Returns a merged vector:
129
+ - If grouped: keys are group labels
130
+ - If ungrouped: keys are per-skill; Likert (group) is projected to skills by averaging groups a skill belongs to
131
+ """
132
+ if likert_grouped_vec is None:
133
+ return resp_vec
134
+
135
+ if grouped:
136
+ out = {}
137
+ for g in SKILL_GROUPS.keys():
138
+ rv = resp_vec.get(g, None)
139
+ lv = likert_grouped_vec.get(g, None)
140
+ if rv is not None and lv is not None:
141
+ out[g] = (rv + lv) / 2.0
142
+ elif rv is not None:
143
+ out[g] = rv
144
+ else:
145
+ out[g] = lv
146
+ return out
147
+ else:
148
+ # project group likert to each skill
149
+ out = {}
150
+ for s in resp_vec.keys():
151
+ rv = resp_vec.get(s, None)
152
+ groups = SKILL_TO_GROUPS.get(s, [])
153
+ lik_vals = [likert_grouped_vec.get(g) for g in groups if likert_grouped_vec.get(g) is not None]
154
+ lv = float(np.mean(lik_vals)) if lik_vals else None
155
+ if rv is not None and lv is not None:
156
+ out[s] = (rv + lv) / 2.0
157
+ elif rv is not None:
158
+ out[s] = rv
159
+ else:
160
+ out[s] = lv
161
+ return out
162
+
163
  # ------------------- Mongo -------------------
164
  def _get_secret(name: str) -> str | None:
165
  try:
 
391
  # ------------------- Output -------------------
392
  # fig = plot_radar(df_final, grouped, chart_title)
393
  # st.plotly_chart(fig, use_container_width=True)
394
+ # ============== Build per-stage vectors for comparisons ==============
395
+ # Columns to use based on mode
396
+ COLS = list(SKILL_GROUPS.keys()) if grouped else SKILLS
397
+
398
+ # Helper to extract the mean vector for (student, source) from df_resp/df_final
399
+ def _mean_vector_for(student: str | None, source: str | None, use_merged: bool) -> dict:
400
+ """
401
+ use_merged=True -> read from df_final (after Likert merge)
402
+ use_merged=False -> read from df_resp (responses-only)
403
+ """
404
+ df_base = df_final if use_merged else df_resp
405
+ if df_base.empty:
406
+ return {k: None for k in COLS}
407
+
408
+ if student and source:
409
+ label = f"{student} — {source}"
410
+ sub = df_base[df_base["label"] == label]
411
+ elif student and source is None:
412
+ # combined sources row (when overlay OFF)
413
+ sub = df_base[df_base["label"] == student]
414
+ else:
415
+ # cohort average across all rows in df_base
416
+ sub = df_base
417
+
418
+ if sub.empty:
419
+ return {k: None for k in COLS}
420
+ means = sub[COLS].mean(numeric_only=True)
421
+ return {k: (None if pd.isna(means.get(k)) else float(means.get(k))) for k in COLS}
422
+
423
+ # Build mapping skill->groups (you already used this in the Likert merge)
424
+ SKILL_TO_GROUPS = {s: [g for g, members in SKILL_GROUPS.items() if s in members] for s in SKILLS}
425
+
426
+ def _likert_grouped_for(student: str, stage: str) -> dict | None:
427
+ if stage not in ("onboarding", "closing"):
428
+ return None
429
+ lg = mongo_get_likert_grouped(mongo_uri, db_name, summaries_coll, student, stage)
430
+ return lg if lg else None
431
+
432
+ def _stage_vector(student: str | None, stage: str) -> dict:
433
+ # Which sources make up this stage?
434
+ if stage == "onboarding":
435
+ srcs = ["onboarding_responses"]
436
+ elif stage == "closing":
437
+ srcs = ["closing_responses"]
438
+ elif stage == "combined_weeks":
439
+ srcs = ["week_2_responses", "week_3_responses", "closing_responses"]
440
+ else:
441
+ srcs = []
442
+
443
+ # Response-only mean across those sources
444
+ if not df_resp.empty:
445
+ if student and source_choice == "(All)":
446
+ # we may have aggregated to one row per student; compute from df_raw instead
447
+ # build per-source labels then average
448
+ rows = []
449
+ for s in srcs:
450
+ lbl = f"{student} — {s}"
451
+ sub = df_resp[df_resp["label"] == lbl]
452
+ if not sub.empty:
453
+ rows.append(sub[COLS].mean(numeric_only=True))
454
+ if rows:
455
+ m = pd.concat(rows, axis=1).mean(axis=1)
456
+ resp_vec = {k: (None if pd.isna(m.get(k)) else float(m.get(k))) for k in COLS}
457
+ else:
458
+ resp_vec = {k: None for k in COLS}
459
+ elif student and source_choice != "(All)":
460
+ # if the UI is filtered to a specific source, ignore that and recompute from df_resp
461
+ rows = []
462
+ for s in srcs:
463
+ lbl = f"{student} — {s}"
464
+ sub = df_resp[df_resp["label"] == lbl]
465
+ if not sub.empty:
466
+ rows.append(sub[COLS].mean(numeric_only=True))
467
+ if rows:
468
+ m = pd.concat(rows, axis=1).mean(axis=1)
469
+ resp_vec = {k: (None if pd.isna(m.get(k)) else float(m.get(k))) for k in COLS}
470
+ else:
471
+ resp_vec = {k: None for k in COLS}
472
+ else:
473
+ # cohort: average across all matching sources
474
+ sub = df_resp[df_resp["label"].str.contains(" — ", na=False)]
475
+ sub = sub[sub["label"].str.split(" — ").str[1].isin(srcs)]
476
+ if not sub.empty:
477
+ m = sub[COLS].mean(numeric_only=True)
478
+ resp_vec = {k: (None if pd.isna(m.get(k)) else float(m.get(k))) for k in COLS}
479
+ else:
480
+ resp_vec = {k: None for k in COLS}
481
+ else:
482
+ resp_vec = {k: None for k in COLS}
483
+
484
+ # Merge in Likert for onboarding/closing (projected to skills if ungrouped)
485
+ if student:
486
+ likert_g = _likert_grouped_for(student, "onboarding" if "onboarding_responses" in srcs else ("closing" if "closing_responses" in srcs and len(srcs)==1 else None))
487
+ else:
488
+ likert_g = None # no cohort Likert
489
+
490
+ merged = _merge_resp_and_likert_vector(resp_vec, likert_g, grouped, SKILL_TO_GROUPS, SKILL_GROUPS)
491
+ return merged
492
+
493
+ # Build the vectors we need
494
+ if student_choice != "(All)":
495
+ vec_onb = _stage_vector(student_choice, "onboarding")
496
+ vec_cls = _stage_vector(student_choice, "closing")
497
+ vec_combo = _stage_vector(student_choice, "combined_weeks")
498
+ else:
499
+ # Cohort-wide comparison
500
+ vec_onb = _stage_vector(None, "onboarding")
501
+ vec_cls = _stage_vector(None, "closing")
502
+ vec_combo = _stage_vector(None, "combined_weeks")
503
+
504
+ # Compute % deltas
505
+ pct_onb_to_cls = {k: _percent_change(vec_cls.get(k), vec_onb.get(k)) for k in COLS}
506
+ pct_onb_to_combo = {k: _percent_change(vec_combo.get(k), vec_onb.get(k)) for k in COLS}
507
+
508
  df_plot = df_final.copy()
509
  avg_label = None
510
 
 
824
  if idx:
825
  st.caption("Available analyses:")
826
  st.write(", ".join(sorted({name.title() for name in idx.keys()})))
827
+ tab_compare, = st.tabs(["📊 Comparisons"])
828
+
829
+ with tab_compare:
830
+ st.subheader("Onboarding vs Closing — % Change")
831
+ df1 = pd.DataFrame({
832
+ "Dimension": COLS,
833
+ "Onboarding": [vec_onb.get(k) for k in COLS],
834
+ "Closing": [vec_cls.get(k) for k in COLS],
835
+ "% Change": [pct_onb_to_cls.get(k) for k in COLS],
836
+ })
837
+ st.dataframe(df1.style.format({"Onboarding": "{:.2f}", "Closing": "{:.2f}", "% Change": "{:+.1f}%"}), use_container_width=True)
838
+
839
+ st.subheader("Onboarding vs (Week2+Week3+Closing) — % Change")
840
+ df2 = pd.DataFrame({
841
+ "Dimension": COLS,
842
+ "Onboarding": [vec_onb.get(k) for k in COLS],
843
+ "Weeks 2+3+Closing (combined)": [vec_combo.get(k) for k in COLS],
844
+ "% Change": [pct_onb_to_combo.get(k) for k in COLS],
845
+ })
846
+ st.dataframe(df2.style.format({"Onboarding": "{:.2f}", "Weeks 2+3+Closing (combined)": "{:.2f}", "% Change": "{:+.1f}%"}), use_container_width=True)
847
+
848
+ # Optional bar chart: % change Onboarding -> Closing
849
+ try:
850
+ fig_delta = go.Figure()
851
+ fig_delta.add_bar(x=COLS, y=[pct_onb_to_cls.get(k) if pct_onb_to_cls.get(k) is not None else 0 for k in COLS], name="%Δ Onb→Closing")
852
+ fig_delta.update_layout(title="% Change: Onboarding → Closing", xaxis_title="Dimension", yaxis_title="% change", margin=dict(l=20, r=20, t=50, b=20))
853
+ st.plotly_chart(fig_delta, use_container_width=True)
854
+ except Exception:
855
+ pass
856
+
857
 
858
  # # app.py — Student Skill Radar (MongoDB, secrets-based, no CSV)
859
  # import os