Deevyankar commited on
Commit
e622de3
·
verified ·
1 Parent(s): b98be59

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -140
app.py CHANGED
@@ -44,7 +44,7 @@ def _guess_cols(df: pd.DataFrame):
44
  cols = list(df.columns)
45
  lower = {c: str(c).strip().lower() for c in cols}
46
 
47
- # marks guess: column with max % numeric
48
  best_marks, best_score = cols[0], -1
49
  for c in cols:
50
  s = _safe_numeric(df[c])
@@ -54,12 +54,12 @@ def _guess_cols(df: pd.DataFrame):
54
  best_marks = c
55
 
56
  grade_guess = next((c for c in cols if "grade" in lower[c] or "grde" in lower[c]), cols[0])
57
- student_guess = next((c for c in cols if any(k in lower[c] for k in ["student", "name", "id", "roll", "reg", "sno"])), cols[0])
58
 
 
59
  course_guess = next((c for c in cols if any(k in lower[c] for k in ["course", "module", "subject"])), None)
60
  section_guess = next((c for c in cols if any(k in lower[c] for k in ["section", "group", "batch", "class"])), None)
61
 
62
- return student_guess, best_marks, grade_guess, course_guess, section_guess
63
 
64
 
65
  def _fig_to_png_bytes(fig):
@@ -70,100 +70,24 @@ def _fig_to_png_bytes(fig):
70
  return buf
71
 
72
 
73
- # =============================
74
- # Load Excel
75
- # =============================
76
- def load_excel(file_obj):
77
- try:
78
- file_bytes = _read_file_bytes(file_obj)
79
- xls = pd.ExcelFile(io.BytesIO(file_bytes), engine="openpyxl")
80
- sheets = xls.sheet_names or []
81
- if not sheets:
82
- raise ValueError("No sheets found in this workbook.")
83
-
84
- sheet0 = sheets[0]
85
- df = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet0, engine="openpyxl")
86
- df = _drop_useless_cols(df)
87
-
88
- s_guess, m_guess, g_guess, c_guess, sec_guess = _guess_cols(df)
89
- cols = list(df.columns)
90
-
91
- # Filters (optional)
92
- course_dd = gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False, label="Course filter")
93
- section_dd = gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False, label="Section filter")
94
-
95
- if c_guess and c_guess in df.columns:
96
- course_vals = ["(all)"] + sorted(df[c_guess].astype(str).fillna("NA").unique().tolist())
97
- course_dd = gr.Dropdown(choices=course_vals, value="(all)", interactive=True, visible=True, label="Course filter")
98
-
99
- if sec_guess and sec_guess in df.columns:
100
- sec_vals = ["(all)"] + sorted(df[sec_guess].astype(str).fillna("NA").unique().tolist())
101
- section_dd = gr.Dropdown(choices=sec_vals, value="(all)", interactive=True, visible=True, label="Section filter")
102
-
103
- return (
104
- gr.Dropdown(choices=sheets, value=sheet0, interactive=True),
105
- gr.Dropdown(choices=cols, value=s_guess, interactive=True),
106
- gr.Dropdown(choices=cols, value=m_guess, interactive=True),
107
- gr.Dropdown(choices=cols, value=g_guess, interactive=True),
108
- gr.Dropdown(choices=cols, value=(c_guess or cols[0]), interactive=bool(c_guess), visible=bool(c_guess), label="Course column"),
109
- gr.Dropdown(choices=cols, value=(sec_guess or cols[0]), interactive=bool(sec_guess), visible=bool(sec_guess), label="Section column"),
110
- course_dd,
111
- section_dd,
112
- file_bytes,
113
- sheet0, # sheet_state
114
- )
115
- except Exception:
116
- return (
117
- gr.Dropdown(choices=[], value=None, interactive=False),
118
- gr.Dropdown(choices=[], value=None, interactive=False),
119
- gr.Dropdown(choices=[], value=None, interactive=False),
120
- gr.Dropdown(choices=[], value=None, interactive=False),
121
- gr.Dropdown(choices=[], value=None, interactive=False, visible=False),
122
- gr.Dropdown(choices=[], value=None, interactive=False, visible=False),
123
- gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False),
124
- gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False),
125
- None,
126
- None,
127
- )
128
-
129
-
130
- def read_sheet(sheet_name, file_bytes, course_col, section_col):
131
- if not file_bytes:
132
- raise ValueError("Upload Excel first.")
133
- df = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet_name, engine="openpyxl")
134
- df = _drop_useless_cols(df)
135
-
136
- course_dd = gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False, label="Course filter")
137
- section_dd = gr.Dropdown(choices=["(all)"], value="(all)", interactive=False, visible=False, label="Section filter")
138
-
139
- if course_col and course_col in df.columns:
140
- course_vals = ["(all)"] + sorted(df[course_col].astype(str).fillna("NA").unique().tolist())
141
- course_dd = gr.Dropdown(choices=course_vals, value="(all)", interactive=True, visible=True, label="Course filter")
142
-
143
- if section_col and section_col in df.columns:
144
- sec_vals = ["(all)"] + sorted(df[section_col].astype(str).fillna("NA").unique().tolist())
145
- section_dd = gr.Dropdown(choices=sec_vals, value="(all)", interactive=True, visible=True, label="Section filter")
146
-
147
- return course_dd, section_dd, sheet_name
148
-
149
-
150
  def apply_filters(df, course_col, section_col, course_filter, section_filter):
