CGA_Insights / appff.py
Deevyankar's picture
Rename app.py to appff.py
74a2d47 verified
Raw
History Blame Contribute Delete
28.3 kB
# app.py — Flexible Marks Clustering Dashboard
# Supports:
# - Any combination of assessments
# - Absent values like A / Absent -> treated as 0
# - Excel/CSV upload
# - Student + Section clustering
# - Manager graphs
# - PCA plots
# - Excel report download
# - Hugging Face Spaces compatible launch
# - Shows only uploaded assessment-related outputs
import io
import tempfile
import numpy as np
import pandas as pd
import gradio as gr
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
import plotly.express as px
MAXIMA = {
"Quiz": 10.0,
"Mid": 20.0,
"Lab Report": 20.0,
"Lab Activity": 10.0,
"Final": 40.0,
}
ASSESS_COLS = ["Quiz", "Mid", "Lab Report", "Lab Activity", "Final"]
# ----------------------------
# Reading / mapping / cleaning
# ----------------------------
def _normalize_colname(c: str) -> str:
c = str(c).strip().lower()
c = c.replace("_", " ").replace("-", " ")
c = c.replace("0", "o")
c = " ".join(c.split())
return c
def _read_excel_safely(path: str) -> pd.DataFrame:
df_try = pd.read_excel(path)
if len(df_try.columns) > 0 and all(str(c).startswith("Unnamed") for c in df_try.columns):
df_try = pd.read_excel(path, header=1)
return df_try
def _map_columns(df: pd.DataFrame):
cols = list(df.columns)
norm = {_normalize_colname(c): c for c in cols}
candidates = {
"studentid": [
"studentid", "student id", "student no", "student number", "id",
"roll", "roll no", "roll number"
],
"section": ["section", "sec", "class", "group", "batch", "tutorial", "lab section"],
"quiz": ["quiz", "quiz (10)", "quiz(10)", "quiz out of 10", "quizzes", "qz"],
"mid": ["mid", "mid (20)", "mid(20)", "mid out of 20", "midterm", "mid term"],
"lab report": [
"lab report", "lab report (20)", "lab report(20)", "labreport",
"report", "lab reports", "lab", "lab(20)", "lab (20)"
],
"lab activity": [
"lab activity", "lab activity (10)", "lab activity(10)", "labactivity",
"activity", "lab act", "lab octivity(10)", "lab 0ctivity(10)"
],
"final": [
"final", "final (40)", "final(40)", "final out of 40",
"final exam", "exam", "endterm", "end term"
],
}
mapped = {}
for logical, opts in candidates.items():
for o in opts:
o2 = _normalize_colname(o)
if o2 in norm:
mapped[logical] = norm[o2]
break
if "section" not in mapped:
raise ValueError("Section column is required.")
present_assessments = [k for k in ["quiz", "mid", "lab report", "lab activity", "final"] if k in mapped]
if len(present_assessments) < 2:
raise ValueError(
"Please provide at least two assessment components. "
"Example: Mid + Final, or Lab Report + Lab Activity."
)
rename_map = {mapped["section"]: "Section"}
if "studentid" in mapped:
rename_map[mapped["studentid"]] = "StudentID"
logical_to_real = {
"quiz": "Quiz",
"mid": "Mid",
"lab report": "Lab Report",
"lab activity": "Lab Activity",
"final": "Final",
}
for k in present_assessments:
rename_map[mapped[k]] = logical_to_real[k]
df2 = df.rename(columns=rename_map).copy()
if "StudentID" not in df2.columns:
df2.insert(0, "StudentID", [f"S{i+1:03d}" for i in range(len(df2))])
active_cols = [logical_to_real[k] for k in present_assessments]
return df2, active_cols
def _clean(df: pd.DataFrame, active_cols):
df = df.copy()
df["StudentID"] = df["StudentID"].astype(str).str.strip()
df["Section"] = df["Section"].astype(str).str.strip()
absent_tokens = ["A", "a", "Absent", "ABSENT", "ab", "AB", ""]
for c in active_cols:
df[c] = df[c].replace(absent_tokens, 0)
df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["Section"] + active_cols)
df = df[(df["StudentID"] != "") & (df["Section"] != "")]
for c in active_cols:
df[c] = df[c].clip(lower=0, upper=MAXIMA[c])
return df
# ----------------------------
# Feature engineering
# ----------------------------
def _features(df: pd.DataFrame, active_cols):
df = df.copy()
for c in active_cols:
df[f"{c}_pct"] = df[c] / MAXIMA[c]
pct_cols = [f"{c}_pct" for c in active_cols]
df["mean_score"] = df[pct_cols].mean(axis=1)
df["std_score"] = df[pct_cols].std(axis=1).fillna(0)
lab_cols = [c for c in ["Lab Report", "Lab Activity"] if c in active_cols]
theory_cols = [c for c in ["Quiz", "Mid", "Final"] if c in active_cols]
if lab_cols:
df["lab_avg"] = df[[f"{c}_pct" for c in lab_cols]].mean(axis=1)
else:
df["lab_avg"] = df["mean_score"]
if theory_cols:
df["theory_avg"] = df[[f"{c}_pct" for c in theory_cols]].mean(axis=1)
else:
df["theory_avg"] = df["mean_score"]
if "Final" in active_cols and any(c in active_cols for c in ["Quiz", "Mid"]):
exam_ref = [f"{c}_pct" for c in ["Quiz", "Mid"] if c in active_cols]
df["exam_gap"] = df["Final_pct"] - df[exam_ref].mean(axis=1)
else:
df["exam_gap"] = 0.0
coursework_cols = [c for c in ["Quiz", "Mid", "Lab Report", "Lab Activity"] if c in active_cols]
if coursework_cols and "Final" in active_cols:
df["coursework_avg"] = df[[f"{c}_pct" for c in coursework_cols]].mean(axis=1)
df["final_vs_coursework_gap"] = df["Final_pct"] - df["coursework_avg"]
else:
df["coursework_avg"] = df["mean_score"]
df["final_vs_coursework_gap"] = 0.0
if lab_cols and theory_cols:
df["lab_theory_gap"] = df["lab_avg"] - df["theory_avg"]
else:
df["lab_theory_gap"] = 0.0
return df
# ----------------------------
# Clustering + PCA plots
# ----------------------------
def _kmeans(df_feat: pd.DataFrame, k=3):
n_samples = len(df_feat)
if n_samples < 3:
raise ValueError("Not enough rows for clustering. Please upload more data.")
k = min(k, n_samples)
if k < 2:
raise ValueError("Not enough rows for clustering.")
scaler = StandardScaler()
X = scaler.fit_transform(df_feat.values)
model = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = model.fit_predict(X)
sil = None
try:
if n_samples >= k + 1 and len(set(labels)) > 1:
sil = float(silhouette_score(X, labels))
except Exception:
sil = None
return labels, X, sil, k
def _pca_fig(X, labels, hover_df, title):
if len(X) < 2:
fig = px.scatter(title=title)
fig.update_layout(height=420)
return fig
pca = PCA(n_components=2, random_state=42)
coords = pca.fit_transform(X)
plot_df = hover_df.copy()
plot_df["PC1"] = coords[:, 0]
plot_df["PC2"] = coords[:, 1]
plot_df["Cluster"] = labels.astype(int).astype(str)
fig = px.scatter(
plot_df, x="PC1", y="PC2",
color="Cluster",
hover_data=list(hover_df.columns),
title=title
)
fig.update_layout(height=420)
return fig
# ----------------------------
# Reasons
# ----------------------------
def _student_reason(r, has_final, has_lab_theory):
s = float(r.get("std_score", 0))
eg = float(r.get("final_vs_coursework_gap", 0))
ltg = float(r.get("lab_theory_gap", 0))
bullets = []
if s <= 0.10:
bullets.append("Low variation across available assessments.")
elif s <= 0.18:
bullets.append("Moderate variation across available assessments.")
else:
bullets.append("High variation across available assessments.")
if has_final:
if abs(eg) >= 0.25:
bullets.append("Large mismatch between final and coursework trend.")
elif abs(eg) >= 0.15:
bullets.append("Noticeable final vs coursework difference.")
if has_lab_theory:
if abs(ltg) >= 0.20:
bullets.append("Large mismatch between lab and theory performance.")
elif abs(ltg) >= 0.12:
bullets.append("Some lab vs theory mismatch.")
flag = (s > 0.18)
if has_final:
flag = flag or (abs(eg) >= 0.25)
if has_lab_theory:
flag = flag or (abs(ltg) >= 0.20)
if flag:
rec = "Recommendation: Consider academic review for assessment alignment and/or student support."
else:
rec = "Recommendation: Pattern looks reasonable; routine moderation is sufficient."
if not bullets:
bullets.append("Assessment pattern looks stable.")
return "Key observations:\n- " + "\n- ".join(bullets) + "\n\n" + rec
def _section_reason(r, has_final, has_lab_theory):
s = float(r.get("std_score", 0))
eg = float(r.get("final_vs_coursework_gap", 0))
ltg = float(r.get("lab_theory_gap", 0))
bullets = []
if s <= 0.08:
bullets.append("Uniform distribution across available assessments.")
elif s <= 0.14:
bullets.append("Expected variation across students in this section.")
else:
bullets.append("Higher variability across section assessment pattern.")
if has_final and abs(eg) >= 0.18:
bullets.append("Final vs coursework mismatch at section level.")
if has_lab_theory and abs(ltg) >= 0.15:
bullets.append("Lab vs theory mismatch at section level.")
flag = (s > 0.14)
if has_final:
flag = flag or (abs(eg) >= 0.18)
if has_lab_theory:
flag = flag or (abs(ltg) >= 0.15)
if flag:
rec = "Recommendation: Review assessment design/moderation consistency for this section."
else:
rec = "Recommendation: Distribution appears aligned; routine moderation is sufficient."
if not bullets:
bullets.append("Section pattern looks stable.")
return "Key observations:\n- " + "\n- ".join(bullets) + "\n\n" + rec
# ----------------------------
# Cluster meanings + manager text
# ----------------------------
def _assign_cluster_meanings(profile_df: pd.DataFrame, cluster_col: str, has_final, has_lab_theory):
dfp = profile_df.copy()
cids = [int(x) for x in dfp[cluster_col].tolist()]
if len(cids) == 0:
return {}
if has_final and has_lab_theory:
cid_final = int(dfp.loc[dfp["final_vs_coursework_gap"].idxmin(), cluster_col])
cid_lab = int(dfp.loc[dfp["lab_theory_gap"].idxmax(), cluster_col])
meaning = {cid_final: "Final Exam Misalignment", cid_lab: "Lab–Theory Mismatch"}
for cid in cids:
if cid not in meaning:
meaning[cid] = "Balanced / Acceptable Pattern"
return meaning
if has_final:
cid_final = int(dfp.loc[dfp["final_vs_coursework_gap"].idxmin(), cluster_col])
meaning = {cid_final: "Final Exam Misalignment"}
for cid in cids:
if cid not in meaning:
meaning[cid] = "Balanced / Acceptable Pattern"
return meaning
if has_lab_theory:
cid_lab = int(dfp.loc[dfp["lab_theory_gap"].idxmax(), cluster_col])
meaning = {cid_lab: "Lab–Theory Mismatch"}
for cid in cids:
if cid not in meaning:
meaning[cid] = "Balanced / Acceptable Pattern"
return meaning
return {cid: "Balanced / Acceptable Pattern" for cid in cids}
def _traffic_light(meaning: str) -> str:
return {
"Balanced / Acceptable Pattern": "🟢 Green",
"Lab–Theory Mismatch": "🟡 Amber",
"Final Exam Misalignment": "🔴 Red",
}.get(meaning, "⚪")
def _manager_signal(meaning: str) -> str:
if meaning == "Balanced / Acceptable Pattern":
return "Components are aligned; routine moderation only."
if meaning == "Lab–Theory Mismatch":
return "Labs stronger than theory; consider theory support and alignment check."
if meaning == "Final Exam Misalignment":
return "Final lower than coursework; review exam difficulty/format/weighting."
return ""
def _brief_cluster_explanation(meaning: str) -> str:
if meaning == "Balanced / Acceptable Pattern":
return "No large gaps in the available assessments."
if meaning == "Lab–Theory Mismatch":
return "Lab/practical scores noticeably higher than theory."
if meaning == "Final Exam Misalignment":
return "Final exam performance lower than coursework trend."
return ""
def _balanced_two_lines_for_managers() -> str:
return (
"Balanced / Acceptable Pattern: Marks are fairly consistent across the available assessments.\n"
"This suggests assessment design is broadly aligned; only routine moderation is needed."
)
def _build_distribution_table(labels: pd.Series, meaning_map: dict, entity_name: str) -> pd.DataFrame:
total = int(labels.shape[0])
counts = labels.value_counts().sort_index()
rows = []
for cid, cnt in counts.items():
cid = int(cid)
meaning = meaning_map.get(cid, f"Cluster {cid}")
rows.append({
"Cluster": cid,
"Cluster Meaning": meaning,
"Traffic Light": _traffic_light(meaning),
"Count": int(cnt),
f"% of {entity_name}": round((int(cnt) / total) * 100, 1) if total else 0.0,
"Why this pattern": _brief_cluster_explanation(meaning),
"Manager interpretation": _manager_signal(meaning),
})
return pd.DataFrame(rows)
# ----------------------------
# Manager graphs + recommendations
# ----------------------------
def _manager_graphs_and_reco(df_students: pd.DataFrame, df_sections: pd.DataFrame, has_final):
s_counts = df_students["StudentClusterMeaning"].value_counts().reset_index()
s_counts.columns = ["Pattern", "Count"]
fig_students = px.pie(s_counts, values="Count", names="Pattern", title="Students by Assessment Pattern")
fig_students.update_layout(height=360)
sec_plot = df_sections[["Section", "SectionClusterMeaning"]].copy()
sec_plot["Count"] = 1
fig_sections = px.bar(
sec_plot,
x="Section",
y="Count",
color="SectionClusterMeaning",
title="Section-wise Assessment Pattern (Each Bar = One Section)",
hover_data=["Section", "SectionClusterMeaning"]
)
fig_sections.update_layout(
height=380,
yaxis_title="Section",
xaxis_title="Section Number",
legend_title="Assessment Pattern"
)
fig_sections.update_xaxes(tickangle=45)
if has_final:
avg_coursework = df_students["coursework_avg"].mean() * 100
avg_final = df_students["Final_pct"].mean() * 100
fig_exam = px.bar(
x=["Average Coursework", "Average Final Exam"],
y=[avg_coursework, avg_final],
title="Overall Coursework vs Final Exam (Average %)",
labels={"x": "Assessment", "y": "Average %"}
)
else:
overall_avg = df_students["mean_score"].mean() * 100
fig_exam = px.bar(
x=["Average of Uploaded Assessments"],
y=[overall_avg],
title="Overall Average of Uploaded Assessments (%)",
labels={"x": "Assessment", "y": "Average %"}
)
fig_exam.update_layout(height=360)
total_students = len(df_students)
pct_bal = (df_students["StudentClusterMeaning"].eq("Balanced / Acceptable Pattern").mean() * 100) if total_students else 0
pct_lab = (df_students["StudentClusterMeaning"].eq("Lab–Theory Mismatch").mean() * 100) if total_students else 0
pct_final = (df_students["StudentClusterMeaning"].eq("Final Exam Misalignment").mean() * 100) if total_students else 0
lines = [f"• {pct_bal:.1f}% of students show a balanced assessment pattern."]
if pct_lab > 0:
lines.append(f"• {pct_lab:.1f}% show stronger labs than theory.")
if has_final:
lines.append(f"• {pct_final:.1f}% show final exam outcomes lower than coursework.")
lines.append("• These are QA/moderation signals; they do not indicate student misconduct.")
rec = "\n".join(lines)
return fig_students, fig_sections, fig_exam, rec
# ----------------------------
# Excel download
# ----------------------------
def _make_excel(student_out, section_out, student_profile, section_profile,
student_distribution, section_distribution, section_list) -> bytes:
out = io.BytesIO()
with pd.ExcelWriter(out, engine="openpyxl") as writer:
student_out.to_excel(writer, index=False, sheet_name="Student_Level")
section_out.to_excel(writer, index=False, sheet_name="Section_Level")
student_profile.to_excel(writer, index=False, sheet_name="Student_Cluster_Profile")
section_profile.to_excel(writer, index=False, sheet_name="Section_Cluster_Profile")
student_distribution.to_excel(writer, index=False, sheet_name="Student_Distribution")
section_distribution.to_excel(writer, index=False, sheet_name="Section_Distribution")
section_list.to_excel(writer, index=False, sheet_name="Section_List_With_Meaning")
out.seek(0)
return out.getvalue()
# ----------------------------
# Main analysis
# ----------------------------
def run_analysis(file_obj):
if file_obj is None:
raise gr.Error("Please upload an Excel/CSV file first.")
name = (getattr(file_obj, "name", "") or "").lower()
try:
if name.endswith(".csv"):
raw = pd.read_csv(file_obj.name)
else:
raw = _read_excel_safely(file_obj.name)
except Exception as e:
raise gr.Error(f"Could not read the file. Details: {e}")
try:
df, active_cols = _map_columns(raw)
except Exception as e:
raise gr.Error(str(e))
df = _clean(df, active_cols)
df = _features(df, active_cols)
if df.empty:
raise gr.Error("No valid rows after cleaning. Please check your file.")
has_final = "Final" in active_cols
has_lab_cols = any(c in active_cols for c in ["Lab Report", "Lab Activity"])
has_theory_cols = any(c in active_cols for c in ["Quiz", "Mid", "Final"])
has_lab_theory = has_lab_cols and has_theory_cols
# Student-level clustering
stud_feat = df[["mean_score", "std_score", "exam_gap", "lab_theory_gap", "final_vs_coursework_gap"]]
stud_labels, Xs, stud_sil, stud_k = _kmeans(stud_feat, k=3)
df["StudentCluster"] = stud_labels.astype(int)
df["StudentReason"] = df.apply(lambda r: _student_reason(r, has_final, has_lab_theory), axis=1)
profile_cols = [f"{c}_pct" for c in active_cols] + ["mean_score", "std_score"]
if has_final:
profile_cols.append("final_vs_coursework_gap")
if has_lab_theory:
profile_cols.append("lab_theory_gap")
stud_profile = (
df.groupby("StudentCluster")[profile_cols]
.mean()
.reset_index()
.sort_values("StudentCluster")
)
# Section-level aggregation
sec_cols = [f"{c}_pct" for c in active_cols] + ["mean_score", "std_score", "exam_gap"]
if has_final:
sec_cols.append("final_vs_coursework_gap")
if has_lab_theory:
sec_cols.append("lab_theory_gap")
sec = df.groupby("Section")[sec_cols].mean().reset_index()
sec_feat = sec[["mean_score", "std_score", "exam_gap"]].copy()
sec_feat["lab_theory_gap"] = sec.get("lab_theory_gap", 0.0)
sec_feat["final_vs_coursework_gap"] = sec.get("final_vs_coursework_gap", 0.0)
sec_labels, Xsec, sec_sil, sec_k = _kmeans(sec_feat, k=min(3, len(sec)))
sec["SectionReason"] = sec.apply(lambda r: _section_reason(r, has_final, has_lab_theory), axis=1)
sec["SectionCluster"] = sec_labels.astype(int)
sec_profile = (
sec.groupby("SectionCluster")[profile_cols]
.mean()
.reset_index()
.sort_values("SectionCluster")
)
# Meaning mapping
student_meaning_map = _assign_cluster_meanings(stud_profile, "StudentCluster", has_final, has_lab_theory)
section_meaning_map = _assign_cluster_meanings(sec_profile, "SectionCluster", has_final, has_lab_theory)
df["StudentClusterMeaning"] = df["StudentCluster"].map(lambda x: student_meaning_map.get(int(x), f"Cluster {int(x)}"))
df["StudentTrafficLight"] = df["StudentClusterMeaning"].map(_traffic_light)
df["StudentManagerSignal"] = df["StudentClusterMeaning"].map(_manager_signal)
sec["SectionClusterMeaning"] = sec["SectionCluster"].map(lambda x: section_meaning_map.get(int(x), f"Cluster {int(x)}"))
sec["SectionTrafficLight"] = sec["SectionClusterMeaning"].map(_traffic_light)
sec["SectionManagerSignal"] = sec["SectionClusterMeaning"].map(_manager_signal)
# Manager tables
student_distribution = _build_distribution_table(df["StudentCluster"], student_meaning_map, "Students")
section_distribution = _build_distribution_table(sec["SectionCluster"], section_meaning_map, "Sections")
# Manager graphs
fig_students, fig_sections, fig_exam, reco = _manager_graphs_and_reco(df, sec, has_final)
# PCA plots
student_hover_cols = ["StudentID", "Section"] + active_cols + [
"StudentCluster", "StudentClusterMeaning", "StudentTrafficLight",
"mean_score", "std_score"
]
if has_final:
student_hover_cols.append("final_vs_coursework_gap")
if has_lab_theory:
student_hover_cols.append("lab_theory_gap")
hover_student = df[student_hover_cols].copy()
fig_student_pca = _pca_fig(Xs, stud_labels, hover_student, "Student-Level Clustering (PCA)")
section_hover_cols = [
"Section", "SectionCluster", "SectionClusterMeaning", "SectionTrafficLight",
"mean_score", "std_score"
]
if has_final:
section_hover_cols.append("final_vs_coursework_gap")
if has_lab_theory:
section_hover_cols.append("lab_theory_gap")
hover_section = sec[section_hover_cols].copy()
fig_section_pca = _pca_fig(Xsec, sec_labels, hover_section, "Section-Level Clustering (PCA)")
# Section list
section_list_cols = [
"Section", "SectionCluster", "SectionClusterMeaning", "SectionTrafficLight",
"SectionManagerSignal", "mean_score", "std_score"
]
if has_final:
section_list_cols.append("final_vs_coursework_gap")
if has_lab_theory:
section_list_cols.append("lab_theory_gap")
section_list_cols.append("SectionReason")
section_list = sec[section_list_cols].copy()
for c in ["mean_score", "std_score", "final_vs_coursework_gap", "lab_theory_gap"]:
if c in section_list.columns:
section_list[c] = (section_list[c] * 100).round(1)
# Student output
student_out_cols = ["StudentID", "Section"] + active_cols + [
"StudentCluster", "StudentClusterMeaning", "StudentTrafficLight",
"StudentManagerSignal", "mean_score", "std_score"
]
if has_final:
student_out_cols.append("final_vs_coursework_gap")
if has_lab_theory:
student_out_cols.append("lab_theory_gap")
student_out_cols.append("StudentReason")
student_out = df[student_out_cols].copy()
for c in ["mean_score", "std_score", "final_vs_coursework_gap", "lab_theory_gap"]:
if c in student_out.columns:
student_out[c] = (student_out[c] * 100).round(1)
# Section output
section_out_cols = [
"Section", "SectionCluster", "SectionClusterMeaning", "SectionTrafficLight",
"SectionManagerSignal", "mean_score", "std_score"
]
if has_final:
section_out_cols.append("final_vs_coursework_gap")
if has_lab_theory:
section_out_cols.append("lab_theory_gap")
section_out_cols.append("SectionReason")
section_out = sec[section_out_cols].copy()
for c in ["mean_score", "std_score", "final_vs_coursework_gap", "lab_theory_gap"]:
if c in section_out.columns:
section_out[c] = (section_out[c] * 100).round(1)
# Profiles as %
stud_profile_show = stud_profile.copy()
sec_profile_show = sec_profile.copy()
for t in [stud_profile_show, sec_profile_show]:
for c in t.columns:
if c != t.columns[0]:
t[c] = (t[c] * 100).round(1)
sil_s = f"{stud_sil:.3f}" if stud_sil is not None else "N/A"
sil_c = f"{sec_sil:.3f}" if sec_sil is not None else "N/A"
active_text = ", ".join(active_cols)
summary = (
f"Students analysed: {len(df)}\n"
f"Sections analysed: {df['Section'].nunique()}\n"
f"Assessments detected: {active_text}\n\n"
f"Student-level silhouette: {sil_s}\n"
f"Section-level silhouette: {sil_c}\n\n"
f"{_balanced_two_lines_for_managers()}\n\n"
"Cluster numbers are labels, not ranks.\n"
"These clusters are for QA/moderation and academic support only."
)
excel_bytes = _make_excel(
student_out, section_out,
stud_profile_show, sec_profile_show,
student_distribution, section_distribution,
section_list
)
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
tmp.write(excel_bytes)
tmp.flush()
tmp.close()
return (
summary,
fig_students, fig_sections, fig_exam, reco,
student_distribution, section_distribution, section_list,
student_out, fig_student_pca, stud_profile_show,
section_out, fig_section_pca, sec_profile_show,
tmp.name
)
# ----------------------------
# UI
# ----------------------------
with gr.Blocks(title="Marks Clustering Dashboard — Flexible Assessments") as demo:
gr.Markdown(
"""
# Marks Clustering Dashboard (Flexible)
Upload any **Excel/CSV** file.
### Required:
- **Section**
### Optional assessments (at least 2 needed):
- Quiz (out of 10)
- Mid (out of 20)
- Lab Report (out of 20)
- Lab Activity (out of 10)
- Final (out of 40)
### Notes:
- The system automatically detects which assessments are included.
- It supports different combinations such as:
- Mid + Final
- Lab Report + Lab Activity
- Quiz + Mid + Final
- All components
- **A / Absent** is treated as **0**
- The output shows only uploaded assessment-related analysis
""".strip()
)
file_in = gr.File(label="Upload Excel/CSV", file_types=[".xlsx", ".xls", ".csv"])
run_btn = gr.Button("Run Analysis", variant="primary")
summary = gr.Textbox(label="Summary", lines=12)
with gr.Tabs():
with gr.Tab("Manager Summary"):
ms_fig_students = gr.Plot(label="Students by Pattern")
ms_fig_sections = gr.Plot(label="Section-wise Pattern")
ms_fig_exam = gr.Plot(label="Overall Assessment Comparison")
ms_reco = gr.Textbox(label="Brief Recommendations", lines=6)
ms_student_dist = gr.Dataframe(label="Student Distribution", wrap=True, interactive=False)
ms_section_dist = gr.Dataframe(label="Section Distribution", wrap=True, interactive=False)
ms_section_list = gr.Dataframe(label="Section List", wrap=True, interactive=False)
with gr.Tab("Student-Level"):
student_df = gr.Dataframe(label="Student-Level Results", wrap=True)
student_plot = gr.Plot(label="Student PCA Plot")
student_profile = gr.Dataframe(label="Student Cluster Profile (%)", wrap=True)
with gr.Tab("Section-Level"):
section_df = gr.Dataframe(label="Section-Level Results", wrap=True)
section_plot = gr.Plot(label="Section PCA Plot")
section_profile = gr.Dataframe(label="Section Cluster Profile (%)", wrap=True)
with gr.Tab("Download"):
dl = gr.File(label="Download Excel Report")
run_btn.click(
run_analysis,
inputs=[file_in],
outputs=[
summary,
ms_fig_students, ms_fig_sections, ms_fig_exam, ms_reco,
ms_student_dist, ms_section_dist, ms_section_list,
student_df, student_plot, student_profile,
section_df, section_plot, section_profile,
dl
],
)
print("Starting Gradio app...")
if __name__ == "__main__":
demo.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True
)