Calcolo efficiente della sovrapposizione dell'intervallo di date in Python?


85

Ho due intervalli di date in cui ogni intervallo è determinato da una data di inizio e di fine (ovviamente, istanze di datetime.date ()). I due intervalli possono sovrapporsi o meno. Mi serve il numero di giorni della sovrapposizione. Ovviamente posso pre-compilare due set con tutte le date all'interno di entrambi gli intervalli ed eseguire un'intersezione di set, ma questo è forse inefficiente ... esiste un modo migliore a parte un'altra soluzione che utilizza una lunga sezione if-elif che copre tutti i casi?

Risposte:


175
  • Determina l'ultima delle due date di inizio e la prima delle due date di fine.
  • Calcola il timedelta sottraendoli.
  • Se il delta è positivo, questo è il numero di giorni di sovrapposizione.

Ecco un esempio di calcolo:

>>> 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
+1 soluzione molto bella. Tuttavia, questo non funziona per le date che sono completamente contenute nell'altro. Per semplicità in numeri interi: Intervallo (1,4) e Intervallo (2,3) restituisce 1
oscurità

3
@darkless In realtà, restituisce 2 che è corretto . Prova questi input r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3)). Penso che ti sei perso il +1calcolo della sovrapposizione (necessario perché l'intervallo è chiuso su entrambe le estremità).
Raymond Hettinger

Oh, hai assolutamente ragione, sembra che me lo sia perso. Grazie :)
oscurità

1
E se volessi calcolare 2 volte invece di 2 date? @RaymondHettinger
Eric

1
Se usi oggetti datetime con orari potresti invece scrivere .days .total_seconds ().
ErikXIII

10

Le chiamate di funzione sono più costose delle operazioni aritmetiche.

Il modo più veloce per farlo prevede 2 sottrazioni e 1 min ():

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

rispetto al successivo migliore che richiede 1 sottrazione, 1 min () e un max ():

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

Ovviamente con entrambe le espressioni è ancora necessario verificare una sovrapposizione positiva.


1
Questo metodo non restituirà sempre la risposta corretta. es. 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 + 1stamperà 4 dove si suppone di stampare 1
tkyass

Ottengo un errore di serie ambiguo utilizzando la prima equazione. Ho bisogno di una libreria particolare?
Arthur D. Howland,

6

Ho implementato una classe TimeRange come puoi vedere di seguito.

Get_overlapped_range prima nega tutte le opzioni non sovrapposte con una condizione semplice, quindi calcola l'intervallo sovrapposto considerando tutte le opzioni possibili.

Per ottenere il numero di giorni è necessario prendere il valore TimeRange restituito da get_overlapped_range e dividere la durata per 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]])

@ L.Guthardt Concordato, ma questa soluzione è organizzata e arriva con più funzionalità
Elad Sofer

1
Ok ... è bello più funzionalità, ma in realtà su StackOverflow una risposta dovrebbe adattarsi alle esigenze specificate di OP. Quindi né più né meno. :)
L. Guthardt

5

Puoi utilizzare il pacchetto 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" all'interno di DateTimeRange () può anche essere formato data / ora, come Timestamp ('2017-08-30 20:36:25').


1
Grazie, DateTimeRangeho appena dato un'occhiata alla documentazione per il pacchetto e sembra che supportino il is_intersectionquale restituisce nativamente un valore booleano (Vero o Falso) a seconda che ci sia o meno un'intersezione tra due intervalli di date. Quindi, per il tuo esempio: time_range1.is_intersection(time_range2)tornerebbero Truese si intersecano altrimentiFalse
Deep

3

Pseudocodice:

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

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

0

Ok la mia soluzione è un po 'traballante perché il mio df usa tutte le serie - ma diciamo che hai le seguenti colonne, 2 delle quali sono fisse che è il tuo "anno fiscale". PoP è "Periodo di prestazione", ovvero i tuoi dati variabili:

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

Supponiamo che tutti i dati siano in formato datetime, ad esempio -

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

Prova le seguenti equazioni per trovare il numero di giorni che si sovrappongono:

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.