Effiziente Berechnung der Datumsbereichsüberlappung in Python?

85

Ich habe zwei Datumsbereiche, in denen jeder Bereich durch ein Start- und ein Enddatum bestimmt wird (offensichtlich datetime.date () -Instanzen). Die beiden Bereiche können sich überlappen oder nicht. Ich brauche die Anzahl der Tage der Überlappung. Natürlich kann ich zwei Sätze mit allen Daten in beiden Bereichen vorab füllen und einen Satzschnitt durchführen, aber dies ist möglicherweise ineffizient. Gibt es einen besseren Weg als eine andere Lösung, bei der ein langer if-elif-Abschnitt verwendet wird, der alle Fälle abdeckt?

Andreas Jung
quelle

Antworten:

173
  • Bestimmen Sie das späteste der beiden Startdaten und das früheste der beiden Enddaten.
  • Berechnen Sie das Zeitdelta durch Subtrahieren.
  • Wenn das Delta positiv ist, ist dies die Anzahl der Überlappungstage.

Hier ist ein Beispiel für eine Berechnung:

>>> 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
quelle
1
+1 sehr schöne Lösung. Dies funktioniert jedoch nicht ganz bei Daten, die vollständig in den anderen enthalten sind. Der Einfachheit halber in ganzen Zahlen: Range (1,4) und Range (2,3) geben 1
darkless
3
@darkless Eigentlich gibt es 2 zurück, was korrekt ist . Probieren Sie diese Eingaben aus r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3)). Ich denke, Sie haben +1die Überlappungsberechnung verpasst (notwendig, weil das Intervall an beiden Enden geschlossen ist).
Raymond Hettinger
Oh, Sie haben absolut Recht, es scheint, dass ich das verpasst habe. Vielen Dank :)
dunkellos
1
Was ist, wenn Sie 2 statt 2 Daten berechnen möchten? @ RaymondHettinger
Eric
10

Funktionsaufrufe sind teurer als arithmetische Operationen.

Der schnellste Weg dazu besteht aus 2 Subtraktionen und 1 Minute ():

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

verglichen mit dem nächstbesten, der 1 Subtraktion, 1 min () und ein max () benötigt:

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

Natürlich müssen Sie bei beiden Ausdrücken immer noch nach einer positiven Überlappung suchen.

John Machin
quelle
1
Diese Methode gibt nicht immer die richtige Antwort zurück. zB 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 + 1wird 4 gedruckt, wo es angenommen wird, um 1
tkyass
Ich erhalte einen mehrdeutigen Serienfehler unter Verwendung der ersten Gleichung. Benötige ich eine bestimmte Bibliothek?
Arthur D. Howland
6

Ich habe eine TimeRange-Klasse implementiert, wie Sie unten sehen können.

Der get_overlapped_range negiert zuerst alle nicht überlappenden Optionen durch eine einfache Bedingung und berechnet dann den überlappenden Bereich unter Berücksichtigung aller möglichen Optionen.

Um die Anzahl der Tage zu ermitteln, müssen Sie den von get_overlapped_range zurückgegebenen TimeRange-Wert verwenden und die Dauer durch 60 * 60 * 24 teilen.

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
quelle
@ L.Guthardt Einverstanden, aber diese Lösung ist organisiert und kommt mit mehr Funktionalität
Elad Sofer
1
Ok ... das ist schön, je mehr Funktionalität, aber tatsächlich sollte eine Antwort auf StackOverflow nur den spezifizierten Anforderungen von OP entsprechen. Also nicht mehr und nicht weniger. :)
L. Guthardt
4

Sie können das datetimerange-Paket verwenden: 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" in DateTimeRange () kann auch ein Datum / Uhrzeit-Format haben, z. B. Timestamp ('2017-08-30 20:36:25').

Songhua Hu
quelle
Vielen Dank. Ich habe mir gerade die Dokumentation für das DateTimeRangePaket angesehen und es scheint, dass sie unterstützen, is_intersectionwas nativ einen booleschen Wert (True oder False) zurückgibt, je nachdem, ob es einen Schnittpunkt zwischen zwei Datumsbereichen gibt oder nicht. Also, für Ihr Beispiel: time_range1.is_intersection(time_range2)würde zurückkehren, Truewenn sie sich kreuzenFalse
Deep
3

Pseudocode:

 1 + max( -1, min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart) )
ypercubeᵀᴹ
quelle
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
quelle
0

Ok, meine Lösung ist ein bisschen wackelig, weil mein df alle Serien verwendet - aber nehmen wir an, Sie haben die folgenden Spalten, von denen 2 festgelegt sind, was Ihr "Geschäftsjahr" ist. PoP ist "Leistungszeitraum", bei dem es sich um Ihre variablen Daten handelt:

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

Angenommen, alle Daten haben das Datum / Uhrzeit-Format, dh -

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

Versuchen Sie die folgenden Gleichungen, um die Anzahl der Überlappungstage zu ermitteln:

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
quelle