【问题标题】:datetime difference in python adjusted for night timepython中的日期时间差异为夜间调整
【发布时间】:2017-09-06 07:49:29
【问题描述】:

我在 python d1 和 d2 中有两个日期时间对象。我想拿他们之间的时间差。我想要比 (d1 - d2) 稍微复杂一点的东西:我希望夜间的时间比白天的时间少一个常数 c,例如晚上一小时只算白天半小时。

在 python (pandas 和/或 numpy) 中是否有一种简单的方法?

谢谢!

编辑:夜间时间是从晚上 9 点到早上 7 点。但理想情况下,我正在寻找一种解决方案,您可以在白天的任意时间段选择任意权重

【问题讨论】:

  • “夜晚”什么时候开始?
  • 您的第一步是定义“晚上”的时间。
  • 请定义夜晚
  • 说从晚上 9 点到早上 7 点。但理想情况下,我正在寻找一种解决方案,您可以在白天的任意时间段选择任意权重
  • 那么当一个人在夜间,而另一个人在外面时,这种神奇的差异如何适用于差异?或者有一次是在深夜里,或者可能只是在夜里?

标签: python pandas datetime numpy


【解决方案1】:

高级概念

  • 获取开始和结束时间戳。
  • 查找它们之间早上 7 点和晚上 9 点的所有实例
  • 创建一个排序的时间戳数组,包括开始、结束、所有上午 7 点、所有下午 9 点
  • 计算此数组的差异
  • 确定起点是白天还是黑夜
  • 总结差异,将适当的一半除以 2

import pandas as pd
import numpy as np

def weighted_delta(start, end, night_start=21, night_end=7):
    start, end = end_points = pd.to_datetime([start, end])
    rng = pd.date_range(start.date(), end.date() + pd.offsets.Day())
    evening = rng + pd.Timedelta(night_start, 'h')
    morning = rng + pd.Timedelta(night_end, 'h')
    rng = evening.union(morning).union(end_points)
    rng = np.clip(rng.values, start.value, end.value)
    rng = np.unique(rng)
    rng = pd.to_datetime(rng).sort_values()
    diffs = np.diff(rng)
    if night_end <= start.hour < night_begin:
        diff_sum = pd.Timedelta(diffs[::2].sum() + diffs[1::2].sum() / 2)
    else:
        diff_sum = pd.Timedelta(diffs[::2].sum() / 2 + diffs[1::2].sum())
    return diff_sum.total_seconds() 

weighted_delta('2017-01-01', '2017-01-03')

136800.0

