用Python自动化统计培训时长:从Excel签到表到汇总表的高效方案
如果你就是那个每月月底对着签到表挨个登记培训时长的人下面这个场景你一定不陌生几十个人、十几场培训得把每个人的参训日期、培训主题、签到签退时间一条条从纸质签到单或者Excel截图里录进汇总表录到一半还有人跑来问“我的时长怎么少了一小时”。我上个月就是用Python写了个自动统计的小脚本把整套手工流程换成了三步导入签到表、跑一遍代码、导出汇总表。这篇文章完整记录我是怎么拆解需求、整理数据、写代码以及落地时踩过的几个坑适合所有想把自己从重复登记里解放出来的HR、培训专员和兼职管培训的同学。1. 培训时长统计为什么会让HR头大1.1 手动登记的三大痛点先算一笔账。假设公司一个月有15场培训每场30人参加一个人登记的耗时大约10秒不算后期核对光录入就是4500秒差不多75分钟。如果用的是纸质签到表还得先把纸上的名字输进电脑这个时间直接翻倍。更要命的是纯手工录入的眼睛疲劳问题——把13:30看成14:30、把张三签到时间填到李四那一行这类错误几乎每个月都会发生。被发现的时候你已经在错误的汇总表上又加了好几行数据返工成本非常高。第二个痛点是核对困难。老板问“这个季度研发部人均培训时长多少”如果你只有一张手工登记的总表最快的方式是先找部门名单再逐个匹配最后打开计算器慢慢加。这已经不是在“统计”而是在“考古”。第三个痛点是数据没法二次利用。手工汇总完的表格基本就为了填一个总数字。哪类培训参与率最高、哪个部门平均时长偏低、哪位讲师的课程被频繁跳过这些信息全部淹没了。而Python做自动化统计本质上不是让你“不用打字”而是把签到表变成一份随时可查、可透视、可分析的数据资产。1.2 从“登记表”到“数据表”的思维转换我接手培训统计的时候第一反应也是想着怎么写代码替换人工但真正卡住我的不是代码而是数据本身乱七八糟。签到表里既有纸质扫描件也有线上会议导出的Excel还有培训助理发来的Word名单。后来我把核心精力放在一件事上把所有来源的签到记录统一整理成一张标准结构的数据表。这张标准表的每行是一次“某人参加了某场培训”的事实每列是描述这次事实的字段比如姓名、部门、培训日期、签到时间、签退时间。人脑统计时是“看到名字找到对应行手工累加”而Python的做法是“按姓名分组对时长列求和”。前者是逐条对账后者是批量计算效率差异完全不在一个量级。所以在写任何代码之前先把“这张表长什么样”想清楚比急着装环境、调库更重要。2. 先把手头的签到表整理成机器能读的样子2.1 推荐的表头结构与字段规范我最终用的是下面这个结构你可以直接照搬字段示例说明姓名张三必须与花名册一致不能有空格和别名部门研发部统一部门名称别一会儿“研发”一会儿“研发部”培训主题新员工入职安全培训同一个主题尽量用同一个名称培训日期2024-11-08统一成YYYY-MM-DD格式签到时间2024-11-08 09:12:30精确到秒签退时间2024-11-08 11:30:00精确到秒备注下午场可留空用于标识上午场/下午场、线上/线下等这个结构的核心原则是“一行一个事实”。千万别把一个人的多场培训压缩进一行单元格里否则后面任何统计都很难做。我在第一次整理数据时发现有的同事把“参加过的三场培训”用顿号并列写在一个格子里这种表Python读进来以后根本没法直接分组最后还得回去拆行重录。2.2 常见脏数据有哪些Excel里的签到表脏数据的重灾区基本集中在五类时间格式混乱同一列里有人填“9:12”有人填“09:12:30”还有人填“上午9点12分”。Python处理这类文本虽然能解析但解析失败时整列会变成空值必须提前统一。合并单元格很多人习惯把同一个人的多场培训合并成一个单元格或者把日期列合并看起来整洁但读进pandas后只有左上角第一个有值其余全是NaN。姓名前后的空格Excel里看着是“张三”实际可能是“ 张三 ”或“张三 ”如果不清理groupby会把同一个人拆成三个组。全角与半角混用比如“下午”和“(下午)”中文输入法下很容易混入全角括号这在文本匹配时会变成完全不同的内容。列名不稳定这个月叫“签到时间”下个月叫“入场时间”Python脚本是按列名取数的列名一变就得改代码。2.3 快速用Excel预处理一遍数据即使你打算用Python自动化前期的数据清洗也建议先人工快速过一遍效率反而更高。我的做法是先把所有签到单录进一张Excel模板表然后做三件事选中“签到时间”和“签退时间”列在单元格格式里统一设置为“YYYY-MM-DD HH:MM:SS”避免显示异常。用Excel的查找替换功能把姓名列里肉眼可见的多余空格和全角符号清掉。取消所有合并单元格填充空值——同一个人同一场培训的信息就把上方的姓名、部门下拉填充下来。这个过程看起来朴素但它决定了后续代码的成败。我见过太多人一上来就写复杂脚本结果数据乱成一锅粥最后反而怀疑Python不好用。其实Python处理规范化表格的能力非常强前提是你喂给它的表格足够规范。提示不要把“数据清洗”理解为特别高深的事。在这里它就是“保证同一列格式一致、同一人名称一致、同一行信息完整”这三件事。3. 环境准备装Python和两个库全程十分钟3.1 Python安装时的两个关键勾选很多HR同事听到“装Python”就觉得头疼但实际操作比装微信复杂不了多少。去python.org下载最新稳定版安装时务必勾选“Add Python to PATH”这一项它的作用是让电脑上的命令行能直接识别python命令。如果不勾选你后面在终端里输入python会提示“不是内部或外部命令”那时候再回去补环境变量会绕不少弯路。第二个建议是“Install launcher for all users”也勾上这样以后多用户使用电脑时不会出现权限问题。安装完之后WinR打开运行窗口输入cmd回车进入黑底命令行界面输入python --version如果输出了类似Python 3.12.x的版本号说明安装成功。这个黑窗口就是你以后运行脚本的地方不用怕它其实只是一个对话界面。3.2 用pip安装pandas和openpyxlPython本身自带了一些基础能力但要处理Excel还得装第三方库。我在这个场景里只用两个库pandas负责数据读取、清洗、分组计算是自动化统计的核心引擎。openpyxl负责Excel文件的读写尤其是给汇总表调样式、改列宽时很顺手。在命令行里执行pip install pandas openpyxl如果下载速度慢可以加一个国内镜像源pip install pandas openpyxl -i https://pypi.tuna.tsinghua.edu.cn/simple等待进度条跑完环境就准备好了。这里多提一句有些教程会让你先建虚拟环境再用pip但对非专业开发者来说完全没必要全局装这两个库足够日常办公使用。3.3 验证环境是否就绪装完以后在命令行里输入python -c import pandas; print(pandas.__version__)如果能打印出版本号比如“2.2.1”说明环境一切正常。这个验证步骤虽然简单但能帮你把“代码写完了才发现库没装”这种尴尬彻底消灭在开始之前。我第一次写脚本时跳过了验证结果写了半天代码才发现pandas没装上去浪费了将近二十分钟。4. 自动统计培训时长的核心代码4.1 读取Excelpandas怎么把登记表读进来环境就绪后先把代码文件和数据文件放在同一个文件夹里。假设签到表文件名是“培训签到记录.xlsx”工作表名为“签到记录”用三行代码就能读进Pythonimport pandas as pd df pd.read_excel(培训签到记录.xlsx, sheet_name签到记录) print(df.head())print(df.head())打印前几行是为了确认数据读进来了、列名是否跟预期一致。这里我见过最典型的报错是“Excel file format cannot be determined”大概率是文件其实是CSV文件但后缀改成了xlsx或者文件被其他程序占用。解决方案是另存为真正的“Excel工作簿”格式再读。4.2 解析时间字段文本变计算单位的两个坑原始签到表里的时间是文本Python无法直接做减法。关键一步是用pd.to_datetime把文本转成标准时间对象df[签到时间] pd.to_datetime(df[签到时间]) df[签退时间] pd.to_datetime(df[签退时间])这一步看起来简单但有两个坑必须处理。第一如果某一列里混入了无法解析的文本比如有人填了“9点半”pd.to_datetime会直接报错。稳妥的做法是加参数errorscoerce这样解析失败的会变成空值而不是中断程序df[签到时间] pd.to_datetime(df[签到时间], errorscoerce) df[签退时间] pd.to_datetime(df[签退时间], errorscoerce)第二Excel里有些“看起来是时间”的单元格实际上存储的是浮点数序列比如45238.38这种。这种情况下pd.to_datetime有时能自动识别有时不能。如果遇到解析异常可以在读取时先把该列指定为字符串然后再手动转换这里不展开后面第六章会详细讲。4.3 计算每个人的累计时长groupby聚合时间解析成功后计算时长就很简单df[时长_分钟] (df[签退时间] - df[签到时间]).dt.total_seconds() / 60两个时间相减会得到一个“时间差”对象.dt.total_seconds()换算成秒再除以60就得到分钟数。这里不用小时是因为分钟精度更高汇总后再转小时更灵活。按人汇总的代码是summary df.groupby(姓名, as_indexFalse)[时长_分钟].sum() summary[累计小时] (summary[时长_分钟] / 60).round(2)groupby可以理解成Excel里的“分类汇总”按“姓名”分组对“时长_分钟”列求和。as_indexFalse是为了让姓名变成普通列而不是索引后面导出时表头更干净。round(2)保留两位小数避免出现“47.1666666667小时”这种难看的结果。如果你还想要部门维度可以在groupby里加列名summary_by_dept df.groupby([部门], as_indexFalse)[时长_分钟].sum() summary_by_dept[累计小时] (summary_by_dept[时长_分钟] / 60).round(2)4.4 导出汇总表计算完成后用一行代码导出Excelsummary.sort_values(累计小时, ascendingFalse).to_excel(培训时长汇总表.xlsx, indexFalse)indexFalse表示不把行索引写进Excel否则文件第一列会出现一堆无意义的数字。sort_values则让时长最长的人排在最前面方便领导一眼看到排名。完整跑通的脚本只有十几行效果却非常直接原来一小时手工登记的工作现在从读入到导出Excel全程不超过三秒。第一次跑通的时候我自己都有点不真实感特意对着Excel里的数字手工验算了三个人确认分毫不差后才敢正式发给业务部门。5. 围绕真实场景的进阶处理5.1 培训有休息时间怎么扣减很多培训不是连续进行的比如上午9点到12点午休一小时下午14点到17点。如果签到表里只记录了一个总的签到时间和一个总的签退时间直接用减法会把午餐时间也算进培训时长数据虚高。我处理这种情况有两种思路取决于原始数据长什么样。思路一干脆拆行。上午一场记录成“2024-11-08 09:00:00”到“2024-11-08 12:00:00”下午一场记录成“2024-11-08 14:00:00”到“2024-11-08 17:00:00”。这样根本不用做任何扣减逻辑两条记录各自算时长后汇总即可。这也是我推荐的方式因为它保留了“场次”粒度后续想看每个时间段谁参加了随时能拆出来。思路二不拆行用固定时长扣减。如果签到表里本来就是一条大记录那只能按规则扣。比如规定中午休息固定1小时可以在算完总时长后统一减去60分钟。写成代码就是df[应扣减分钟] df[培训主题].apply(lambda x: 60 if 全天 in str(x) else 0) df[实际时_分钟] df[时长_分钟] - df[应扣减分钟]但“固定扣减”的前提是每个人进出午休的时间都一致实际情况往往对不上所以能用思路一就尽量用思路一。5.2 有人漏签退怎么标记而不是乱算现实里总有同事提前走或者忘了签退签退时间那一列就会出现空值。如果不处理相减后得到的是空值汇总时这个人会被直接跳过累计时长少一大截。我的处理方式是保留数据但单独标记出来让人工确认。df.loc[df[签退时间].isna(), 异常标记] 缺少签退时间需人工确认这样导出的明细表里凡是异常的人都会有一个显眼的标记。你拿着这张表去问本人“你这场培训几点走的”补上后再重新跑一遍脚本就比手工去翻签到单快得多。5.3 多场次多月份的合并统计如果公司每个月都有十几场培训你手里会有12张Excel表。千万别把每张表单独跑一遍再把结果抄进总表正确做法是循环读取、合并计算。用glob模块可以一行找出所有符合条件的文件import glob files glob.glob(培训签到_*.xlsx) df_all pd.concat([pd.read_excel(f) for f in files], ignore_indexTrue)只要你的文件名有规律比如“培训签到_2024_01.xlsx”这段代码就能把所有文件一次性合并成一个大表然后直接走groupby汇总。第一次跑通批量合并的时候我发现有些同事在不同月份里的部门名称又变了所以建议在正式汇总前先看一眼df_all[部门].unique()把模糊的地方提前统一掉。5.4 按部门透视给领导一张漂亮报表领导想看的不只是“谁最长”更常见的需求是“哪个部门整体参与度高”。这时候用透视函数pivot_table最方便pivot pd.pivot_table( df_all, values时长_分钟, index部门, aggfunc{时长_分钟: [sum, mean, count]} ) pivot.columns [总时长_分钟, 人均时长_分钟, 参与人次] pivot[总时长_小时] (pivot[总时长_分钟] / 60).round(2) pivot[人均时长_小时] (pivot[人均时长_分钟] / 60).round(2) pivot pivot.sort_values(总时长_分钟, ascendingFalse) pivot.to_excel(部门培训时长透视.xlsx)sum给出部门总时长mean给出人均时长count给出参与人次。一张透视表基本覆盖了领导问得最多的三个问题哪个部门学得多、平均每个人学了多少、哪个部门参与率最低。6. 落地时最容易踩的坑我替你踩过了6.1 时间格式被Excel偷偷改掉这是我在真实数据里遇到的最隐蔽的坑。Excel表格里看起来清清楚楚的“2024/11/8 9:12”实际存储的可能是浮点数比如45334.383333。这种单元格读进pandas后pd.to_datetime可能解析出完全错误的时间甚至直接报错。一个稳妥的解决方法是读取时把时间列先按字符串读进来然后再手动指定格式转换df pd.read_excel( 培训签到记录.xlsx, dtype{签到时间: str, 签退时间: str} ) df[签到时间] pd.to_datetime(df[签到时间], format%Y-%m-%d %H:%M:%S, errorscoerce)但format参数必须跟你的实际格式严格匹配否则会全部转成空值。如果你不确定格式最保险的办法是先在Excel里把该列统一设置成“文本”格式再手动输一遍或者在Excel里用“分列”功能把格式彻底固化。注意运行代码前备份原始签到表。数据清洗出错时至少还有一份原始文件可以回退不用从头开始录。6.2 文件名、路径、编码问题Windows系统里跑Python中文文件名和中文路径是最容易出问题的环节。比如“培训签到记录.xlsx”在导入时老版本pandas可能会有编码识别失败的情况但新版基本已经解决。如果你遇到UnicodeDecodeError大概率不是Excel读取问题而是打开或保存CSV文件时编码没选对。我的建议是把脚本、Excel文件都放在同一个英文路径的文件夹里比如D:\hr_training_stat文件名尽量用英文或拼音如training_records.xlsx。虽然这不是必须的但能少掉一大批让人抓狂的编码问题。代码文件本身用VS Code或者记事本保存时选择UTF-8编码即可。6.3 合并单元格读取后的空值原始表里最常见的“美观陷阱”就是合并单元格。比如一个同事参加了两场培训Excel里把他的姓名合并成一个格子看起来很整洁。但pandas读进来以后第二行“姓名”列是空值NaN直接groupby的话这个人会被单独拆成一个“无名氏”组时长全算到NaN头上导致最终汇总表里多出一行空行。识别方式很简单读完后执行print(df[姓名].isna().sum())如果数量大于0说明存在合并单元格或漏填。处理方法是前向填充——把上方同一个人的值填充到空白行df[姓名] df[姓名].ffill() df[部门] df[部门].ffill()这种“前向填充”逻辑说起来有点学术但你可以理解为每一行如果姓名和部门是空的就自动抄上面的一行。它完美适配了合并单元格被拆开后的场景。6.4 别盲目相信汇总数据留好核对样本自动化脚本不是“跑完就完事”的魔法。我第一次跑通完整流程后做了一件事从结果里随机抽了5个人手工去原签到表里数这5个人这个月到底参加了几场、每场多久加总后跟脚本结果对比。确认完全一致后才正式投入使用。从那以后每次调整脚本逻辑我都会保留一份“黄金样本”做回归验证。所谓的黄金样本就是某个月三到五个人的人工核对结果。脚本只要改了数据清洗规则或时间解析逻辑就用这份样本再测一遍确保新逻辑没把原来跑对的数据搞坏。这个习惯后来帮我避免了好几次因为调整“休息时长扣减”规则导致的整体数据偏差。另外所有生成的汇总Excel我都会复制一份原签到表备份文件按月份归档文件名叫“原始数据_2024年11月_备份.xlsx”。一旦有人质疑数据随时可以从原始数据重新生成一遍而不是只能对着结果表解释“这数怎么来的”。这一点在跟业务部门和财务对账时特别有底气。最后再说说这套流程的边界如果你所在的公司已经上线了企业培训系统系统本身就能导出每个人的学时明细那你可能不需要从零造轮子直接在系统里做导出和分析就行。但如果你和我当初一样手里只有一堆格式各异的Excel签到表那么这个“Excel数据规范 pandas自动聚合”的思路绝对值得动手试一次。代码就摆在上面的章节里复制下来把自己公司的表头改一改名基本就能用。第一次跑通之后你可能会发现培训时长统计这件事终于不是“月末恐怖的代名词”了甚至还有余力多做一张“各部门培训投入对比图”那画面比在表格里一个个数名字舒服多了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →