【问题标题】:Efficient date range overlap calculation in python?python中有效的日期范围重叠计算?
【发布时间】:2023-03-26 17:24:01
【问题描述】:

我有两个日期范围,每个范围都由开始日期和结束日期确定(显然是 datetime.date() 实例)。这两个范围可以重叠也可以不重叠。我需要重叠的天数。当然,我可以用两个范围内的所有日期预先填充两组并执行一组交集,但这可能效率低下……除了使用涵盖所有情况的长 if-elif 部分的另一种解决方案之外,还有更好的方法吗?

【问题讨论】:

标签: python date date-range


【解决方案1】:
  • 确定两个开始日期中的最晚日期和两个结束日期中最早的日期。
  • 通过减去它们来计算时间增量。
  • 如果增量为正,则表示重叠天数。

这是一个计算示例:

>>> from datetime import datetime
>>> from collections import namedtuple
>>> Range = namedtuple('Range', ['start', 'end'])

>>> r1 = Range(start=datetime(2012, 1, 15), end=datetime(2012, 5, 10))
>>> r2 = Range(start=datetime(2012, 3, 20), end=datetime(2012, 9, 15))
>>> latest_start = max(r1.start, r2.start)
>>> earliest_end = min(r1.end, r2.end)
>>> delta = (earliest_end - latest_start).days + 1
>>> overlap = max(0, delta)
>>> overlap
52

【讨论】:

  • +1 非常好的解决方案。但是,这对于完全包含在另一个日期中的日期并不完全有效。为简单起见整数: Range(1,4) 和 Range(2,3) 返回 1
  • @darkless 实际上,它返回 2,这是 正确。试试这些输入r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3))。我认为您在重叠计算中错过了+1(这是必要的,因为间隔在两端都是封闭的)。
  • 如果你想计算 2 次而不是 2 个日期怎么办? @RaymondHettinger
  • 如果你使用带时间的日期时间对象,你可以用 .total_seconds() 代替 .days。
【解决方案2】:

函数调用比算术运算更昂贵。

最快的方法是 2 次减法和 1 min():

min(r1.end - r2.start, r2.end - r1.start).days + 1

与需要 1 个减法、1 个 min() 和一个 max() 的次优相比:

(min(r1.end, r2.end) - max(r1.start, r2.start)).days + 1

当然,对于这两个表达式,您仍然需要检查是否存在正重叠。

【讨论】:

  • 此方法不会总是返回正确答案。例如Range = namedtuple('Range', ['start', 'end']) r1 = Range(start=datetime(2016, 6, 15), end=datetime(2016, 6, 15)) r2 = Range(start=datetime(2016, 6, 11), end=datetime(2016, 6, 18)) print min(r1.end - r2.start, r2.end - r1.start).days + 1 将在应该打印 1 的地方打印 4
  • 我使用第一个方程得到了一个模棱两可的系列错误。我需要一个特定的库吗?
【解决方案3】:

我实现了一个 TimeRange 类,如下所示。

get_overlapped_range首先通过一个简单的条件否定所有不重叠的选项,然后通过考虑所有可能的选项来计算重叠范围。

要获取天数,您需要获取从 get_overlapped_range 返回的 TimeRange 值并将持续时间除以 60*60*24。

class TimeRange(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.duration = self.end - self.start

    def is_overlapped(self, time_range):
        if max(self.start, time_range.start) < min(self.end, time_range.end):
            return True
        else:
            return False

    def get_overlapped_range(self, time_range):
        if not self.is_overlapped(time_range):
            return

        if time_range.start >= self.start:
            if self.end >= time_range.end:
                return TimeRange(time_range.start, time_range.end)
            else:
                return TimeRange(time_range.start, self.end)
        elif time_range.start < self.start:
            if time_range.end >= self.end:
                return TimeRange(self.start, self.end)
            else:
                return TimeRange(self.start, time_range.end)

    def __repr__(self):
        return '{0} ------> {1}'.format(*[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(d))
                                          for d in [self.start, self.end]])

