Datetime python difference set to night time

I have two datetime objects in python d1 and d2. I want to take the time difference between them. I want something more complex than (d1 - d2): I want the time during the night to be considered less than the time during the day, using a constant fraction of c, for example. one hour at night is considered only half an hour in the daytime.

Is there an easy way to do this in python (pandas and / or numpy)?

Thanks!

Edit: night time speaks from 9pm to 7am. But ideally, I'm looking for a solution in which you can choose arbitrary weights for arbitrary periods throughout the day.

+8
python numpy pandas datetime
source share
6 answers

A solution that allows you to determine as many periods as you want, with their respective weights.

Firstly, an auxiliary function that cuts off the interval between our dates:

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 that calculates weighted durations for a given list of periods and weights:

 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 

We create an instance of WeightedDuration that defines our periods and their weights. We can have as many periods as we want, with weights less than or greater than 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% 

Let me calculate the weighted duration between dates:

 # 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 
+1
source share

Here is the solution.

This does two things: first it calculates the number of full days between two dates, and since we know (well, we can get closer) that every day is 24 hours, it's pretty trivial for the weight of โ€œdaytimeโ€ and โ€œdayโ€, nighttime โ€œ (calculations are done in hours.) So now we need to find out the remaining less than 24-hour interval. The trick here is to โ€œresetโ€ the time so that the โ€œdawnโ€ is not in the middle of the day, but at 0, so we only have one separator for twilight, so we have only three cases: both are days an apparent both night-time, or such later date - at night, but before that - the daytime.

Updated based on comments.

The 4.588s time for 1 million function calls was 4.588s on my laptop.

 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)) 
+3
source share

The following are two approaches. I assumed that the second will be faster in large date ranges (for example, at a distance of 5 years), but it turns out that the first of them:

  • goes through all the minutes between your dates
  • creates a series of date ranges, then a series of weights (using the np.where () conditional logic) and summarizes them

Approach 1: cycle in minutes and update weighted timedelta.
4.2 seconds (Notebook operating time in the 5-year range)

 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 

Approach 2: Create a Pandas series of weights using date_range and np.where() .
15 seconds (Battery life in the range of 5 years)

 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() 

Each of them has also been tested for accuracy using:

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

This solution calculates the weighted number of full dates, and then subtracts or adds the remainders of the first and last dates. This does not take into account the effects of the summer effect.

 import pandas as pd def timediff(t1, t2): DAY_SECS = 24 * 60 * 60 DUSK = pd.Timedelta("21h") # Dawn is chosen as 7 am 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 

This provides a solution in measured seconds, but you can convert it to convenient after

 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 
+2
source share

try this code:

 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 

method 1: slow but clear.

 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 

method 2: faster but a little harder

 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 
+1
source share

High level concept

  • Mark the start and end times.
  • Find all instances of 7am and 9pm in between
  • Create a sorted array of temporary stamps that include start, end, all 7 ams, all 9 pms
  • Compute diff on this array
  • Determine if the starting point is day or night.
  • Explain the difference by dividing the corresponding half by 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 
0
source share

All Articles