This repository has been archived on 2026-04-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-04-12 21:40:41 +08:00

302 lines
12 KiB
Python

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()