2024年10月5日 星期六 考勤打卡数据分析 案例

一颗大白菜2周前我的技能36

原始表格:

image.png

一、软件概述

本软件是一个考勤管理系统,主要用于处理和展示员工的考勤数据。通过导入特定格式的 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 。


成品文件:

image.png

image.png

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


免责声明
如果您对本文有异议,请先阅读本站《免责声明》,如仍保持您个人观点可与本人联系。

相关文章

总体而言,在一定程度上反映了未来招聘求职的一种趋势。

随着人工智能技术的不断发展和普及,在未来的招聘求职中,对 AI 有一定的了解很可能会成为一种普遍的要求,就如同在 90 年代会打字、做基础表格对于求职者来说是基本技能一样。如今,AI 已经在各个领域展...

2024年8月28日 星期三 每天都有病毒在你们身边

2024年8月28日 星期三 每天都有病毒在你们身边

不要相信所有发来的EXE执行文件。...

2024年10月16日 星期三 明天科一考试了

2024年10月16日 星期三 明天科一考试了

明天就要参加 C 票科目一考试了,此刻的心情既紧张又期待。最近一段时间,为了这场考试,我付出了很多努力。每天都认真学习驾校发放的教材,那些交通法规、标志标线和安全常识,一遍又一遍地在脑海中反复记忆。还...

2024年8月3日 星期六 多云 《关于域名的回忆》

2024年8月3日 星期六 多云 《关于域名的回忆》

今天,心中涌起了对过去的诸多感慨,都是因为那个曾经与我紧密相连的网站域名。ccxb.net 已经跟随了我 19 年,它见证了我在网络世界里的许多足迹。而 ccxb.com ...

2024年8月30日 星期五 百度收录“优秀经办人”AI 知识库

2024年8月30日 星期五 百度收录“优秀经办人”AI 知识库

百度收录“优秀经办人”智能体,方便我平台用户了解吉林省保险政策及操作流程。如解答不如意,请联系系统管理员,一对一讲诉。AI知识库已接入“你好优秀经办人”微信公众号,也可直接在公众号内发送问题,AI为您...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

服务热线

18686626826

微信客服

微信客服