2024年10月5日 星期六 考勤打卡数据分析 案例
原始表格:
一、软件概述
本软件是一个考勤管理系统,主要用于处理和展示员工的考勤数据。通过导入特定格式的 Excel 文件,可以对员工的考勤情况进行统计和分析,并能够查看特定员工的详细考勤记录。
二、使用要求
导入表格必须为 .xlsx 格式,表格内容应包含员工的工号、姓名、部门以及 1 到 31 天的考勤记录(可包含打卡时间或者空白表示未打卡)。
表格中,前四行数据会被跳过读取。
三、考勤时间判断规则
默认班次:
上班时间为 08:00,下班时间为 17:00。
若员工打卡时间晚于 08:00,则记为迟到。
若员工下班打卡时间早于 17:00,记为缺卡。
学员服务中心_早班:
上班时间为 08:00,下班时间为 17:00。
判断早班情况:
若员工打卡时间晚于 08:00,或者在 08:00 但分钟数大于 0,则记为迟到。
若最后一次打卡时间不在 17:00 之后,则记为缺卡。
学员服务中心_晚班:
上班时间为 10:00,下班时间为 19:00。
若员工打卡时间在 10:00 之前或者 19:00 之后,则认为是晚班。
四、功能介绍
导入文件:
通过点击 “选择文件” 按钮,可以选择要导入的考勤数据 Excel 文件。
导入后,系统会自动读取文件内容,并在树形控件中展示考勤概要。
显示考勤概要:
树形控件展示的考勤概要包括工号、姓名、部门、迟到次数、缺卡次数、正常休息天数、额外休息天数和晚班次数。
可以点击 “部门” 列标题对员工进行排序,但导出统计数据时不会根据排序后的结果导出。
导出统计数据:
点击 “导出统计数据” 按钮,可以将统计后的考勤数据导出为一个新的 Excel 文件,文件名为 “考勤统计汇总.xlsx”。
五、注意事项
软件在运行过程中可能会出现错误,如无法加载文件等情况,请检查文件格式和内容是否正确。
在处理考勤数据时,系统会根据设定的规则进行统计,但可能会存在一些特殊情况无法准确判断,需要人工进行核实。
导出的统计数据仅为当前导入文件的统计结果,不会保留历史数据。若需要多次统计,每次都需要重新导入文件。
软件由“一颗大白菜”设计开发,如遇到问题可联系本人调整。微信、QQ:35515105 电话:18686626826 。
成品文件:
import pandas as pd import tkinter as tk from tkinter import ttk from tkinter import filedialog # 全局变量 filepath = None attendance_summary = {} def load_attendance_data(filepath): """加载考勤数据""" try: df = pd.read_excel(filepath, header=None, skiprows=4, names=['工号', '姓名', '部门'] + [str(i) for i in range(1, 32)]) print(f"读取的DataFrame列名: {df.columns.tolist()}") return df except Exception as e: print(f"加载文件时出现错误:{e}") return None def process_attendance(df, progress_var): """处理考勤数据并更新进度""" RULES = { 'default': {'check_in': '08:00', 'check_out': '17:00'}, '学员服务中心_早班': {'check_in': '08:00', 'check_out': '17:00'}, '学员服务中心_晚班': {'check_in': '10:00', 'check_out': '19:00'}, '新媒体_早班': {'check_in': '08:00', 'check_out': '17:00'}, # 添加新媒体规则 '新媒体_晚班': {'check_in': '10:00', 'check_out': '19:00'}, # 添加新媒体规则 } attendance_summary.clear() total_rows = len(df) if df is not None else 0 for index, row in df.iterrows(): if df is None: break try: employee_id = row['工号'] name = row['姓名'] department = row['部门'] # 修改此处以支持新媒体部门 if '早班' in str(department): shift = '早班' rules_key = f'{department}_{shift}' if department in ['学员服务中心', '新媒体'] else 'default' elif '晚班' in str(department): shift = '晚班' rules_key = f'{department}_{shift}' if department in ['学员服务中心', '新媒体'] else 'default' else: shift = 'default' rules_key = 'default' rules = RULES.get(rules_key, RULES['default']) except KeyError as e: print(f"缺少必要的列: {e}") continue attendance_summary[employee_id] = { 'name': name, 'department': department, 'late': 0, 'missed': 0, 'rest': 0, 'extra_rest': 0, 'evening_shifts': 0, # 修改此处以设置新媒体部门的背景颜色 'bg_color': '#b0dfe9' if department == '新媒体' else '#f0f0f0' if department == '学员服务中心' else '#ffffff' } days = list(filter(lambda x: isinstance(x, str) and x.isdigit(), row.keys())) for day in days: if pd.isnull(row[day]): if attendance_summary[employee_id]['rest'] < 4: attendance_summary[employee_id]['rest'] += 1 else: attendance_summary[employee_id]['extra_rest'] += 1 continue punches = row[day].split('\n') if len(punches) == 0: if attendance_summary[employee_id]['rest'] < 4: attendance_summary[employee_id]['rest'] += 1 else: attendance_summary[employee_id]['extra_rest'] += 1 continue valid_punches = [punch for punch in punches if ':' in punch] if department == '学员服务中心' and len(valid_punches) == 1: attendance_summary[employee_id]['missed'] += 1 print(f"{employee_id} missed on day {day}: {valid_punches}") continue if len(valid_punches) < 2: attendance_summary[employee_id]['missed'] += 1 print(f"{employee_id} missed on day {day}: {valid_punches}") continue first_punch = pd.to_datetime(valid_punches[0]).time() last_punch = pd.to_datetime(valid_punches[-1]).time() if department == '学员服务中心': if any(t.hour < 10 or t.hour >= 19 for t in (first_punch, last_punch)): rules = RULES['学员服务中心_晚班'] shift = '晚班' attendance_summary[employee_id]['evening_shifts'] += 1 else: # 判断早班情况 if shift == '早班': if first_punch.hour > 8 or (first_punch.hour == 8 and first_punch.minute > 0): attendance_summary[employee_id]['late'] += 1 # 判断最后一次打卡是否在 17:00 之后 if last_punch.hour > 17 or (last_punch.hour == 17 and last_punch.minute > 0): # 这里标记为非有效考勤,但不影响迟到和缺卡统计 pass else: attendance_summary[employee_id]['missed'] += 1 print(f"{employee_id} missed on day {day}: {valid_punches}") # 对非学员服务中心的迟到情况也进行判断 if first_punch.hour >= int(rules['check_in'].split(':')[0]) and department != '学员服务中心': attendance_summary[employee_id]['late'] += 1 if last_punch.hour < int(rules['check_out'].split(':')[0]) and department != '学员服务中心': attendance_summary[employee_id]['missed'] += 1 print(f"{employee_id} missed on day {day}: {valid_punches}") # 更新进度条 progress_var.set(index / total_rows * 100) root.update_idletasks() return attendance_summary def display_attendance_summary(summary, tree): """在树形控件中展示考勤概要""" for item in tree.get_children(): tree.delete(item) for emp_id, info in summary.items(): tree.insert("", "end", text=emp_id, values=( emp_id, info['name'], info['department'], info['late'], info['missed'], info['rest'], info['extra_rest'], info['evening_shifts'] ), tags=(info['bg_color'],)) tree.tag_configure('#ffffff', background='#ffffff') tree.tag_configure('#f0f0f0', background='#f0f0f0') tree.tag_configure('#c4c4c4', background='#c4c4c4') def browse_file(): """选择文件对话框""" global filepath selected_filepath = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")]) if selected_filepath: filepath = selected_filepath attendance_df = load_attendance_data(filepath) global attendance_summary progress_var.set(0) if attendance_df is not None: attendance_summary = process_attendance(attendance_df, progress_var) display_attendance_summary(attendance_summary, tree) progress_var.set(100) else: print("无法加载文件,请检查文件格式或内容。") def export_marked_data(): """导出统计后的数据""" global attendance_summary if not attendance_summary: print("请先加载考勤数据") return export_data = { '工号': [], '姓名': [], '部门': [], '迟到次数': [], '缺卡次数': [], '正常休息天数': [], '额外休息天数': [], '晚班次数': [] } for emp_id, info in attendance_summary.items(): export_data['工号'].append(emp_id) export_data['姓名'].append(info['name']) export_data['部门'].append(info['department']) export_data['迟到次数'].append(info['late']) export_data['缺卡次数'].append(info['missed']) export_data['正常休息天数'].append(info['rest']) export_data['额外休息天数'].append(info['extra_rest']) export_data['晚班次数'].append(info['evening_shifts']) export_df = pd.DataFrame(export_data) marked_filepath = "考勤统计汇总.xlsx" export_df.to_excel(marked_filepath, index=False) print(f"已导出考勤统计数据到: {marked_filepath}") return marked_filepath def show_info(): """显示说明信息""" info_window = tk.Toplevel(root) info_window.title("使用说明") info_window.geometry("1000x400") # 窗口大小 info_text = ( "一、软件概述\n" "本软件是一个考勤管理系统,主要用于处理和展示员工的考勤数据。通过导入特定格式的 Excel 文件,可以对员工的考勤情况进行统计和分析,并能够查看特定员工的详细考勤记录。\n\n" "二、使用要求\n" "导入表格必须为 .xlsx 格式,表格内容应包含员工的工号、姓名、部门以及 1 到 31 天的考勤记录(可包含打卡时间或者空白表示未打卡)。\n" "表格中,前四行数据会被跳过读取。\n\n" "三、考勤时间判断规则\n\n" "默认班次:\n" "上班时间为 08:00,下班时间为 17:00。\n" "若员工打卡时间晚于 08:00,则记为迟到。\n" "若员工下班打卡时间早于 17:00,记为缺卡。\n\n" "学员服务中心_早班:\n" "上班时间为 08:00,下班时间为 17:00。\n" "判断早班情况:\n" "若员工打卡时间晚于 08:00,或者在 08:00 但分钟数大于 0,则记为迟到。\n" "若最后一次打卡时间不在 17:00 之后,则记为缺卡。\n\n" "学员服务中心_晚班:\n" "上班时间为 10:00,下班时间为 19:00。\n" "若员工打卡时间在 10:00 之前或者 19:00 之后,则认为是晚班。\n\n" "四、功能介绍\n" "导入文件:\n" "通过点击 “选择文件” 按钮,可以选择要导入的考勤数据 Excel 文件。\n" "导入后,系统会自动读取文件内容,并在树形控件中展示考勤概要。\n\n" "显示考勤概要:\n" "树形控件展示的考勤概要包括工号、姓名、部门、迟到次数、缺卡次数、正常休息天数、额外休息天数和晚班次数。\n" "可以点击 “部门” 列标题对员工进行排序,但导出统计数据时不会根据排序后的结果导出。\n\n" "导出统计数据:\n" "点击 “导出统计数据” 按钮,可以将统计后的考勤数据导出为一个新的 Excel 文件,文件名为 “考勤统计汇总.xlsx”。\n\n" "五、注意事项\n\n" "软件在运行过程中可能会出现错误,如无法加载文件等情况,请检查文件格式和内容是否正确。\n\n" "在处理考勤数据时,系统会根据设定的规则进行统计,但可能会存在一些特殊情况无法准确判断,需要人工进行核实。\n\n" "导出的统计数据仅为当前导入文件的统计结果,不会保留历史数据。若需要多次统计,每次都需要重新导入文件。\n\n" "软件由“一颗大白菜”设计开发,如遇到问题可联系本人调整。微信、QQ:35515105 电话:18686626826 。\n\n" ) # 创建文本框 text_box = tk.Text(info_window, wrap=tk.WORD) text_box.insert(tk.END, info_text) text_box.config(state=tk.DISABLED) # 设置为只读 text_box.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # 创建 GUI root = tk.Tk() root.title("考勤管理系统") root.geometry("800x600") root.minsize(800, 600) frame = tk.Frame(root) frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # 添加说明按钮 button_info = tk.Button(frame, text="说明", command=show_info) button_info.grid(row=0, column=0, padx=5, pady=5) button_browse = tk.Button(frame, text="选择文件", command=browse_file) button_browse.grid(row=0, column=1, padx=5, pady=5) button_export = tk.Button(frame, text="导出统计数据", command=export_marked_data) button_export.grid(row=0, column=2, padx=5, pady=5) # 添加进度条和标签 progress_var = tk.DoubleVar() progress_bar = ttk.Progressbar(frame, orient=tk.HORIZONTAL, length=400, variable=progress_var, maximum=100) progress_bar.grid(row=0, column=3, padx=5, pady=5) progress_label = tk.Label(frame, text="进度") progress_label.grid(row=0, column=4, padx=5, pady=5) # 设置树形控件并定义列宽 tree = ttk.Treeview(frame, columns=("工号", "姓名", "部门", "迟到次数", "缺卡次数", "正常休息天数", "额外休息天数", "晚班次数"), show="headings") tree.heading("工号", text="工号") tree.heading("姓名", text="姓名") # 获取部门列的索引 department_index = tree['columns'].index('部门') tree.heading("部门", text="部门", command=lambda: sort_treeview(tree, department_index)) tree.heading("迟到次数", text="迟到次数") tree.heading("缺卡次数", text="缺卡次数") tree.heading("正常休息天数", text="正常休息天数") tree.heading("额外休息天数", text="额外休息天数") tree.heading("晚班次数", text="晚班次数") # 设置各列的宽度 tree.column("工号", width=10) tree.column("姓名", width=50) tree.column("部门", width=50) tree.column("迟到次数", width=20) tree.column("缺卡次数", width=20) tree.column("正常休息天数", width=30) tree.column("额外休息天数", width=30) tree.column("晚班次数", width=30) def show_missed_details(event): item_id = tree.identify_row(event.y) if item_id: # 获取选中行的工号、姓名、部门 emp_id = tree.item(item_id, 'values')[0] name = tree.item(item_id, 'values')[1] department = tree.item(item_id, 'values')[2] if filepath: original_df = pd.read_excel(filepath, header=None, skiprows=4, names=['工号', '姓名', '部门'] + [str(i) for i in range(1, 32)]) details_df = original_df[(original_df['工号'] == emp_id) & (original_df['姓名'] == name) & (original_df['部门'] == department)] # 创建一个新的窗口展示明细表格 detail_window = tk.Toplevel(root) detail_tree = ttk.Treeview(detail_window, columns=details_df.columns.tolist(), show="headings") for col in details_df.columns: detail_tree.heading(col, text=col) # 设置列宽 for col in details_df.columns: if col in ['工号', '迟到次数', '缺卡次数', '晚班次数']: detail_tree.column(col, width=20) elif col in ['姓名', '部门']: detail_tree.column(col, width=50) else: detail_tree.column(col, width=30) # 插入数据 for index, row in details_df.iterrows(): detail_tree.insert("", "end", values=tuple(row)) detail_tree.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) tree.bind("<Double-1>", show_missed_details) tree.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky='nsew') scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview) scrollbar.grid(row=1, column=4, sticky='ns') tree.configure(yscrollcommand=scrollbar.set) frame.columnconfigure(0, weight=1) frame.rowconfigure(1, weight=1) def sort_treeview(tree, column_index): """对树形控件进行排序""" items = [(tree.set(item, column_index), item) for item in tree.get_children('')] sorted_items = sorted(items, key=lambda x: x[0]) for index, (_, item) in enumerate(sorted_items): tree.move(item, '', index) root.mainloop()