import tkinter as tk from tkinter import ttk, messagebox import pandas as pd import numpy as np import matplotlib.pyplot as plt import os # 定义常量 SUBJECTS = ['语文', '数学', '英语', '物理', '化学', '生物'] # 设置绘图字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 这里假设Windows环境,Linux/Mac可能需要更改 plt.rcParams['axes.unicode_minus'] = False class GradeSystemApp: def __init__(self, root): self.root = root self.root.title("学生成绩分析系统 V2.0") self.root.geometry("800x600") # 数据存储列表 self.student_data_list = [] self.setup_ui() def setup_ui(self): # --- 顶部:录入区域 --- input_frame = tk.LabelFrame(self.root, text="成绩录入", padx=10, pady=10) input_frame.pack(fill="x", padx=10, pady=5) # 学号录入 tk.Label(input_frame, text="学号:").grid(row=0, column=0, padx=5, pady=5) self.entry_id = tk.Entry(input_frame, width=15) self.entry_id.grid(row=0, column=1, padx=5, pady=5) # 各科目录入框字典 self.score_entries = {} row = 1 col = 0 for i, subj in enumerate(SUBJECTS): tk.Label(input_frame, text=f"{subj}:").grid(row=row, column=col * 2, padx=5, pady=5) entry = tk.Entry(input_frame, width=10) entry.grid(row=row, column=col * 2 + 1, padx=5, pady=5) self.score_entries[subj] = entry # 每行显示3个科目,换行 col += 1 if col > 2: col = 0 row += 1 # 按钮区域 btn_frame = tk.Frame(input_frame) btn_frame.grid(row=row + 1, column=0, columnspan=6, pady=10) tk.Button(btn_frame, text="添加学生", command=self.add_student, bg="#e1f5fe", width=12).pack(side=tk.LEFT, padx=10) tk.Button(btn_frame, text="清空输入框", command=self.clear_entries, width=12).pack(side=tk.LEFT, padx=10) tk.Button(btn_frame, text="生成分析报告", command=self.generate_report, bg="#4caf50", fg="white", width=15).pack(side=tk.LEFT, padx=10) # --- 中部:数据展示区域 (Treeview) --- list_frame = tk.LabelFrame(self.root, text="已录入学生列表 (可选中并删除)", padx=10, pady=10) list_frame.pack(fill="both", expand=True, padx=10, pady=5) columns = ['学号'] + SUBJECTS self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=10) # 设置表头和列宽 for col in columns: self.tree.heading(col, text=col) self.tree.column(col, width=80, anchor='center') # 添加滚动条 scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscroll=scrollbar.set) self.tree.pack(side=tk.LEFT, fill="both", expand=True) scrollbar.pack(side=tk.RIGHT, fill="y") # 绑定删除事件(双击或按钮,这里用按钮) del_btn = tk.Button(list_frame, text="删除选中行", command=self.delete_selected, bg="#ffcdd2") del_btn.pack(side=tk.BOTTOM, fill="x", pady=5) def add_student(self): stu_id = self.entry_id.get().strip() if not stu_id: messagebox.showwarning("提示", "请输入学号!") return # 检查ID是否重复 for data in self.student_data_list: if data['ID'] == stu_id: messagebox.showerror("错误", f"学号 {stu_id} 已存在!") return # 获取分数 row_data = {'ID': stu_id} display_values = [stu_id] try: for subj in SUBJECTS: val = self.score_entries[subj].get().strip() if not val: messagebox.showwarning("提示", f"请输入 {subj} 成绩!") return score = float(val) if score < 0 or score > 150: # 简单校验 messagebox.showwarning("提示", f"{subj} 成绩不合理,请检查!") return row_data[subj] = score display_values.append(score) # 保存数据 self.student_data_list.append(row_data) # 更新界面表格 self.tree.insert('', tk.END, values=display_values) # 清空输入并将焦点回到学号框 self.clear_entries() self.entry_id.focus_set() except ValueError: messagebox.showerror("错误", "成绩必须是数字!") def delete_selected(self): selected_item = self.tree.selection() if not selected_item: return for item in selected_item: values = self.tree.item(item, 'values') stu_id = values[0] # 学号是第一列 # 从数据源删除 self.student_data_list = [d for d in self.student_data_list if d['ID'] != stu_id] # 从UI删除 self.tree.delete(item) def clear_entries(self): self.entry_id.delete(0, tk.END) for entry in self.score_entries.values(): entry.delete(0, tk.END) def generate_report(self): if not self.student_data_list: messagebox.showinfo("提示", "当前没有数据,无法生成报告。") return try: # 转换为DataFrame df = pd.DataFrame(self.student_data_list) df.set_index('ID', inplace=True) # 确保保存报告的文件夹存在 report_dir = "学生成绩报告单" if not os.path.exists(report_dir): os.makedirs(report_dir) # ======================== # 1. 学生维度分析 # ======================== student_stats = df.copy() student_stats['总分'] = student_stats[SUBJECTS].sum(axis=1) student_stats['平均分'] = student_stats[SUBJECTS].mean(axis=1).round(1) student_stats['成绩波动(标准差)'] = student_stats[SUBJECTS].std(axis=1).round(2) def get_best_worst(row): scores = row[SUBJECTS] best_s = scores.idxmax() worst_s = scores.idxmin() return pd.Series([f"{best_s}({scores[best_s]})", f"{worst_s}({scores[worst_s]})"]) student_stats[['最好科目', '最差科目']] = student_stats.apply(get_best_worst, axis=1) # 排名 student_stats = student_stats.sort_values(by='总分', ascending=False) student_stats.insert(0, '排名', range(1, len(student_stats) + 1)) # 导出 CSV 1 student_stats.to_csv('1_学生总成绩排名表.csv', encoding='utf-8-sig') # ======================== # 2. 科目维度分析 # ======================== subject_stats_list = [] bins = range(0, 101, 10) # 0-10, ..., 90-100 (不含101,需要特殊处理) for subj in SUBJECTS: s_data = df[subj] excellent = (s_data >= 90).sum() passing = (s_data >= 60).sum() count = len(s_data) # 分布统计 dist_str = [] for i in range(0, 100, 10): if i == 90: # 90-100 (包含100) c = ((s_data >= 90) & (s_data <= 200)).sum() # 稍微放大上限容错 dist_str.append(f"90分以上:{c}人") else: c = ((s_data >= i) & (s_data < i + 10)).sum() dist_str.append(f"{i}-{i + 9}:{c}人") stats = { '科目': subj, '最高分': s_data.max(), '最低分': s_data.min(), '平均分': round(s_data.mean(), 1), '优秀率': f"{excellent / count:.1%}", '及格率': f"{passing / count:.1%}", '分数分布': " | ".join(dist_str), '_std': s_data.std() # 内部使用,导出时删除 } subject_stats_list.append(stats) sub_df = pd.DataFrame(subject_stats_list) # 导出 CSV 2 sub_df.drop(columns=['_std']).to_csv('2_科目统计分析表.csv', index=False, encoding='utf-8-sig') # ======================== # 3. 生成每个学生的图片 # ======================== # 准备科目平均分字典和标准差字典,用于绘图对比 sub_avg_map = dict(zip(sub_df['科目'], sub_df['平均分'])) sub_std_map = dict(zip(sub_df['科目'], sub_df['_std'])) for student_id, row in df.iterrows(): # 获取该生统计信息 info = student_stats.loc[student_id] fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) fig.suptitle(f'学生成绩报告: {student_id} (第{info["排名"]}名)', fontsize=16, fontweight='bold') # A. 柱状图 scores = [row[s] for s in SUBJECTS] avgs = [sub_avg_map[s] for s in SUBJECTS] x = np.arange(len(SUBJECTS)) width = 0.35 r1 = ax1.bar(x - width / 2, scores, width, label='个人得分', color='#4CAF50') r2 = ax1.bar(x + width / 2, avgs, width, label='班级平均', color='#2196F3', alpha=0.6) ax1.set_ylabel('分数') ax1.set_title('各科得分与班级平均分对比') ax1.set_xticks(x) ax1.set_xticklabels(SUBJECTS) ax1.legend() ax1.bar_label(r1, padding=3) ax1.bar_label(r2, padding=3, fmt='%.1f') # B. 异常检测与文本 anomalies = [] for s in SUBJECTS: mu = sub_avg_map[s] sigma = sub_std_map[s] val = row[s] if sigma > 0: # 避免除0 if val > mu + 2 * sigma: anomalies.append(f"★ {s}: {val}分 (显著高于平均 {mu},表现极其优异)") elif val < mu - 2 * sigma: anomalies.append(f"⚠ {s}: {val}分 (显著低于平均 {mu},需重点帮扶)") text_c = f"--- 综合评价 ---\n" text_c += f"总分: {info['总分']}\n" text_c += f"平均分: {info['平均分']}\n" text_c += f"发挥稳定性(标准差): {info['成绩波动(标准差)']} (越低越稳)\n" text_c += f"最好科目: {info['最好科目']}\n" text_c += f"最差科目: {info['最差科目']}\n\n" text_c += f"--- 异常预警 (超出平均分±2个标准差) ---\n" if anomalies: text_c += "\n".join(anomalies) else: text_c += "各科成绩均在正常波动范围内。" ax2.text(0.05, 0.95, text_c, transform=ax2.transAxes, fontsize=12, verticalalignment='top', linespacing=1.8) ax2.axis('off') output_path = os.path.join(report_dir, f"{student_id}_分析报告.png") plt.tight_layout() plt.savefig(output_path) plt.close(fig) messagebox.showinfo("成功", f"分析完成!\n\n已生成表格文件和 {len(df)} 张学生报告图片。\n请查看程序所在目录。") except Exception as e: messagebox.showerror("运行错误", f"分析过程中发生错误:\n{str(e)}") if __name__ == "__main__": root = tk.Tk() app = GradeSystemApp(root) root.mainloop()