¿Cálculo eficiente de superposición de rango de fechas en Python?

85

Tengo dos rangos de fechas donde cada rango está determinado por una fecha de inicio y finalización (obviamente, instancias de datetime.date ()). Los dos rangos pueden superponerse o no. Necesito el número de días de superposición. Por supuesto, puedo precompletar dos conjuntos con todas las fechas dentro de ambos rangos y realizar una intersección de conjuntos, pero esto es posiblemente ineficiente ... ¿hay una mejor manera aparte de otra solución que use una sección if-elif larga que cubra todos los casos?

Andreas Jung
fuente

Respuestas:

174
  • Determine la última de las dos fechas de inicio y la más temprana de las dos fechas de finalización.
  • Calcule el delta de tiempo restándolos.
  • Si el delta es positivo, ese es el número de días de superposición.

Aquí hay un ejemplo de cálculo:

>>> 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
fuente
1
+1 muy buena solución. Sin embargo, esto no funciona del todo en fechas que están completamente contenidas en la otra. Para simplificar los números enteros: Range (1,4) y Range (2,3) devuelve 1
oscuridad
3
@darkless En realidad, devuelve 2, lo cual es correcto . Pruebe estas entradas r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3)). Creo que se perdió el +1cálculo de superposición (necesario porque el intervalo está cerrado en ambos extremos).
Raymond Hettinger
Oh, tienes toda la razón, parece que me lo he perdido. Gracias :)
darkless
1
¿Qué pasa si desea calcular 2 veces en lugar de 2 fechas? @RaymondHettinger
Eric
Si usa objetos de fecha y hora con horas, podría en lugar de .days escribir .total_seconds ().
ErikXIII
10

Las llamadas a funciones son más caras que las operaciones aritméticas.

La forma más rápida de hacer esto implica 2 restas y 1 minuto ():

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

en comparación con el siguiente mejor que necesita 1 resta, 1 min () y un máximo ():

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

Por supuesto, con ambas expresiones, aún necesita verificar una superposición positiva.

John Machin
fuente
1
Este método no siempre devolverá la respuesta correcta. por ejemplo 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 + 1, imprimirá 4 donde se suponía que imprimía 1
tkyass
Obtengo un error de serie ambiguo usando la primera ecuación. ¿Necesito una biblioteca en particular?
Arthur D. Howland
6

Implementé una clase TimeRange como puede ver a continuación.

Get_overlapped_range primero niega todas las opciones no superpuestas mediante una condición simple, y luego calcula el rango superpuesto considerando todas las opciones posibles.

Para obtener la cantidad de días, deberá tomar el valor de TimeRange que se devolvió de get_overlapped_range y dividir la duración entre 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
fuente
@ L.Guthardt De acuerdo, pero esta solución está organizada y viene con más funcionalidad
Elad Sofer
1
Ok ... eso es bueno, la mayor funcionalidad, pero en realidad en StackOverflow una respuesta debería ajustarse a las necesidades especificadas de OP. Así que ni más ni menos. :)
L.Guthardt
5

Puede utilizar el paquete 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" dentro de DateTimeRange () también puede ser formato de fecha y hora, como Timestamp ('2017-08-30 20:36:25').

Songhua Hu
fuente
1
Gracias, acabo de echar un vistazo a la documentación del DateTimeRangepaquete y parece que admiten is_intersectionque devuelve de forma nativa un valor booleano (Verdadero o Falso) dependiendo de si hay o no una intersección entre dos rangos de fechas. Entonces, para su ejemplo: time_range1.is_intersection(time_range2)regresaría Truesi se cruzan de otra maneraFalse
Deep
3

Pseudocódigo:

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

Ok, mi solución es un poco inestable porque mi df usa todas las series, pero digamos que tiene las siguientes columnas, 2 de las cuales son fijas, que es su "Año fiscal". PoP es "Período de rendimiento", que son sus datos variables:

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

Suponga que todos los datos están en formato de fecha y hora, es decir,

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

Pruebe las siguientes ecuaciones para encontrar el número de días que se superponen:

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
fuente