|
python
import pandas as pd
from datetime import datetime
import os
# -------------------------- 1. 配置参数(可根据实际需求修改)--------------------------
# 考勤规则配置(适配大多数企业考勤制度,可灵活调整)
WORK_START = "09:00:00" # 标准上班时间
WORK_END = "17:00:00" # 标准下班时间
OVERTIME_THRESHOLD = 8.0 # 标准工作时长(小时),超过部分算加班
LATE_THRESHOLD = "09:05:00" # 迟到阈值,超过该时间算迟到
EARLY_LEAVE_THRESHOLD = "16:55:00" # 早退阈值,早于该时间算早退
# 数据源配置(替换为你的Excel文件路径)
EXCEL_PATH = "员工考勤数据.xlsx" # 考勤数据文件
SHEET_NAME = "考勤记录" # Excel工作表名称
# 输出配置
OUTPUT_HTML = "员工考勤加班分析报告.html" # 可视化报告输出路径
# -------------------------- 2. 数据读取与预处理 --------------------------
def load_and_preprocess_data(file_path, sheet_name):
"""
读取Excel考勤数据,并进行预处理(处理时间格式、删除无效数据)
:param file_path: Excel文件路径
:param sheet_name: 工作表名称
:return: 预处理后的DataFrame
"""
# 读取Excel文件
df = pd.read_excel(file_path, sheet_name=sheet_name, engine="openpyxl")
# 查看数据结构,确保列名正确(需与你的Excel列名对应,可根据实际修改)
required_columns = ["姓名", "人员编号", "日期", "上班时间", "下班时间"]
if not all(col in df.columns for col in required_columns):
raise ValueError(f"Excel文件缺少必要列,需包含:{required_columns}")
# 转换时间格式(将字符串时间转为datetime格式,便于计算)
df["上班时间"] = pd.to_datetime(df["上班时间"], format="%H:%M:%S", errors="coerce")
df["下班时间"] = pd.to_datetime(df["下班时间"], format="%H:%M:%S", errors="coerce")
# 删除无效数据(时间为空、日期异常的记录)
df = df.dropna(subset=["上班时间", "下班时间", "日期"])
df = df[df["日期"].notna()]
# 提取日期中的年、月、日,便于后续统计
df["日期"] = pd.to_datetime(df["日期"])
df["年份"] = df["日期"].dt.year
df["月份"] = df["日期"].dt.month
print(f"数据预处理完成,有效考勤记录共 {len(df)} 条")
return df
# -------------------------- 3. 考勤指标计算(核心模块)--------------------------
def calculate_attendance_indicators(df):
"""
计算核心考勤指标:工作时长、加班时长、是否迟到/早退、出勤率等
:param df: 预处理后的考勤数据
:return: 包含所有指标的DataFrame、统计摘要
"""
# 转换标准时间为datetime格式,用于对比
work_start = datetime.strptime(WORK_START, "%H:%M:%S").time()
work_end = datetime.strptime(WORK_END, "%H:%M:%S").time()
late_threshold = datetime.strptime(LATE_THRESHOLD, "%H:%M:%S").time()
early_leave_threshold = datetime.strptime(EARLY_LEAVE_THRESHOLD, "%H:%M:%S").time()
# 计算每日工作时长(小时):下班时间 - 上班时间,保留2位小数
df["工作时长(小时)"] = (df["下班时间"] - df["上班时间"]).dt.total_seconds() / 3600
df["工作时长(小时)"] = df["工作时长(小时)"].round(2)
# 计算加班时长(小时):超过标准工作时长的部分,不足0则按0计算
df["加班时长(小时)"] = df["工作时长(小时)"].apply(lambda x: max(x - OVERTIME_THRESHOLD, 0))
df["加班时长(小时)"] = df["加班时长(小时)"].round(2)
# 判断是否迟到:上班时间超过迟到阈值
df["是否迟到"] = df["上班时间"].dt.time.apply(lambda x: "是" if x > late_threshold else "否")
# 计算迟到分钟数
df["迟到分钟数"] = df.apply(
lambda row: round((row["上班时间"].dt.time() - late_threshold).total_seconds() / 60)
if row["是否迟到"] == "是" else 0, axis=1
)
# 判断是否早退:下班时间早于早退阈值
df["是否早退"] = df["下班时间"].dt.time.apply(lambda x: "是" if x < early_leave_threshold else "否")
# 计算早退分钟数
df["早退分钟数"] = df.apply(
lambda row: round((early_leave_threshold - row["下班时间"].dt.time()).total_seconds() / 60)
if row["是否早退"] == "是" else 0, axis=1
)
# 判断是否加班:加班时长大于0
df["是否加班"] = df["加班时长(小时)"].apply(lambda x: "是" if x > 0 else "否")
# 统计摘要(按员工分组,汇总核心指标)
summary = df.groupby(["姓名", "人员编号"]).agg({
"日期": "count", # 统计天数
"是否迟到": lambda x: (x == "是").sum(), # 迟到天数
"是否早退": lambda x: (x == "是").sum(), # 早退天数
"是否加班": lambda x: (x == "是").sum(), # 加班天数
"加班时长(小时)": "sum", # 总加班时长
"工作时长(小时)": "mean", # 平均工作时长
"上班时间": "min", # 最早上班时间
"下班时间": "max" # 最晚下班时间
}).reset_index()
# 重命名列名,使其更直观
summary.columns = [
"员工姓名", "员工编号", "统计天数", "迟到天数", "早退天数",
"加班天数", "总加班时长(小时)", "平均工作时长(小时)", "最早上班时间", "最晚下班时间"
]
# 计算出勤率(假设统计天数均为工作日,可根据实际调整)
summary["出勤天数"] = summary["统计天数"] - (summary["迟到天数"] + summary["早退天数"])
summary["出勤率"] = (summary["出勤天数"] / summary["统计天数"] * 100).round(1)
# 计算加班天数占比
summary["加班天数占比"] = (summary["加班天数"] / summary["统计天数"] * 100).round(1)
# 计算平均每天加班时长
summary["平均每天加班时长(小时)"] = (summary["总加班时长(小时)"] / summary["加班天数"]).round(2)
# 处理除数为0的情况(无加班时,平均加班时长设为0)
summary["平均每天加班时长(小时)"] = summary["平均每天加班时长(小时)"].fillna(0)
# 格式化时间字段,便于展示
summary["最早上班时间"] = summary["最早上班时间"].dt.strftime("%H:%M:%S")
summary["最晚下班时间"] = summary["最晚下班时间"].dt.strftime("%H:%M:%S")
print("考勤指标计算完成,生成统计摘要")
return df, summary
# -------------------------- 4. 生成可视化HTML报告 --------------------------
def generate_html_report(df, summary, output_path):
"""
生成交互式可视化HTML报告,包含数据表格、多维度图表
:param df: 包含所有指标的考勤数据
:param summary: 考勤统计摘要
:param output_path: HTML报告输出路径
"""
# 取第一个员工的信息(可扩展为多员工切换,此处以单员工为例)
employee_info = summary.iloc[0]
employee_name = employee_info["员工姓名"]
employee_id = employee_info["员工编号"]
# 提取样例数据(前10条,用于报告展示)
sample_data = df.head(10).to_dict("records")
# 提取图表所需数据
# 加班时长趋势数据(按日期排序)
overtime_trend = df.sort_values("日期")[["日期", "加班时长(小时)"]]
overtime_chart_data = {
"labels": [d.strftime("%Y-%m-%d") for d in overtime_trend["日期"]],
"data": overtime_trend["加班时长(小时)"].tolist()
}
# 工作时长趋势数据
work_hours_trend = df.sort_values("日期")[["日期", "工作时长(小时)"]]
work_hours_chart_data = {
"labels": [d.strftime("%Y-%m-%d") for d in work_hours_trend["日期"]],
"data": work_hours_trend["工作时长(小时)"].tolist()
}
# 考勤状态分布数据
status_data = {
"labels": ["正常出勤", "迟到", "早退", "加班"],
"data": [
employee_info["出勤天数"],
employee_info["迟到天数"],
employee_info["早退天数"],
employee_info["加班天数"]
]
}
# HTML模板(集成Tailwind CSS和Chart.js,实现响应式可视化)
html_template = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{employee_name}考勤加班分析报告</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
<script>
// 样例数据
const sampleData = {sample_data};
// 统计摘要
const summary = {employee_info.to_dict()};
// 加班时长图表数据
const overtimeChartData = {overtime_chart_data};
// 工作时长图表数据
const workHoursChartData = {work_hours_chart_data};
// 考勤状态分布数据
const statusChartData = {status_data};
</script>
<style>
.fade-in {{animation: fadeIn 0.8s ease-in;}}
@keyframes fadeIn {{from {{opacity: 0; transform: translateY(20px);}} to {{opacity: 1; transform: translateY(0);}}}}
.card-hover {{transition: all 0.3s ease;}}
.card-hover:hover {{transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.15);}}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<header class="bg-gradient-to-r from-blue-600 to-purple-600 text-white py-8">
<div class="container mx-auto px-6 text-center">
<h1 class="text-4xl font-bold mb-2"><i class="fas fa-clock mr-3"></i>员工考勤加班分析报告</h1>
<p class="text-blue-100 text-lg">基于{df["日期"].min().strftime("%Y-%m-%d")}至{df["日期"].max().strftime("%Y-%m-%d")}考勤数据</p>
<div class="mt-4 flex justify-center items-center space-x-8 text-sm">
<span><i class="fas fa-user mr-2"></i>员工: {employee_name}</span>
<span><i class="fas fa-id-card mr-2"></i>编号: {employee_id}</span>
</div>
</div>
</header>
<section class="py-12">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center"><i class="fas fa-chart-line mr-3 text-blue-600"></i>关键指标概览</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 card-hover fade-in">
<div class="flex items-center justify-between">
<div><p class="text-gray-600 text-sm font-medium">出勤率</p><p class="text-3xl font-bold text-green-600">{employee_info["出勤率"]}%</p></div>
<div class="bg-green-100 p-3 rounded-full"><i class="fas fa-check-circle text-green-600 text-xl"></i></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-blue-500 card-hover fade-in">
<div class="flex items-center justify-between">
<div><p class="text-gray-600 text-sm font-medium">加班天数</p><p class="text-3xl font-bold text-blue-600">{employee_info["加班天数"]}天</p></div>
<div class="bg-blue-100 p-3 rounded-full"><i class="fas fa-clock text-blue-600 text-xl"></i></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-purple-500 card-hover fade-in">
<div class="flex items-center justify-between">
<div><p class="text-gray-600 text-sm font-medium">总加班时长</p><p class="text-3xl font-bold text-purple-600">{employee_info["总加班时长(小时)"]}小时</p></div>
<div class="bg-purple-100 p-3 rounded-full"><i class="fas fa-hourglass-half text-purple-600 text-xl"></i></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6 border-l-4 border-orange-500 card-hover fade-in">
<div class="flex items-center justify-between">
<div><p class="text-gray-600 text-sm font-medium">平均工作时长</p><p class="text-3xl font-bold text-orange-600">{employee_info["平均工作时长(小时)"]}小时</p></div>
<div class="bg-orange-100 p-3 rounded-full"><i class="fas fa-briefcase text-orange-600 text-xl"></i></div>
</div>
</div>
</div>
</div>
</section>
<section class="py-12 bg-white">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center"><i class="fas fa-table mr-3 text-blue-600"></i>样例数据表格</h2>
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日期</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">上班时间</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">下班时间</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">工作时长</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">加班时长</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">考勤状态</th>
</tr>
</thead>
<tbody id="sampleDataTable" class="bg-white divide-y divide-gray-200"></tbody>
</table>
</div>
</div>
</div>
</section>
<section class="py-12">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center"><i class="fas fa-chart-bar mr-3 text-blue-600"></i>数据可视化分析</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">每日加班时长</h3>
<div class="h-80"><canvas id="overtimeChart"></canvas></div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">每日工作时长趋势</h3>
<div class="h-80"><canvas id="workHoursChart"></canvas></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-lg p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">考勤状态分布</h3>
<div class="flex justify-center"><div class="h-96 w-96"><canvas id="statusChart"></canvas></div></div>
</div>
</div>
</section>
<section class="py-12 bg-gradient-to-r from-gray-800 to-gray-900 text-white">
<div class="container mx-auto px-6">
<h2 class="text-3xl font-bold mb-8 text-center"><i class="fas fa-lightbulb mr-3 text-yellow-400"></i>数据分析总结</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="bg-white bg-opacity-10 rounded-xl p-6 backdrop-blur-sm">
<h3 class="text-xl font-bold mb-4 text-yellow-400">优势表现</h3>
<ul class="space-y-3 text-gray-200">
<li class="flex items-start"><i class="fas fa-check-circle text-green-400 mt-1 mr-3"></i><span>出勤率较高:{employee_info["统计天数"]}个工作日中出勤{employee_info["出勤天数"]}天,出勤率达{employee_info["出勤率"]}%</span></li>
<li class="flex items-start"><i class="fas fa-check-circle text-green-400 mt-1 mr-3"></i><span>工作投入度高:{employee_info["加班天数占比"]}%的工作日存在加班,表现出较强的责任心</span></li>
<li class="flex items-start"><i class="fas fa-check-circle text-green-400 mt-1 mr-3"></i><span>加班时长稳定:平均每天加班{employee_info["平均每天加班时长(小时)"]}小时,加班强度适中</span></li>
</ul>
</div>
<div class="bg-white bg-opacity-10 rounded-xl p-6 backdrop-blur-sm">
<h3 class="text-xl font-bold mb-4 text-yellow-400">改进建议</h3>
<ul class="space-y-3 text-gray-200">
<li class="flex items-start"><i class="fas fa-arrow-right text-blue-400 mt-1 mr-3"></i><span>关注工作效率:建议分析加班原因,优化工作流程提高效率,减少无效加班</span></li>
<li class="flex items-start"><i class="fas fa-arrow-right text-blue-400 mt-1 mr-3"></i><span>注意劳逸结合:{employee_info["加班天数占比"]}%的加班率较高,建议合理安排工作时间</span></li>
<li class="flex items-start"><i class="fas fa-arrow-right text-blue-400 mt-1 mr-3"></i><span>完善考勤管理:对迟到、早退情况进行跟踪,建立异常预警机制</span></li>
</ul>
</div>
</div>
</section>
<footer class="bg-gray-800 text-white py-6">
<div class="container mx-auto px-6 text-center">
<p class="text-gray-400"><i class="fas fa-robot mr-2"></i>考勤分析报告 | 生成时间: <span id="currentTime"></span></p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {{
// 填充样例数据表格
const tableBody = document.getElementById('sampleDataTable');
sampleData.forEach(record => {{
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors duration-200';
let status = '正常';
let statusClass = 'bg-green-100 text-green-800';
if (record.是否迟到 === '是') {{status = '迟到'; statusClass = 'bg-red-100 text-red-800';}}
else if (record.是否早退 === '是') {{status = '早退'; statusClass = 'bg-yellow-100 text-yellow-800';}}
else if (record.加班时长_小时_ > 0) {{status = '加班'; statusClass = 'bg-blue-100 text-blue-800';}}
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${{record.日期.slice(0,10)}}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${{record.上班时间.slice(11,19)}}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${{record.下班时间.slice(11,19)}}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${{record.工作时长_小时_}}小时</td>
<td class="px-6 py-4 whitespace-nowrap text-sm ${{record.加班时长_小时_ > 0 ? 'text-blue-600 font-medium' : 'text-gray-500'}}">${{record.加班时长_小时_}}小时</td>
<td class="px-6 py-4 whitespace-nowrap"><span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${{statusClass}}">${{status}}</span></td>
`;
tableBody.appendChild(row);
}});
// 创建加班时长图表
new Chart(document.getElementById('overtimeChart'), {{
type: 'bar', data: {{labels: overtimeChartData.labels, datasets: [{label: '加班时长(小时)', data: overtimeChartData.data, backgroundColor: 'rgba(59, 130, 246, 0.8)', borderColor: 'rgba(59, 130, 246, 1)', borderRadius: 4}]}},
options: {{responsive: true, maintainAspectRatio: false, scales: {{y: {beginAtZero: true, title: {display: true, text: '时长(小时)'}}, x: {title: {display: true, text: '日期'}}}}
}});
// 创建工作时长图表
new Chart(document.getElementById('workHoursChart'), {{
type: 'line', data: {{labels: workHoursChartData.labels, datasets: [{label: '工作时长(小时)', data: workHoursChartData.data, borderColor: 'rgba(16, 185, 129, 1)', backgroundColor: 'rgba(16, 185, 129, 0.1)', borderWidth: 2, fill: true, tension: 0.4}]}},
options: {{responsive: true, maintainAspectRatio: false, scales: {{y: {beginAtZero: true, title: {display: true, text: '时长(小时)'}}, x: {title: {display: true, text: '日期'}}}}
}});
// 创建考勤状态饼图
new Chart(document.getElementById('statusChart'), {{
type: 'doughnut', data: {{labels: statusChartData.labels, datasets: [{data: statusChartData.data, backgroundColor: ['rgba(16, 185, 129, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(59, 130, 246, 0.8)'], borderWidth: 2}]}},
options: {{responsive: true, maintainAspectRatio: false, plugins: {{legend: {position: 'bottom'}}}}
}});
// 更新当前时间
document.getElementById('currentTime').textContent = new Date().toLocaleString('zh-CN');
}});
</script>
</body>
</html>
"""
# 写入HTML文件
with open(output_path, "w", encoding="utf-8") as f:
f.write(html_template)
print(f"可视化HTML报告已生成,路径:{os.path.abspath(output_path)}")
# -------------------------- 5. 主函数(程序入口)--------------------------
def main():
"""主函数,串联所有模块,执行完整流程"""
try:
# 1. 读取并预处理数据
df = load_and_preprocess_data(EXCEL_PATH, SHEET_NAME)
# 2. 计算考勤指标
df, summary = calculate_attendance_indicators(df)
# 3. 生成可视化报告
generate_html_report(df, summary, OUTPUT_HTML)
print("="*50)
print("考勤数据自动分析完成!")
print(f"核心统计:共处理 {len(df)} 条考勤记录,生成 {len(summary)} 名员工的分析报告")
print(f"报告路径:{os.path.abspath(OUTPUT_HTML)}")
print("="*50)
except Exception as e:
print(f"程序执行出错:{str(e)}")
print("请检查以下内容:1. Excel文件路径是否正确;2. 列名是否与代码中一致;3. 时间格式是否正确")
# 执行主函数
if __name__ == "__main__":
main()
|
所有评论(0)