151
  d = df.copy()
152
- if course_col in d.columns and course_filter and course_filter != "(all)":
153
  d = d[d[course_col].astype(str).fillna("NA") == course_filter]
154
- if section_col in d.columns and section_filter and section_filter != "(all)":
155
  d = d[d[section_col].astype(str).fillna("NA") == section_filter]
156
  return d
157
 
158
 
159
  # =============================
160
- # Analytics (NO student-level tables)
161
  # =============================
162
  def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_col, course_filter, section_filter):
163
  if df is None or df.empty:
164
  raise gr.Error("Sheet is empty.")
165
 
166
  d = apply_filters(df, course_col, section_col, course_filter, section_filter).copy()
 
167
  d["_marks"] = _safe_numeric(d[marks_col]) if marks_col in d.columns else np.nan
168
  d["_grade"] = d[grade_col].astype(str).str.strip().replace({"nan": "NA"}) if grade_col in d.columns else "NA"
169
 
@@ -177,6 +101,7 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
177
  minv = float(valid["_marks"].min()) if n else 0.0
178
  maxv = float(valid["_marks"].max()) if n else 0.0
179
 
 
180
  pass_count = int((valid["_marks"] >= pass_mark).sum()) if n else 0
181
  pass_rate = (pass_count / n * 100.0) if n else 0.0
182
 
@@ -190,10 +115,11 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
190
  pct_rows.append((f"P{p}", round(float(np.percentile(valid["_marks"], p)), 2)))
191
  percentiles_df = pd.DataFrame(pct_rows, columns=["Percentile", "Marks"]) if pct_rows else pd.DataFrame()
192
 
193
- # Grade distribution + grade-to-marks mapping
194
  grade_dist = d["_grade"].value_counts(dropna=False).rename("count").to_frame().reset_index()
195
  grade_dist.columns = [grade_col, "count"]
196
 
 
197
  grade_stats = (
198
  valid.groupby(d["_grade"])["_marks"]
199
  .agg(["count", "mean", "std", "min", "median", "max"])
@@ -202,7 +128,7 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
202
  .sort_values("mean", ascending=False)
203
  )
204
 
205
- # Heaping: repeated marks
206
  heaping_df = (
207
  valid["_marks"].round(0).astype(int)
208
  .value_counts().head(12)
@@ -210,7 +136,7 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
210
  .rename(columns={"index": "Mark"})
211
  )
212
 
213
- # Outliers count (IQR)
214
  outlier_count = 0
215
  low_thr = high_thr = 0.0
216
  if n:
@@ -229,25 +155,23 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
229
  else:
230
  status = "RED"
231
 
232
- # Teacher-friendly interpretation
233
  flags = []
234
  if missing > 0:
235
- flags.append(f"{missing} missing mark(s) → verify before final approval.")
236
  if abs(skew) > 0.7:
237
- flags.append("Skewed distribution → performance is not balanced (many low or many high).")
238
  if len(heaping_df) and heaping_df["count"].iloc[0] >= max(10, 0.06 * n):
239
- flags.append("Heaping detected → many students share the same mark (rounding/marking pattern).")
240
  if outlier_count > 0:
241
- flags.append(f"{outlier_count} outlier(s) by IQR rule → check special cases.")
242
-
243
  flags_text = " | ".join(flags) if flags else "No major warning patterns detected."