【讨论】:

    【解决方案2】:

    一种解决方案,让您可以根据需要定义任意数量的时段,并使用它们各自的权重。

    首先,一个帮助函数分割我们的日期时间之间的间隔:

    from datetime import date, time, datetime, timedelta
    
    def slice_datetimes_interval(start, end):
        """
        Slices the interval between the datetimes start and end.
    
        If start and end are on different days:
        start time -> midnight | number of full days | midnight -> end time
        ----------------------   -------------------   --------------------
                   ^                     ^                      ^
              day_part_1             full_days              day_part_2
    
        If start and end are on the same day:
        start time -> end time
        ----------------------
                  ^
             day_part_1              full_days = 0
    
        Returns full_days and the list of day_parts (as tuples of time objects).
        """
    
        if start > end:
            raise ValueError("Start time must be before end time")
    
        # Number of full days between the end of start day and the beginning of end day
        # If start and end are on the same day, it will be -1
        full_days = (datetime.combine(end, time.min) - 
                     datetime.combine(start, time.max)).days
        if full_days >= 0:
            day_parts = [(start.time(), time.max),
                         (time.min, end.time())]
        else:
            full_days = 0
            day_parts = [(start.time(), end.time())]
    
        return full_days, day_parts
    

    计算给定周期和权重列表的加权持续时间的类:

    class WeightedDuration:
        def __init__(self, periods):
            """
            periods is a list of tuples (start_time, end_time, weight)
            where start_time and end_time are datetime.time objects.
    
            For a period including midnight, like 22:00 -> 6:30,
            we create two periods:
              - midnight (start of day) -> 6:30,
              - 22:00 -> midnight(end of day)
    
            so periods will be:
              [(time.min, time(6, 30), 0.5),
               (time(22, 0), time.max, 0.5)]
    
            """
            self.periods = periods
            # We store the weighted duration of a whole day for later reuse
            self.day_duration = self.time_interval_duration(time.min, time.max)
    
        def time_interval_duration(self, start_time, end_time):
            """ 
            Returns the weighted duration, in seconds, between the datetime.time objects
            start_time and end_time - so, two times on the *same* day.
            """
            dummy_date = date(2000, 1, 1)
    
            # First, we calculate the total duration, *without weight*.
            # time objects can't be substracted, so
            # we turn them into datetimes on dummy_date
            duration = (datetime.combine(dummy_date, end_time) -
                        datetime.combine(dummy_date, start_time)).total_seconds()
    
            # Then, we calculate the reductions during all periods
            # intersecting our interval
            reductions = 0
            for period in self.periods:
                period_start, period_end, weight = period
                if period_end < start_time or period_start > end_time:
                    # the period and our interval don't intersect
                    continue
    
                # Intersection of the period and our interval
                start = max(start_time, period_start)
                end = min (end_time, period_end)
    
                reductions += ((datetime.combine(dummy_date, end) -
                               datetime.combine(dummy_date, start)).total_seconds()
                               * (1 - weight))
            # as time.max is midnight minus a µs, we round the result
            return round(duration - reductions)
    
        def duration(self, start, end):
            """
            Returns the weighted duration, in seconds, between the datetime.datetime
            objects start and end.
            """
            full_days, day_parts = slice_datetimes_interval(start, end)
            dur = full_days * self.day_duration
            for day_part in day_parts:
                dur += self.time_interval_duration(*day_part)
            return dur
    

    我们创建一个 WeightedDuration 实例,定义我们的周期及其权重。 我们可以有任意多个周期,权重小于或大于 1。

    wd = WeightedDuration([(time.min, time(7, 0), 0.5),      # from midnight to 7, 50%
                           (time(12, 0), time(13, 0), 0.75), # from 12 to 13, 75%
                           (time(21, 0), time.max, 0.5)])    # from 21 to midnight, 50%
    

    让我们计算日期时间之间的加权持续时间:

    # 1 hour at 50%, 1 at 100%: that should be 3600 + 1800 = 5400 s
    print(wd.duration(datetime(2017, 1, 3, 6, 0), datetime(2017, 1, 3, 8)))
    # 5400
    
    # a few tests
    intervals = [
        (datetime(2017, 1, 3, 9, 0), datetime(2017, 1, 3, 10)),  # 1 hour with weight 1
        (datetime(2017, 1, 3, 23, 0), datetime(2017, 1, 4, 1)),  # 2 hours, weight 0.5
        (datetime(2017, 1, 3, 5, 0), datetime(2017, 1, 4, 5)),   # 1 full day
        (datetime(2017, 1, 3, 5, 0), datetime(2017, 1, 3, 23)),  # same day
        (datetime(2017, 1, 3, 5, 0), datetime(2017, 1, 4, 23)),  # next day
        (datetime(2017, 1, 3, 5, 0), datetime(2017, 1, 5, 23)),  # 1 full day in between
                ]
    for interval in intervals:
        print(interval)
        print(wd.duration(*interval))  
    
    # (datetime.datetime(2017, 1, 3, 9, 0), datetime.datetime(2017, 1, 3, 10, 0))
    # 3600
    # (datetime.datetime(2017, 1, 3, 23, 0), datetime.datetime(2017, 1, 4, 1, 0))
    # 3600
    # (datetime.datetime(2017, 1, 3, 5, 0), datetime.datetime(2017, 1, 4, 5, 0))
    # 67500
    # (datetime.datetime(2017, 1, 3, 5, 0), datetime.datetime(2017, 1, 3, 23, 0))
    # 56700
    # (datetime.datetime(2017, 1, 3, 5, 0), datetime.datetime(2017, 1, 4, 23, 0))
    # 124200
    # (datetime.datetime(2017, 1, 3, 5, 0), datetime.datetime(2017, 1, 5, 23, 0))
    # 191700
    

    【讨论】:

      【解决方案3】:

      这里有一个解决方案。

      它做了两件事,首先它计算两个日期之间的完整天数,并且由于我们知道(好吧,我们可以近似)每天是 24 小时,因此加权“白天时间”和“夜间”(计算以小时为单位)。所以现在我们只需要计算出剩余的不到 24 小时的时间间隔。这里的诀窍是“折叠”时间,这样“黎明”就不是在一天的中间,而是在 0,所以我们只有一个“黄昏”的分隔符,所以我们只有三种情况,都是白天,两者都是夜间或较晚的日期是夜间,较早的日期是白天。

      根据 cmets 更新。

      在我的笔记本电脑上,100 万次函数调用的运行时间是 4.588s

      from datetime import datetime,timedelta
      
      def weighteddiff(d2,d1,dawn,dusk,night_weight):
      
          #if dusk is "before" dawn, switch roles
          day_weight = 1
          if dusk < dawn:
              day_weight = night_weight
              night_weight = 1
              placeholder = dawn
              dawn = dusk
              dusk = placeholder
      
          nighttime = dawn.total_seconds()/3600 + 24 - dusk.total_seconds()/3600
          daytime = 24-nighttime
      
      
          dt = d2-d1
      
          total_hours = 0
          total_hours += dt.days*daytime*day_weight + dt.days*nighttime*night_weight
      
          d1 += timedelta(days=dt.days)
          d1 -= dawn
          d2 -= dawn
      
          dawntime = datetime(d2.year,d2.month,d2.day,0)
          dusktime = dawntime + dusk - dawn
      
          if d1 < dusktime and d2 < dusktime:
              total_hours += (d2-d1).total_seconds()/3600*day_weight
          elif d1 < dusktime and d2 >= dusktime:
              total_hours += (dusktime - d1).total_seconds()/3600*day_weight
              total_hours += (d2 - dusktime).total_seconds()/3600*night_weight
          elif d1 >= dusktime and d2 >= dusktime:
              total_hours += (d2-d1).total_seconds()/3600*night_weight
          else:
              pass
      
          return total_hours
      
      
      weight = 0.5 #weight of nightime hours
      
      #dawn and dusk supplied as timedelta from midnight
      dawn = timedelta(hours=5,minutes=0,seconds=0)
      dusk = timedelta(hours=19,minutes=4,seconds=0)
      
      d1 = datetime(2017,10,23, 14)
      d2 = datetime(2017,10,23, 22)
      print("test1",weighteddiff(d2,d1,dawn,dusk,weight))
      
      d1 = datetime(2016,10,22, 20)
      d2 = datetime(2016,10,23, 20) 
      print("test2",weighteddiff(d2,d1,dawn,dusk,weight))
      
      dawn = timedelta(hours=6,minutes=0,seconds=0)
      dusk = timedelta(hours=1,minutes=4,seconds=0)
      
      d1 = datetime(2017,10,22, 2)
      d2 = datetime(2017,10,23, 19)
      print("test3",weighteddiff(d2,d1,dawn,dusk,weight))
      
      d1 = datetime(2016,10,22, 20)
      d2 = datetime(2016,10,23, 20) 
      print("test4",weighteddiff(d2,d1,dawn,dusk,weight))
      

      【讨论】:

      • 我认为你的代码有问题,这不应该给出0:d1 = datetime(2017,10,23, 14) d2 = datetime(2017,10,23, 19) print(加权差异(d2,d1))
      • 你是对的,我错过了时间正好等于黄昏时间的情况,修复它
      • 我认为我的解决方案是目前最好的,原因有两个:1)它不使用迭代,而是直接计算它,这意味着它总是需要恒定的时间才能完成,而其他的会变慢随着日期越来越远,appart 2) 由于不使用迭代,它与 datetime 表示一样精确,并且不会受到采样问题的影响
      • 您的解决方案是否处理黄昏和黎明同时发生的情况(例如黄昏=1、黎明=6)?
      • 我觉得还是有一些问题:d1 = datetime(2016,10,22, 20) d2 = datetime(2016,10,23, 20) weighteddiff(d2,d1)
      【解决方案4】:

      此解决方案计算完整日期的加权数,然后从第一个日期和最后一个日期减去或添加任何残差。这不考虑任何夏令时效应。

      import pandas as pd
      
      
      def timediff(t1, t2):
      
          DAY_SECS = 24 * 60 * 60
          DUSK = pd.Timedelta("21h")
          # Dawn is chosen as 7 a.m.
          FRAC_NIGHT = 10 / 24
          FRAC_DAY = 14 / 24
          DAY_WEIGHT = 1
          NIGHT_WEIGHT = 0.5
      
          full_days = ((t2.date() - t1.date()).days * DAY_SECS *
                       (FRAC_NIGHT * NIGHT_WEIGHT + FRAC_DAY * DAY_WEIGHT))
      
          def time2dusk(t):
              time = (pd.Timestamp(t.date()) + DUSK) - t
              time = time.total_seconds()
              wtime = (min(time * NIGHT_WEIGHT, 0) +
                       min(max(time, 0), FRAC_DAY * DAY_SECS) * DAY_WEIGHT +
                       max(time - DAY_SECS * FRAC_DAY, 0) * NIGHT_WEIGHT)
              return wtime
      
          t1time2dusk = time2dusk(t1)
          t2time2dusk = time2dusk(t2)
          return full_days + t1time2dusk - t2time2dusk
      

      这提供了加权秒数的解决方案,但之后您可以转换为方便的任何内容

      times = [(pd.Timestamp("20170101T12:00:00"), pd.Timestamp("20170101T15:00:00")),
               (pd.Timestamp("20170101T12:00:00"), pd.Timestamp("20170101T23:00:00")),
               (pd.Timestamp("20170101T12:00:00"), pd.Timestamp("20170102T12:00:00")),
               (pd.Timestamp("20170101T22:00:00"), pd.Timestamp("20170101T23:00:00")),
               (pd.Timestamp("20170101T22:00:00"), pd.Timestamp("20170102T05:00:00")),
               (pd.Timestamp("20170101T06:00:00"), pd.Timestamp("20170101T08:00:00"))]
      
      exp_diff_hours = [3, 9 + 2*0.5, 9 + 10*0.5 + 5, 1*0.5, 7*0.5, 1 + 1*0.5]
      
      for i, ts in enumerate(times):
          t1, t2 = ts
          print("\n")
          print("Time1: %s" % t1)
          print("Time2: %s" % t2)
          print("Weighted Time2 - Time1: %s" % (timediff(t1, t2) / 3600))
          print("Weighted Time2 - Time1 Expected: %s" % exp_diff_hours[i])
      
      for i, ts in enumerate(times):
          t2, t1 = ts
          print("\n")
          print("Time1: %s" % t1)
          print("Time2: %s" % t2)
          print("Weighted Time2 - Time1: %s" % (timediff(t1, t2) / 3600))
          print("Weighted Time2 - Time1 Expected: %s" % -exp_diff_hours[i])
      
      Time1: 2017-01-01 12:00:00
      Time2: 2017-01-01 15:00:00
      Weighted Time2 - Time1: 3.000000000000001
      Weighted Time2 - Time1 Expected: 3
      
      
      Time1: 2017-01-01 12:00:00
      Time2: 2017-01-01 23:00:00
      Weighted Time2 - Time1: 10.0
      Weighted Time2 - Time1 Expected: 10.0
      
      
      Time1: 2017-01-01 12:00:00
      Time2: 2017-01-02 12:00:00
      Weighted Time2 - Time1: 19.0
      Weighted Time2 - Time1 Expected: 19.0
      
      
      Time1: 2017-01-01 22:00:00
      Time2: 2017-01-01 23:00:00
      Weighted Time2 - Time1: 0.5
      Weighted Time2 - Time1 Expected: 0.5
      
      
      Time1: 2017-01-01 22:00:00
      Time2: 2017-01-02 05:00:00
      Weighted Time2 - Time1: 3.5
      Weighted Time2 - Time1 Expected: 3.5
      
      
      Time1: 2017-01-01 06:00:00
      Time2: 2017-01-01 08:00:00
      Weighted Time2 - Time1: 1.5
      Weighted Time2 - Time1 Expected: 1.5
      
      
      Time1: 2017-01-01 15:00:00
      Time2: 2017-01-01 12:00:00
      Weighted Time2 - Time1: -3.000000000000001
      Weighted Time2 - Time1 Expected: -3
      
      
      Time1: 2017-01-01 23:00:00
      Time2: 2017-01-01 12:00:00
      Weighted Time2 - Time1: -10.0
      Weighted Time2 - Time1 Expected: -10.0
      
      
      Time1: 2017-01-02 12:00:00
      Time2: 2017-01-01 12:00:00
      Weighted Time2 - Time1: -19.0
      Weighted Time2 - Time1 Expected: -19.0
      
      
      Time1: 2017-01-01 23:00:00
      Time2: 2017-01-01 22:00:00
      Weighted Time2 - Time1: -0.5
      Weighted Time2 - Time1 Expected: -0.5
      
      
      Time1: 2017-01-02 05:00:00
      Time2: 2017-01-01 22:00:00
      Weighted Time2 - Time1: -3.5
      Weighted Time2 - Time1 Expected: -3.5
      
      
      Time1: 2017-01-01 08:00:00
      Time2: 2017-01-01 06:00:00
      Weighted Time2 - Time1: -1.5
      Weighted Time2 - Time1 Expected: -1.5
      

      【讨论】:

        【解决方案5】:

        试试这个代码:

        from pandas import date_range
        from pandas import Series
        from datetime import datetime
        from datetime import time
        from dateutil.relativedelta import relativedelta
        
        # initial date
        d1 = datetime(2017, 1, 1, 8, 0, 0)
        d2 = d1 + relativedelta(days=10)
        print d1, d1
        

        方法一:速度慢但容易理解。

        ts = Series(1, date_range(d1, d2, freq='S'))
        c1 = ts.index.time >= time(21, 0, 0)
        c2 = ts.index.time < time(7, 0, 0)
        ts[c1 | c2] = .5
        ts.iloc[-1] = 0
        print ts.sum()   # result in seconds
        

        方法2:更快,但有点复杂

        def get_seconds(ti):
            ts = Series(1, ti)
            c1 = ts.index.time >= time(21, 0, 0)
            c2 = ts.index.time < time(7, 0, 0)
            ts[c1 | c2] = .5
            ts.iloc[-1] = 0
            return ts.sum() * ti.freq.delta.seconds
        
        ti0 = date_range(d1, d2, freq='H', normalize=True)
        ti1 = date_range(ti0[0], d1, freq='S')
        ti2 = date_range(ti0[-1], d2, freq='S')
        print get_seconds(ti0) - get_seconds(ti1) + get_seconds(ti2) # result in seconds
        

        【讨论】:

          【解决方案6】:

          以下是两种方法。我认为第二个在较大的日期范围(例如相隔 5 年)上会更快,但事实证明第一个是:

          1. 循环遍历您的日期时间之间的所有分钟
          2. 创建一个日期范围系列,然后是一系列权重(使用 np.where() 条件逻辑)并将它们相加

          方法 1:循环遍历分钟并更新加权时间增量。
          4.2 seconds(笔记本电脑运行时间在 5 年 dt 范围内)

          import datetime    
          def weighted_timedelta(start_dt, end_dt,
                                 nights_start = datetime.time(21,0),
                                 nights_end   = datetime.time(7,0),
                                 night_weight = 0.5):
          
              # initialize counters
              weighted_timedelta = 0
              i = start_dt
          
              # loop through minutes in datetime-range, updating weighted_timedelta
              while i <= end_dt:
                  i += timedelta(minutes=1)
          
                  if i.time() >= nights_start or i.time() <= nights_end:
                      weighted_timedelta += night_weight
                  else:
                      weighted_timedelta += 1
          
              return weighted_timedelta
          

          方法 2:使用 date_range 和 np.where() 创建 Pandas 一系列权重。
          15 seconds(笔记本电脑运行时间为 5 年 dt 范围)

          def weighted_timedelta(start_dt, end_dt,
                                 nights_start = datetime.time(21,0),
                                 nights_end   = datetime.time(7,0),
                                 night_weight = 0.5):
          
              # convert dts to pandas date-range series, minute-resolution
              dt_range = pd.date_range(start=start_dt, end=end_dt, freq='min')
          
              # Assign 'weight' as -night_weight- or 1, for each minute, depeding on day/night
              dt_weights = np.where((dt_range2.time >= nights_start) |  # | is bitwise 'or' for arrays of booleans
                                    (dt_range2.time <= nights_end), 
                                    night_weight, 1)
          
              # return value as weighted minutes
              return dt_weights.sum()
          

          每个都经过了准确性测试:

          d1 = datetime.datetime(2016,1,22,20,30)
          d2 = datetime.datetime(2016,1,22,21,30)
          
          weighted_timedelta(d1, d2)
          45.0
          

          【讨论】:

          • 如果日期很远,这不会运行很长时间吗?
          猜你喜欢
          • 1970-01-01
          • 2012-02-04
          • 1970-01-01
          • 2017-01-20
          • 2012-01-11
          • 2023-03-26
          • 2022-11-15
          • 1970-01-01
          • 2013-02-20
          相关资源
          最近更新 更多