Efektywne obliczanie nakładania się zakresu dat w Pythonie?

85

Mam dwa zakresy dat, w których każdy zakres jest określany na podstawie daty rozpoczęcia i zakończenia (oczywiście instancje datetime.date ()). Te dwa zakresy mogą się pokrywać lub nie. Potrzebuję liczby dni nakładania się. Oczywiście mogę wstępnie wypełnić dwa zestawy wszystkimi datami w obu zakresach i wykonać zestaw przecięcia, ale jest to prawdopodobnie nieefektywne ... czy jest lepszy sposób niż inne rozwiązanie, używając długiej sekcji if-elif obejmującej wszystkie przypadki?

Andreas Jung
źródło

Odpowiedzi:

174
  • Określ najpóźniejszą z dwóch dat rozpoczęcia i najwcześniejszą z dwóch dat końcowych.
  • Obliczyć timedelta, odejmując je.
  • Jeśli delta jest dodatnia, jest to liczba dni pokrywania się.

Oto przykład obliczenia:

>>> 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
Raymond Hettinger
źródło
1
+1 bardzo fajne rozwiązanie. Chociaż to nie działa w przypadku dat, które są w pełni zawarte w drugim. Dla uproszczenia w liczbach całkowitych: Range (1,4) i Range (2,3) zwracają 1
ciemności
3
@darkless Właściwie zwraca 2, co jest poprawne . Wypróbuj te dane wejściowe r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3)). Myślę, że przegapiłeś +1w obliczeniu nakładania się (konieczne, ponieważ interwał jest zamknięty na obu końcach).
Raymond Hettinger
Och, masz absolutną rację, wydaje się, że to przegapiłem. Dziękuję :)
mroku
1
A co jeśli chcesz obliczyć 2 razy zamiast 2 dat? @RaymondHettinger
Eric
Jeśli używasz obiektów datetime z czasami, możesz zamiast .days napisać .total_seconds ().
ErikXIII
10

Wywołania funkcji są droższe niż operacje arytmetyczne.

Najszybszy sposób to 2 odejmowania i 1 min ():

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

w porównaniu z następną najlepszą, która wymaga 1 odejmowania, 1 min () i max ():

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

Oczywiście w przypadku obu wyrażeń nadal musisz sprawdzić, czy nie ma pozytywnego nakładania się.

John Machin
źródło
1
Ta metoda nie zawsze zwraca poprawną odpowiedź. np. 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 + 1wypisze 4 tam, gdzie przypuszczano, że ma wydrukować 1
tkyass
Otrzymuję niejednoznaczny błąd szeregu przy użyciu pierwszego równania. Czy potrzebuję konkretnej biblioteki?
Arthur D. Howland
6

Zaimplementowałem klasę TimeRange, jak widać poniżej.

Get_overlapped_range najpierw neguje wszystkie nienakładające się opcje za pomocą prostego warunku, a następnie oblicza pokrywający się zakres, biorąc pod uwagę wszystkie możliwe opcje.

Aby uzyskać liczbę dni, musisz wziąć wartość TimeRange, która została zwrócona z get_overlapped_range i podzielić czas trwania przez 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]])
Elad Sofer
źródło
@ L.Guthardt Zgoda, ale to rozwiązanie jest zorganizowane i ma większą funkcjonalność
Elad Sofer
1
Ok ... to fajne, im więcej funkcji, ale tak naprawdę w StackOverflow odpowiedź powinna po prostu pasować do określonych potrzeb OP. Więc nie mniej i nie więcej. :)
L. Guthardt
5

Możesz skorzystać z pakietu 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()

„2015-01-01T00: 00: 00 + 0900” wewnątrz DateTimeRange () może mieć również format daty i godziny, na przykład Timestamp ('2017-08-30 20:36:25').

Songhua Hu
źródło
1
Dzięki, właśnie przejrzałem dokumentację DateTimeRangepakietu i wygląda na to, że obsługują, is_intersectionktóra natywnie zwraca wartość logiczną (True lub False) w zależności od tego, czy występuje przecięcie między dwoma zakresami dat. Na przykład: time_range1.is_intersection(time_range2)powróci, Truejeśli się przecinająFalse
Głębokie
3

Pseudo kod:

 1 + max( -1, min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart) )
ypercubeᵀᴹ
źródło
0
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
andros1337
źródło
0

Ok, moje rozwiązanie jest trochę dziwne, ponieważ mój plik df wykorzystuje wszystkie serie - ale powiedzmy, że masz następujące kolumny, z których 2 są ustalone, czyli jest to Twój „Rok obrotowy”. PoP to „Okres wykonania”, czyli dane zmienne:

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

Załóżmy, że wszystkie dane są w formacie daty i godziny, tj. -

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

Wypróbuj poniższe równania, aby znaleźć liczbę dni pokrywających się:

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)
Arthur D. Howland
źródło