244
 
245
  insight_text = (
246
- f"Overall Status: {status}. Pass rate {pass_rate:.1f}% (Pass mark {pass_mark}). "
247
- f"Avg {mean:.1f} (Std {std:.1f}); Min {minv:.1f}, Max {maxv:.1f}. "
248
- f"Skew {skew:.2f}, Kurtosis {kurt:.2f}. "
249
- f"Outliers (IQR): {outlier_count}. Missing marks: {missing}. "
250
- f"Teacher flags: {flags_text}"
251
  )
252
 
253
  kpi_df = pd.DataFrame(
@@ -267,31 +191,27 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
267
  ("Outlier low threshold (IQR)", round(low_thr, 2)),
268
  ("Outlier high threshold (IQR)", round(high_thr, 2)),
269
  ("Outlier count (IQR)", outlier_count),
270
- ("Status", status),
271
  ("Teacher insight", insight_text),
272
  ],
273
  columns=["Metric", "Value"],
274
  )
275
 
276
- # ---- Charts
277
- # Histogram
278
  fig1 = plt.figure()
279
  plt.hist(valid["_marks"].dropna(), bins=12)
280
  plt.title("Marks distribution (Histogram)")
281
  plt.xlabel("Marks")
282
  plt.ylabel("Students")
283
 
284
- # CDF
285
  fig2 = plt.figure()
286
  xs = np.sort(valid["_marks"].dropna().values) if n else np.array([])
287
  ys = np.arange(1, len(xs) + 1) / len(xs) if len(xs) else np.array([])
288
  if len(xs):
289
  plt.plot(xs, ys)
290
- plt.title("CDF (Proportion of students ≤ mark)")
291
  plt.xlabel("Marks")
292
  plt.ylabel("Proportion")
293
 
294
- # Grade bar
295
  fig3 = plt.figure()
296
  gd = grade_dist.set_index(grade_col)["count"]
297
  plt.bar(gd.index.astype(str), gd.values)
@@ -300,7 +220,6 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
300
  plt.ylabel("Count")
301
  plt.xticks(rotation=45, ha="right")
302
 
303
- # Boxplot by grade (if possible)
304
  fig4 = plt.figure()
305
  if not grade_stats.empty:
306
  order = grade_stats[grade_stats.columns[0]].tolist()
@@ -319,9 +238,7 @@ def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_co
319
  # =============================
320
  # PDF
321
  # =============================
322
- def make_pdf(kpi_df, percentiles_df, grade_dist, grade_stats, heaping_df,
323
- fig1, fig2, fig3, fig4,
324
- title="HoD Result Dashboard Report"):
325
  buf = io.BytesIO()
326
  c = canvas.Canvas(buf, pagesize=A4)
327
  width, height = A4