【讨论】:

    【解决方案4】:

    您可以使用 datetimerange 包:https://pypi.org/project/DateTimeRange/

    from datetimerange import DateTimeRange
    time_range1 = DateTimeRange("2015-01-01T00:00:00+0900", "2015-01-04T00:20:00+0900") 
    time_range2 = DateTimeRange("2015-01-01T00:00:10+0900", "2015-01-04T00:20:00+0900")
    tem3 = time_range1.intersection(time_range2)
    if tem3.NOT_A_TIME_STR == 'NaT':  # No overlap
        S_Time = 0
    else: # Output the overlap seconds
        S_Time = tem3.timedelta.total_seconds()
    

    DateTimeRange() 中的“2015-01-01T00:00:00+0900”也可以是日期时间格式,如 Timestamp('2017-08-30 20:36:25')。

    【讨论】:

    • 谢谢,刚刚查看了DateTimeRange 包的文档,似乎他们支持is_intersection,它根据是否存在交叉点本机返回布尔值(真或假)在两个日期范围之间。因此,对于您的示例:time_range1.is_intersection(time_range2) 将返回 True 如果它们相交否则 False
    • intersection() 返回一个 DateTimeRange 对象,其属性 NOT_A_TIME_STR 始终等于 'NaT',因此 if 条件始终为真。更好的方法是使用返回 True 或 False 的 is_intersection。
    【解决方案5】:

    伪代码:

     1 + max( -1, min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart) )
    

    【讨论】:

      【解决方案6】:

      基于@Raymond Hettinger 的解决方案,从python 3.6 开始,您现在可以使用typing 模块中的NamedTuple

      from datetime import datetime
      from typing import NamedTuple
      
      class Range(NamedTuple):
          start: datetime
          end: datetime
      
      >>> r1 = Range(start=datetime(2012, 1, 15), end=datetime(2012, 5, 10))
      >>> r2 = Range(start=datetime(2012, 3, 20), end=datetime(2012, 9, 15))
      >>> latest_start = max(r1.start, r2.start)
      >>> earliest_end = min(r1.end, r2.end)
      >>> delta = (earliest_end - latest_start).days + 1
      >>> overlap = max(0, delta)
      >>> overlap
      

      【讨论】:

        【解决方案7】:
        def get_overlap(r1,r2):
            latest_start=max(r1[0],r2[0])
            earliest_end=min(r1[1],r2[1])
            delta=(earliest_end-latest_start).days
            if delta>0:
                return delta+1
            else:
                return 0
        

        【讨论】:

          【解决方案8】:

          好吧,我的解决方案有点不稳定,因为我的 df 使用了所有系列 - 但假设您有以下列,其中 2 列是固定的,即您的“财政年度”。 PoP 是“性能周期”,它是您的可变数据:

          df['PoP_Start']
          df['PoP_End']
          df['FY19_Start'] = '10/1/2018'
          df['FY19_End'] = '09/30/2019'
          

          假设所有数据都是日期时间格式,即 -

          df['FY19_Start'] = pd.to_datetime(df['FY19_Start'])
          df['FY19_End'] = pd.to_datetime(df['FY19_End'])
          

          尝试以下等式找出重叠天数:

          min1 = np.minimum(df['POP_End'], df['FY19_End'])
          max2 = np.maximum(df['POP_Start'], df['FY19_Start'])
          
          df['Overlap_2019'] = (min1 - max2) / np.timedelta64(1, 'D')
          df['Overlap_2019'] = np.maximum(df['Overlap_2019']+1,0)
          

          【讨论】:

            【解决方案9】:

            另一种解决方案是通过先升序然后循环并比较日期来对源数组进行排序,如下所示:

            date_ranges = sorted(
                date_ranges,
                key=lambda item: item['start_date'],
            )
            for i in range(len(date_ranges)-1):
                if date_ranges[i]['end_date'] > date_ranges[i+1]['start_date']:
                    raise Exception('Overlap'})
            

            【讨论】:

              猜你喜欢
              • 2020-02-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2023-01-19
              • 1970-01-01
              相关资源
              最近更新 更多