302 lines
12 KiB
Python
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() |