@@ -369,18 +286,16 @@ def make_pdf(kpi_df, percentiles_df, grade_dist, grade_stats, heaping_df,
369
  img = ImageReader(png)
370
  img_w = width - 4 * cm
371
  img_h = 7.0 * cm
372
-
373
  if y < (img_h + 3.0 * cm):
374
  c.showPage()
375
  y = height - 2 * cm
376
-
377
  c.setFont("Helvetica-Bold", 10.5)
378
  c.drawString(x, y, caption)
379
  y -= 0.5 * cm
380
  c.drawImage(img, x, y - img_h, width=img_w, height=img_h, preserveAspectRatio=True, anchor="nw")
381
  y -= (img_h + 0.7 * cm)
382
 
383
- h(title)
384
  line(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
385
 
386
  sh("1) KPI Summary (Teacher Insight)")
@@ -417,23 +332,19 @@ def generate_pdf_report(file_bytes, sheet_name, marks_col, grade_col, pass_mark,
417
  df, marks_col, grade_col, int(pass_mark), course_col, section_col, course_filter, section_filter
418
  )
419
 
420
- pdf_buf = make_pdf(
421
- kpi_df, percentiles_df, grade_dist, grade_stats, heaping_df,
422
- fig1, fig2, fig3, fig4,
423
- title="HoD Result Dashboard Report"
424
- )
425
  fname = f"dashboard_report__{sheet_name}__{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
426
  return (fname, pdf_buf.getvalue())
427
 
428
 
429
  # =============================
430
- # UI
431
  # =============================
432
  with gr.Blocks(title="HoD Result Dashboard") as demo:
433
- gr.Markdown("## 📊 HoD Result Dashboard — Clean Teacher Insights (No Student Tables)")
434
 
435
- file_state = gr.State(None)
436
- sheet_state = gr.State(None)
437
 
438
  with gr.Row():
439
  upload = gr.File(label="Upload Excel (.xlsx)", file_types=[".xlsx"])
@@ -458,12 +369,12 @@ with gr.Blocks(title="HoD Result Dashboard") as demo:
458
  kpi_table = gr.Dataframe(label="KPI Summary + Teacher Insight", interactive=False, wrap=True)
459
 
460
  with gr.Tab("Patterns"):
461
- percentiles_table = gr.Dataframe(label="Percentiles (P10/P25/P50/P75/P90)", interactive=False, wrap=True)
462
  heaping_table = gr.Dataframe(label="Mark Heaping (Top repeated marks)", interactive=False, wrap=True)
463
 
464
  with gr.Tab("Grades"):
465
  grade_dist_table = gr.Dataframe(label="Grade distribution", interactive=False, wrap=True)
466
- grade_stats_table = gr.Dataframe(label="Grade → Marks mapping (min/max/mean/median)", interactive=False, wrap=True)
467
 
468
  with gr.Tab("Charts"):
469
  with gr.Row():
@@ -477,46 +388,89 @@ with gr.Blocks(title="HoD Result Dashboard") as demo:
477
  pdf_btn = gr.Button("📄 Generate PDF Report")
478
  pdf_out = gr.File(label="Download PDF")
479
 
480
- # Events
481
- def _on_upload(file_obj):
482
- sheet_dd_u, s_guess, m_guess, g_guess, c_col, sec_col, c_filter, sec_filter, fbytes, sh = load_excel(file_obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
- # we don't need student column anymore, so ignore s_guess
485
  return (
486
- sheet_dd_u,
487
- gr.Dropdown(choices=sheet_dd_u.choices, value=sheet_dd_u.value, interactive=True),
488
- m_guess,
489
- g_guess,
490
- c_col,
491
- sec_col,
492
- c_filter,
493
- sec_filter,
494
- fbytes,
495
- sh,
496
  )
497
 
498
  upload.change(
499
- fn=load_excel,
500
  inputs=[upload],
501
  outputs=[sheet_dd, marks_col, grade_col, course_col, section_col, course_filter, section_filter, file_state, sheet_state],
502
  )
503
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  sheet_dd.change(
505
- fn=read_sheet,
506
  inputs=[sheet_dd, file_state, course_col, section_col],
507
  outputs=[course_filter, section_filter, sheet_state],
508
  )
509
 
510
- def on_refresh(file_bytes, sheet_name, m_col, g_col, pmark, c_col, sec_col, c_filter, sec_filter):
511
  if not file_bytes:
512
  raise gr.Error("Upload Excel first.")
513
  if not sheet_name:
514
  raise gr.Error("Select a sheet.")
515
-
516
  df = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet_name, engine="openpyxl")
517
  df = _drop_useless_cols(df)
518
 
519
- return compute_insights(df, m_col, g_col, int(pmark), c_col, sec_col, c_filter, sec_filter)
520
 
521
  analyze_btn.click(
522
  fn=on_refresh,
@@ -531,3 +485,4 @@ with gr.Blocks(title="HoD Result Dashboard") as demo:
531
  )
532
 
533
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
 
 
44
  cols = list(df.columns)
45
  lower = {c: str(c).strip().lower() for c in cols}
46
 
47
+ # marks guess = column with most numeric values
48
  best_marks, best_score = cols[0], -1
49
  for c in cols:
50
  s = _safe_numeric(df[c])
 
54
  best_marks = c
55
 
56
  grade_guess = next((c for c in cols if "grade" in lower[c] or "grde" in lower[c]), cols[0])
 
57
 
58
+ # optional columns
59
  course_guess = next((c for c in cols if any(k in lower[c] for k in ["course", "module", "subject"])), None)
60
  section_guess = next((c for c in cols if any(k in lower[c] for k in ["section", "group", "batch", "class"])), None)
61
 
62
+ return best_marks, grade_guess, course_guess, section_guess
63
 
64
 
65
  def _fig_to_png_bytes(fig):
 
70
  return buf
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def apply_filters(df, course_col, section_col, course_filter, section_filter):
74
  d = df.copy()
75
+ if course_col and course_col in d.columns and course_filter and course_filter != "(all)":
76
  d = d[d[course_col].astype(str).fillna("NA") == course_filter]
77
+ if section_col and section_col in d.columns and section_filter and section_filter != "(all)":
78
  d = d[d[section_col].astype(str).fillna("NA") == section_filter]
79
  return d
80
 
81
 
82
  # =============================
83
+ # Core Insights (NO student tables)
84
  # =============================
85
  def compute_insights(df, marks_col, grade_col, pass_mark, course_col, section_col, course_filter, section_filter):
86
  if df is None or df.empty:
87
  raise gr.Error("Sheet is empty.")
88
 
89
  d = apply_filters(df, course_col, section_col, course_filter, section_filter).copy()
90
+
91
  d["_marks"] = _safe_numeric(d[marks_col]) if marks_col in d.columns else np.nan
92
  d["_grade"] = d[grade_col].astype(str).str.strip().replace({"nan": "NA"}) if grade_col in d.columns else "NA"
93
 
 
101
  minv = float(valid["_marks"].min()) if n else 0.0
102
  maxv = float(valid["_marks"].max()) if n else 0.0
103
 
104
+ pass_mark = int(pass_mark)
105
  pass_count = int((valid["_marks"] >= pass_mark).sum()) if n else 0
106
  pass_rate = (pass_count / n * 100.0) if n else 0.0
107
 
 
115
  pct_rows.append((f"P{p}", round(float(np.percentile(valid["_marks"], p)), 2)))
116
  percentiles_df = pd.DataFrame(pct_rows, columns=["Percentile", "Marks"]) if pct_rows else pd.DataFrame()
117
 
118
+ # Grade distribution
119
  grade_dist = d["_grade"].value_counts(dropna=False).rename("count").to_frame().reset_index()
120
  grade_dist.columns = [grade_col, "count"]
121
 
122
+ # Grade to marks mapping
123
  grade_stats = (
124
  valid.groupby(d["_grade"])["_marks"]
125
  .agg(["count", "mean", "std", "min", "median", "max"])
 
128
  .sort_values("mean", ascending=False)
129
  )
130
 
131
+ # Mark heaping (repeated marks)
132
  heaping_df = (
133
  valid["_marks"].round(0).astype(int)
134
  .value_counts().head(12)
 
136
  .rename(columns={"index": "Mark"})
137
  )
138
 
139
+ # Outlier count (IQR)
140
  outlier_count = 0
141
  low_thr = high_thr = 0.0
142
  if n:
 
155
  else:
156
  status = "RED"
157
 
158
+ # Teacher flags
159
  flags = []
160
  if missing > 0:
161
+ flags.append(f"{missing} missing mark(s) → verify.")
162
  if abs(skew) > 0.7:
163
+ flags.append("Skewed distribution → performance not balanced.")
164
  if len(heaping_df) and heaping_df["count"].iloc[0] >= max(10, 0.06 * n):
165
+ flags.append("Heaping → many students share same mark (rounding/marking pattern).")
166
  if outlier_count > 0:
167
+ flags.append(f"{outlier_count} outlier(s) by IQR → check special cases.")
 
168
  flags_text = " | ".join(flags) if flags else "No major warning patterns detected."
169
 
170
  insight_text = (
171
+ f"Status: {status}. Pass rate {pass_rate:.1f}% (Pass mark {pass_mark}). "
172
+ f"Avg {mean:.1f} (Std {std:.1f}), Min {minv:.1f}, Max {maxv:.1f}. "
173
+ f"Skew {skew:.2f}, Kurtosis {kurt:.2f}. Outliers: {outlier_count}. Missing: {missing}. "
174
+ f"Flags: {flags_text}"
 
175
  )
176
 
177
  kpi_df = pd.DataFrame(
 
191
  ("Outlier low threshold (IQR)", round(low_thr, 2)),
192
  ("Outlier high threshold (IQR)", round(high_thr, 2)),
193
  ("Outlier count (IQR)", outlier_count),
 
194
  ("Teacher insight", insight_text),
195
  ],
196
  columns=["Metric", "Value"],
197
  )
198
 
199
+ # Charts
 
200
  fig1 = plt.figure()
201
  plt.hist(valid["_marks"].dropna(), bins=12)
202
  plt.title("Marks distribution (Histogram)")
203
  plt.xlabel("Marks")
204
  plt.ylabel("Students")
205
 
 
206
  fig2 = plt.figure()
207
  xs = np.sort(valid["_marks"].dropna().values) if n else np.array([])
208
  ys = np.arange(1, len(xs) + 1) / len(xs) if len(xs) else np.array([])
209
  if len(xs):
210
  plt.plot(xs, ys)
211
+ plt.title("CDF (Proportion ≤ mark)")
212
  plt.xlabel("Marks")
213
  plt.ylabel("Proportion")
214
 
 
215
  fig3 = plt.figure()
216
  gd = grade_dist.set_index(grade_col)["count"]
217
  plt.bar(gd.index.astype(str), gd.values)
 
220
  plt.ylabel("Count")
221
  plt.xticks(rotation=45, ha="right")
222
 
 
223
  fig4 = plt.figure()
224
  if not grade_stats.empty:
225
  order = grade_stats[grade_stats.columns[0]].tolist()
 
238
  # =============================
239
  # PDF
240
  # =============================
241
+ def make_pdf(kpi_df, percentiles_df, grade_dist, grade_stats, heaping_df, fig1, fig2, fig3, fig4):
 
 
242
  buf = io.BytesIO()
243
  c = canvas.Canvas(buf, pagesize=A4)
244
  width, height = A4
 
286
  img = ImageReader(png)
287
  img_w = width - 4 * cm
288
  img_h = 7.0 * cm
 
289
  if y < (img_h + 3.0 * cm):
290
  c.showPage()
291
  y = height - 2 * cm
 
292
  c.setFont("Helvetica-Bold", 10.5)
293
  c.drawString(x, y, caption)
294
  y -= 0.5 * cm
295
  c.drawImage(img, x, y - img_h, width=img_w, height=img_h, preserveAspectRatio=True, anchor="nw")
296
  y -= (img_h + 0.7 * cm)
297
 
298
+ h("HoD Result Dashboard Report")
299
  line(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
300
 
301
  sh("1) KPI Summary (Teacher Insight)")
 
332
  df, marks_col, grade_col, int(pass_mark), course_col, section_col, course_filter, section_filter
333
  )
334
 
335
+ pdf_buf = make_pdf(kpi_df, percentiles_df, grade_dist, grade_stats, heaping_df, fig1, fig2, fig3, fig4)
 
 
 
 
336
  fname = f"dashboard_report__{sheet_name}__{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
337
  return (fname, pdf_buf.getvalue())
338
 
339
 
340
  # =============================
341
+ # UI (IMPORTANT: outputs order is correct)
342
  # =============================
343
  with gr.Blocks(title="HoD Result Dashboard") as demo:
344
+ gr.Markdown("## 📊 HoD Result Dashboard — Teacher Insights Only (No student tables)")
345
 
346
+ file_state = gr.State(None) # bytes
347
+ sheet_state = gr.State(None) # string
348
 
349
  with gr.Row():
350
  upload = gr.File(label="Upload Excel (.xlsx)", file_types=[".xlsx"])
 
369
  kpi_table = gr.Dataframe(label="KPI Summary + Teacher Insight", interactive=False, wrap=True)
370
 
371
  with gr.Tab("Patterns"):
372
+ percentiles_table = gr.Dataframe(label="Percentiles", interactive=False, wrap=True)
373
  heaping_table = gr.Dataframe(label="Mark Heaping (Top repeated marks)", interactive=False, wrap=True)
374
 
375
  with gr.Tab("Grades"):
376
  grade_dist_table = gr.Dataframe(label="Grade distribution", interactive=False, wrap=True)
377
+ grade_stats_table = gr.Dataframe(label="Grade → Marks mapping", interactive=False, wrap=True)
378
 
379
  with gr.Tab("Charts"):
380
  with gr.Row():
 
388
  pdf_btn = gr.Button("📄 Generate PDF Report")
389
  pdf_out = gr.File(label="Download PDF")
390
 
391
+ # ---- Callbacks
392
+ def on_upload(file_obj):
393
+ file_bytes = _read_file_bytes(file_obj)
394
+ xls = pd.ExcelFile(io.BytesIO(file_bytes), engine="openpyxl")
395
+ sheets = xls.sheet_names or []
396
+ if not sheets:
397
+ raise gr.Error("No sheets found.")
398
+
399
+ sheet0 = sheets[0]
400
+ df0 = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet0, engine="openpyxl")
401
+ df0 = _drop_useless_cols(df0)
402
+
403
+ m_guess, g_guess, c_guess, s_guess = _guess_cols(df0)
404
+ cols = list(df0.columns)
405
+
406
+ # optional filter choices (based on guessed cols)
407
+ course_filter_update = gr.update(choices=["(all)"], value="(all)", visible=False, interactive=False)
408
+ section_filter_update = gr.update(choices=["(all)"], value="(all)", visible=False, interactive=False)
409
+
410
+ course_col_update = gr.update(choices=cols, value=(c_guess or cols[0]), visible=bool(c_guess), interactive=bool(c_guess))
411
+ section_col_update = gr.update(choices=cols, value=(s_guess or cols[0]), visible=bool(s_guess), interactive=bool(s_guess))
412
+
413
+ if c_guess and c_guess in df0.columns:
414
+ vals = ["(all)"] + sorted(df0[c_guess].astype(str).fillna("NA").unique().tolist())
415
+ course_filter_update = gr.update(choices=vals, value="(all)", visible=True, interactive=True)
416
+
417
+ if s_guess and s_guess in df0.columns:
418
+ vals = ["(all)"] + sorted(df0[s_guess].astype(str).fillna("NA").unique().tolist())
419
+ section_filter_update = gr.update(choices=vals, value="(all)", visible=True, interactive=True)
420
 
 
421
  return (
422
+ gr.update(choices=sheets, value=sheet0, interactive=True), # sheet_dd
423
+ gr.update(choices=cols, value=m_guess, interactive=True), # marks_col
424
+ gr.update(choices=cols, value=g_guess, interactive=True), # grade_col
425
+ course_col_update, # course_col
426
+ section_col_update, # section_col
427
+ course_filter_update, # course_filter
428
+ section_filter_update, # section_filter
429
+ file_bytes, # file_state (BYTES!)
430
+ sheet0, # sheet_state (STRING!)
 
431
  )
