本文介绍如何使用 pandas 对按日期和人员分组的不规则进出记录(in/out)进行清洗、配对与时间差计算,精准提取所有中间进出对(如 in→out→in→out 中的第2、3对),并汇总为总秒数。
本文介绍如何使用 pandas 对按日期和人员分组的不规则进出记录(in/out)进行清洗、配对与时间差计算,精准提取所有中间进出对(如 in→out→in→out 中的第2、3对),并汇总为总秒数。
在考勤分析、工时统计或门禁日志处理中,常遇到同一人在单日内多次进出的场景(例如:07:30进 → 09:00出 → 09:30进 → 16:00出)。若仅需统计“中间段”的停留时长(即排除首进前与末出后的无效区间,且只计算成对的 IN→OUT 区间),直接用 diff() 或 shift() 简单配对会出错——因为原始数据未必严格交替、有序或完整。
核心思路是:先确保每条记录可被唯一配对(IN 后紧跟 OUT),再提取所有非首尾的 IN 行所对应的前向时间差(即该 IN 到上一 OUT 的间隔),最后求和。但注意:题目中“中间时间条目”实际指 所有 IN 行(除首次外)与其前一个 OUT 行之间的时间差之和 —— 这正是示例答案中 g['dt'].diff().iloc[1:-1][g['In/Out'].eq('IN')] 的逻辑本质:对时间序列做一阶差分后,diff()[i] 表示第 i 行与第 i−1 行的时间差;iloc[1:-1] 跳过首行(无前值)与末行(避免跨人/跨日干扰),再用 g['In/Out'].eq('IN') 筛选出「当前行为 IN」的位置,即这些位置的 diff 值恰好代表「上一 OUT 到本次 IN」的空档?等等——这与直觉矛盾。
⚠️ 实际细读答案代码与示例数据发现:作者此处存在术语歧义。“Intermediate time entries” 在题干表格中高亮的是 Homer 的第二段 IN (10:00) 和 Bart 的第二、三段 IN/OUT 组合,但最终目标输出却是 每组内所有成对 IN→OUT 的持续时间之和(排除首段)。观察示例输出:
- Homer:09:30 IN → 16:00 OUT ⇒ 6.5 小时 = 23400 秒?但期望是 1800(30 分钟)→ 对应 09:30–10:00?不对。
再核对原始问题表格高亮行:
→ Homer 高亮 OUT (09:30), IN (10:00), OUT (16:00) → 中间段实为 09:30–10:00(30 min) + 10:00–16:00(6h)?但 30min=1800s ✅,即只取首个 OUT 到下一个 IN 之间的间隙?不,1800s 是 30 分钟,而 09:30→10:00 正好 30 分钟 —— 但这是“空档”,不是“在场时长”。
✅ 正确解读(结合代码与输出):
题目中“intermediate time entries” 指 所有非首段的 IN→OUT 时间对。Homer 有两段:
- 第一段:07:10 IN → 09:30 OUT(首段,排除)
- 第二段:10:00 IN → 16:00 OUT ⇒ 6h = 21600s ≠ 1800s。
仍不符。
? 关键线索在答案代码:
g['dt'].diff().iloc[1:-1][g['In/Out'].eq('IN')]对 Homer 时间列 ['07:10','09:30','10:00','16:00'] 转 datetime 后 diff 得:
[NaT, 02:20:00, 00:30:00, 06:00:00]
iloc[1:-1] → [02:20:00, 00:30:00]
其中 g['In/Out'] == 'IN' 的索引是第2行(10:00)和第3行(16:00?但 16:00 是 OUT)→ 实际 In/Out 列为 ['IN','OUT','IN','OUT'],所以 eq('IN') 在索引0和2成立;iloc[1:-1] 取索引1、2 → 对应 OUT 和 IN 行;只有索引2(IN)满足条件 → 取 00:30:00 = 1800s ✅。
即:计算每个 IN 行与其前一行(必为 OUT)的时间差,并仅累加这些差值。这恰好是“从上一次离开到本次进入”的空档时间 —— 但题干说“total timedelta of intermediate time entries”,而示例答案输出 1800 和 2700(45min)正对应 Homer 的 09:30→10:00 和 Bart 的 10:00→10:15 + 12:00→12:30?Bart 高亮 OUT(10:00), IN(10:15), OUT(12:00), IN(12:30) → 两个空档:15min + 30min = 45min = 2700s ✅。
✔️ 结论:本教程解决的问题是——计算每人每日所有“中途再次进入”前的空档时长总和(即每个非首 IN 行减去其前一 OUT 行的时间差之和)。
以下是稳健实现步骤:
✅ 步骤 1:数据预处理
统一大小写、转换时间为 datetime 类型(仅含时分):
df['In/Out'] = df['In/Out'].str.upper() df['dt'] = pd.to_datetime(df['Time'], format='%H:%M')
✅ 步骤 2:按日期+姓名分组,逐组计算空档和
关键逻辑:
- 对每组内时间排序(确保时序正确);
- 计算时间列一阶差分(diff()),得到相邻记录时间差;
- 取 iloc[1:-1] 排除首行(无前值)和末行(防跨组干扰);
- 筛选当前行为 'IN' 的位置,取出对应差值(即该 IN 减前一行 OUT 的间隔);
- 求和并转为秒数。
result = (df
.assign(dt=pd.to_datetime(df['Time'], format='%H:%M'))
.sort_values(['Date', 'Name', 'dt']) # 强制保序
.groupby(['Date', 'Name'])
.apply(lambda g:
g['dt'].diff().iloc[1:-1]
[g['In/Out'].eq('IN').iloc[1:-1]] # 安全对齐索引
.sum().total_seconds() if len(g) > 2 else 0)
.reset_index(name='TimeDelta')
.query('TimeDelta > 0')
)? 提示:.iloc[1:-1] 需同步作用于布尔索引,避免长度不匹配。更健壮写法是重置组内索引:
lambda g: (g.assign(diff=g['dt'].diff()) .iloc[1:-1] .query("`In/Out` == 'IN'") ['diff'].sum().total_seconds())
✅ 步骤 3:异常处理建议
- 若存在连续 IN 或 OUT,需先清洗:df = df.sort_values(['Date','Name','dt']).drop_duplicates(['Date','Name','In/Out','Time']);
- 使用 shift(-1) 验证配对有效性:df['next_io'] = df.groupby(['Date','Name'])['In/Out'].shift(-1),保留 In/Out=='IN' & next_io=='OUT' 的行对;
- 对于无配对的孤立记录,diff() 会产生 NaT,.sum() 会自动忽略,无需额外 fillna(0)。
? 总结
本方法利用 Pandas 的向量化差分与布尔索引,在无需显式循环或复杂状态机的前提下,高效提取多段进出记录中的“中间空档”总时长。适用于考勤补卡分析、设备待机间隙统计等场景。核心在于理解 diff() 的物理意义与索引切片的边界控制——iloc[1:-1] 是安全计算相邻有效对的关键防线。

















