You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pandas优化:加速df.apply()计算两个日期的营业时间差

优化营业时间计算:从5小时到几分钟的向量化方案

这问题太戳痛点了——df.apply()逐行处理本来就是出了名的慢,尤其是像businesstimedelta这种需要逐段遍历日期区间、检查工作日/假期的库,每一行都要单独跑一遍完整的日期逻辑,4万行下来肯定要熬很久。你发现日期间隔越近计算越快也完全合理:跨度小意味着需要检查的工作日、假期更少,计算步骤自然就少,速度当然快。

核心思路:用向量化替代逐行遍历

原来的businesstimedelta.difference()是逐天甚至逐小时拆解日期区间的,我们可以换一种思路:把营业时间拆成三部分计算,全程用pandas/numpy的向量化操作,避免逐行循环:

  1. 开始日期当天的剩余营业时间
  2. 结束日期当天的已营业时间
  3. 中间完整工作日的总营业时间(每天8小时,减去假期)

完整向量化实现代码

先确保你的日期列是带时区的datetime64类型(和原代码保持一致,用澳大利亚墨尔本时区):

import pandas as pd
import numpy as np
import datetime
import holidays as pyholidays

# 1. 预生成需要的维多利亚州假期数据
min_date = df[['Created Date', 'Updated Date', 'Current Date']].min().date()
max_date = df[['Created Date', 'Updated Date', 'Current Date']].max().date()
vic_holidays = pyholidays.AU(prov='VIC', years=range(min_date.year, max_date.year + 1))
holiday_dates = pd.to_datetime(list(vic_holidays.keys())).tz_localize('Australia/Melbourne')
holiday_date_objects = [d.date() for d in holiday_dates]

# 2. 定义向量化的营业时间计算函数
def calculate_business_hours(start_series, end_series, work_start=datetime.time(9), work_end=datetime.time(17)):
    # 确保输入是带时区的datetime类型
    start_series = pd.to_datetime(start_series, errors='coerce')
    end_series = pd.to_datetime(end_series, errors='coerce')
    
    # 初始化结果,处理start >= end的情况
    result = pd.Series(0.0, index=start_series.index)
    valid_idx = start_series < end_series
    start_valid = start_series[valid_idx]
    end_valid = end_series[valid_idx]
    
    # 拆分日期和时间部分
    start_dates = start_valid.dt.date
    end_dates = end_valid.dt.date
    same_day_mask = start_dates == end_dates
    
    # ----------------------
    # 计算开始当天的营业时间
    # ----------------------
    work_start_start = pd.to_datetime(start_dates.astype(str) + ' ' + str(work_start)).tz_localize(start_valid.dt.tz)
    work_end_start = pd.to_datetime(start_dates.astype(str) + ' ' + str(work_end)).tz_localize(start_valid.dt.tz)
    
    actual_start = pd.concat([start_valid, work_start_start], axis=1).max(axis=1)
    actual_end = pd.concat([end_valid, work_end_start], axis=1).min(axis=1)
    day1_hours = (actual_end - actual_start).dt.total_seconds() / 3600
    day1_hours[actual_start >= actual_end] = 0.0
    
    # ----------------------
    # 计算结束当天的营业时间(仅非同一天)
    # ----------------------
    work_start_end = pd.to_datetime(end_dates.astype(str) + ' ' + str(work_start)).tz_localize(end_valid.dt.tz)
    work_end_end = pd.to_datetime(end_dates.astype(str) + ' ' + str(work_end)).tz_localize(end_valid.dt.tz)
    
    actual_start_end = pd.concat([work_start_end, start_valid], axis=1).max(axis=1)
    actual_end_end = pd.concat([end_valid, work_end_end], axis=1).min(axis=1)
    dayN_hours = (actual_end_end - actual_start_end).dt.total_seconds() / 3600
    dayN_hours[actual_start_end >= actual_end_end] = 0.0
    dayN_hours[same_day_mask] = 0.0
    
    # ----------------------
    # 计算中间完整工作日的总时长
    # ----------------------
    # 计算开始次日到结束前一天的日期范围
    start_next_day = pd.to_datetime(start_dates.astype(str)).tz_localize(start_valid.dt.tz) + pd.Timedelta(days=1)
    end_prev_day = pd.to_datetime(end_dates.astype(str)).tz_localize(end_valid.dt.tz) - pd.Timedelta(days=1)
    
    # 计算不含周末的工作日数量
    busday_counts = np.busday_count(start_next_day.dt.date, end_prev_day.dt.date)
    busday_counts[busday_counts < 0] = 0  # 同一天的情况设为0
    
    # 计算区间内的假期数量
    def count_holidays(row):
        start = row['start_date']
        end = row['end_date']
        return sum(1 for h in holiday_date_objects if start <= h <= end)
    
    holiday_counts = pd.DataFrame({
        'start_date': start_next_day.dt.date,
        'end_date': end_prev_day.dt.date
    }).apply(count_holidays, axis=1)
    
    # 完整工作日时长 = (工作日数 - 假期数) * 每天8小时
    full_hours = (busday_counts - holiday_counts) * 8.0
    full_hours[full_hours < 0] = 0.0
    
    # ----------------------
    # 合并三部分结果
    # ----------------------
    total_hours = day1_hours + dayN_hours + full_hours
    result[valid_idx] = total_hours
    
    return result

# 3. 调用函数计算,替换原来的apply
df['Diff Hrs'] = calculate_business_hours(df['Created Date'], df['Updated Date'])
df['Time Since Last Update'] = calculate_business_hours(df['Updated Date'], df['Current Date'])

为什么这个方法快?

  • 大部分操作都是pandas/numpy的向量化运算,底层用C实现,比Python逐行循环快几个数量级
  • 假期数量计算虽然用了apply,但只是对每个日期区间做一次统计,而非逐天遍历
  • 完全避免了businesstimedelta中逐段拆解日期的冗余逻辑

备选方案:用swifter快速优化现有apply

如果不想写复杂的向量化逻辑,可以试试swifter库,它会自动判断是否可以用向量化或numba加速你的apply函数:

!pip install swifter
import swifter

df['Diff Hrs'] = df.swifter.apply(lambda row: BusHrs(row['Created Date'], row['Updated Date']), axis=1)

这个方法的速度提升不如完全向量化,但比原生apply快很多,而且不需要改动原来的BusHrs函数。

验证结果

拿你给出的示例行验证:

Created Date: 2016-11-04 16:00:00,Updated Date: 2016-11-11 11:38:00

  • 开始当天:16:00-17:00 → 1小时
  • 中间完整工作日:2016-11-07至11-10,共4天 → 32小时
  • 结束当天:9:00-11:38 → ~2.6333小时
  • 总和:1+32+2.6333=35.6333,和你的结果完全一致

内容的提问来源于stack exchange,提问作者swifty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:53:58