432
 
433
  upload.change(
434
+ fn=on_upload,
435
  inputs=[upload],
436
  outputs=[sheet_dd, marks_col, grade_col, course_col, section_col, course_filter, section_filter, file_state, sheet_state],
437
  )
438
 
439
+ def on_sheet_change(sheet_name, file_bytes, course_col_val, section_col_val):
440
+ if not file_bytes:
441
+ raise gr.Error("Upload Excel first.")
442
+ df = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet_name, engine="openpyxl")
443
+ df = _drop_useless_cols(df)
444
+
445
+ # update filter dropdown choices for this sheet (if columns exist)
446
+ cf = gr.update(choices=["(all)"], value="(all)", visible=False, interactive=False)
447
+ sf = gr.update(choices=["(all)"], value="(all)", visible=False, interactive=False)
448
+
449
+ if course_col_val and course_col_val in df.columns:
450
+ vals = ["(all)"] + sorted(df[course_col_val].astype(str).fillna("NA").unique().tolist())
451
+ cf = gr.update(choices=vals, value="(all)", visible=True, interactive=True)
452
+
453
+ if section_col_val and section_col_val in df.columns:
454
+ vals = ["(all)"] + sorted(df[section_col_val].astype(str).fillna("NA").unique().tolist())
455
+ sf = gr.update(choices=vals, value="(all)", visible=True, interactive=True)
456
+
457
+ return cf, sf, sheet_name
458
+
459
  sheet_dd.change(
460
+ fn=on_sheet_change,
461
  inputs=[sheet_dd, file_state, course_col, section_col],
462
  outputs=[course_filter, section_filter, sheet_state],
463
  )
464
 
465
+ def on_refresh(file_bytes, sheet_name, m_col, g_col, pmark, c_col, s_col, c_filter, s_filter):
466
  if not file_bytes:
467
  raise gr.Error("Upload Excel first.")
468
  if not sheet_name:
469
  raise gr.Error("Select a sheet.")
 
470
  df = pd.read_excel(io.BytesIO(file_bytes), sheet_name=sheet_name, engine="openpyxl")
471
  df = _drop_useless_cols(df)
472
 
473
+ return compute_insights(df, m_col, g_col, int(pmark), c_col, s_col, c_filter, s_filter)
474
 
475
  analyze_btn.click(
476
  fn=on_refresh,
 
485
  )
486
 
487
  demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
488
+