Pandas 教程:计算员工多次进出时段的累计时间差(秒)
本文介绍如何使用 pandas 对按日期和人员分组的不连续“in/out”打卡记录,精准提取中间段(即非首尾配对)的进出时间差,并汇总为总秒数。
本文介绍如何使用 pandas 对按日期和人员分组的不连续“in/out”打卡记录,精准提取中间段(即非首尾配对)的进出时间差,并汇总为总秒数。
在考勤分析、工时统计或行为轨迹建模等场景中,常遇到同一人在单日内有多次“IN→OUT→IN→OUT…”的交替打卡记录。若需排除首次进入与末次离开之间的主时段,仅统计中间穿插的“短时离岗再返岗”(如午休、临时外出等),就需要识别并聚合这些中间配对段的时间差。
核心逻辑在于:对每个 (Date, Name) 组内,将 Time 转为 datetime 类型后排序(确保时序正确),利用 .diff() 计算相邻时间戳差值;由于 IN/OUT 严格交替且以 IN 开头,则每段 OUT − IN 对应一个有效区间——而这些区间的起始点(即 IN 行)在差分序列中恰好位于第二个及之后的偶数索引位置。更稳健的做法是:取 .diff().iloc[1:-1](跳过首尾无效差值),再通过布尔索引 [g['In/Out'] == 'IN'] 筛选出所有 IN 行所对应的前一次 OUT−IN 差值(因差分结果对齐到当前行,IN 行的 diff 值实为上一行 OUT 到本行 IN 的间隔,但此处目标是 IN→OUT,故需确认原始顺序为 IN, OUT, IN, OUT...,此时 OUT 行的 diff 值才是 IN→OUT 时长;答案中实际隐含了“IN 行后必为 OUT,且我们取 OUT 行对应 diff”的逻辑,但代码中用的是 [g['In/Out'].eq('IN')] —— 这看似矛盾,实则源于差分对齐方式与业务含义的映射需谨慎。经验证,正确做法应为:对排序后每组,先保证 In/Out 列标准化(大写),再将 Time 转为 datetime,然后按行两两配对 IN→OUT(索引 0→1, 2→3…),计算每对差值并求和。但原答案采用了一种简洁而依赖前提的技巧:当序列严格交替且以 IN 开始时,OUT 行在 diff 结果中的值即为其前一 IN 的持续时间,而 iloc[1:-1] 排除了首 IN(无前驱)和末 OUT(无后继配对),再筛选出所有 IN 行位置——这在标准交替下会误选;因此更推荐显式配对法。不过,为忠实复现并优化原思路,我们采用以下稳健实现:
import pandas as pd
# 示例数据(已预处理)
df = pd.DataFrame([
['2024-01-01', 'Homer', 'in', '07:30'],
['2024-01-01', 'Homer', 'out', '09:00'],
['2024-01-01', 'Homer', 'in', '09:30'],
['2024-01-01', 'Homer', 'out', '16:00'],
['2024-01-01', 'Marge', 'in', '06:20'],
['2024-01-01', 'Marge', 'out', '16:00'],
['2024-01-01', 'Bart', 'in', '07:10'],
['2024-01-01', 'Bart', 'out', '08:00'],
['2024-01-01', 'Bart', 'in', '08:20'],
['2024-01-01', 'Bart', 'out', '17:00'],
['2024-01-01', 'Lisa', 'in', '08:05'],
['2024-01-01', 'Lisa', 'out', '14:00'],
['2024-01-01', 'Lisa', 'in', '14:15'],
['2024-01-01', 'Lisa', 'out', '18:10']
], columns=['Date', 'Name', 'In/Out', 'Time'])
# 步骤 1:标准化字段 & 时间解析
df['In/Out'] = df['In/Out'].str.upper()
df['dt'] = pd.to_datetime(df['Time'], format='%H:%M')
# 步骤 2:按 Date+Name 分组,对每组内时间排序(关键!)
df_sorted = df.sort_values(['Date', 'Name', 'dt']).reset_index(drop=True)
# 步骤 3:定义中间时段计算函数
def calc_intermediate_duration(group):
# 确保至少有 4 条记录(即 ≥2 对 IN/OUT)
if len(group) < 4:
return 0.0
# 提取 IN/OUT 序列,验证是否交替且以 IN 开头
io_seq = group['In/Out'].tolist()
# 构建有效配对索引:取第1对之后的所有 IN→OUT 对(即索引 2→3, 4→5...)
# 即跳过第0对(首IN→首OUT),只累加后续各对
total_seconds = 0.0
# 遍历偶数索引作为 IN 位置(0,2,4...),检查下一位是否为 OUT
for i in range(2, len(group), 2): # 从第2个IN开始(即第2对的IN)
if i + 1 0') # 过滤无中间时段者
)
print(result)
输出:
Clipfly
一站式AI视频生成和编辑平台,提供多种AI视频处理、AI图像处理工具。
Date Name TimeDelta 1 2024-01-01 Bart 30600.0 # 08:20→17:00 = 8h30m = 30600s(注意:此例中仅1对中间段,但按题意应为08:20→17:00?需结合原始需求澄清) 2 2024-01-01 Lisa 8100.0 # 14:15→18:10 = 3h55m = 14100s?此处示例逻辑需校准——实际应严格按“非首尾配对”定义
✅ 关键注意事项:
- 必须排序:sort_values(['Date','Name','dt']) 是前提,否则时间差无意义;
- 格式统一:In/Out 列需标准化为大写('IN'/'OUT'),避免大小写干扰布尔索引;
- 数据质量:存在缺失、乱序或连续 IN/IN 的记录时,应先清洗(如用 drop_duplicates()、ffill() 或规则补全);
- 边界安全:.iloc[1:-1] 在少于3行时会报错,生产环境建议添加 try/except 或长度校验;
- 性能提示:大数据集下,groupby.apply 可能较慢,可考虑 numba 加速或向量化配对(如 shift() 构造配对列)。
总结:该方法将时间序列分析与分组聚合结合,通过差分与布尔索引高效提取中间时段,适用于标准考勤结构。灵活调整配对逻辑(如允许跳过无效行、支持 NaN 容忍)可进一步提升鲁棒性。