Cálculo de sobreposição de intervalo de datas eficiente em python?

85

Eu tenho dois intervalos de datas onde cada intervalo é determinado por uma data de início e de término (obviamente, instâncias de datetime.date ()). Os dois intervalos podem se sobrepor ou não. Preciso do número de dias da sobreposição. Claro que posso pré-preencher dois conjuntos com todas as datas dentro de ambos os intervalos e realizar uma interseção de conjuntos, mas isso é possivelmente ineficiente ... há uma maneira melhor de se separar de outra solução usando uma seção if-elif longa cobrindo todos os casos?

Andreas Jung
fonte

Respostas:

175
  • Determine a mais recente das duas datas de início e a mais antiga das duas datas de término.
  • Calcule o timedelta subtraindo-os.
  • Se o delta for positivo, esse é o número de dias de sobreposição.

Aqui está um exemplo 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
fonte
1
1 solução muito agradável. Porém, isso não funciona muito bem em datas que estão totalmente contidas no outro. Para simplificar os números inteiros: Intervalo (1,4) e Intervalo (2,3) retorna 1
escuridão
3
@darkless Na verdade, ele retorna 2, o que é correto . Experimente essas 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)). Acho que você perdeu o +1no cálculo de sobreposição (necessário porque o intervalo é fechado em ambas as extremidades).
Raymond Hettinger
Oh, você está absolutamente certo, parece que perdi isso. Obrigado :)
darkless de
1
E se você quiser calcular 2 vezes em vez de 2 datas? @RaymondHettinger
Eric
1
Se você usar objetos datetime com horários, você pode, em vez de .days, escrever .total_seconds ().
ErikXIII
10

As chamadas de função são mais caras do que as operações aritméticas.

A maneira mais rápida de fazer isso envolve 2 subtrações e 1 min ():

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

em comparação com o próximo melhor que precisa de 1 subtração, 1 min () e um máximo ():

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

É claro que com ambas as expressões você ainda precisa verificar se há uma sobreposição positiva.

John Machin
fonte
1
Este método não retornará sempre uma resposta correta. por exemplo 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 onde deveria imprimir 1
tkyass
Recebo um erro de série ambíguo usando a primeira equação. Eu preciso de uma biblioteca específica?
Arthur D. Howland
6

Implementei uma classe TimeRange como você pode ver abaixo.

O get_overlapped_range primeiro nega todas as opções não sobrepostas por uma condição simples e, em seguida, calcula o intervalo sobreposto considerando todas as opções possíveis.

Para obter a quantidade de dias, você precisará pegar o valor TimeRange que foi retornado de get_overlapped_range e dividir a duração por 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
fonte
@ L.Guthardt Concordo, mas esta solução é organizada e vem com mais funcionalidade
Elad Sofer
1
Ok ... isso é bom quanto mais funcionalidade, mas na verdade no StackOverflow uma resposta deve atender às necessidades especificadas do OP. Portanto, nem mais nem menos. :)
L. Guthardt
5

Você pode usar o pacote 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 () também pode ser o formato de data e hora, como Timestamp ('2017-08-30 20:36:25').

Songhua Hu
fonte
1
Obrigado, Acabei de dar uma olhada na documentação do DateTimeRangepacote e parece que eles suportam is_intersectionque retorne nativamente um valor booleano (Verdadeiro ou Falso) dependendo se há ou não uma interseção entre dois intervalos de datas. Então, para seu exemplo: time_range1.is_intersection(time_range2)retornaria Truese eles se cruzassem de outra formaFalse
Profundo
3

Pseudo-código:

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

Ok, minha solução é um pouco instável porque meu df usa todas as séries - mas digamos que você tenha as seguintes colunas, 2 das quais são fixas, que é o seu "Ano Fiscal". PoP é o "período de desempenho", que são seus dados variáveis:

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

Suponha que todos os dados estejam no formato de data e hora, ou seja -

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

Tente as seguintes equações para encontrar o número de dias sobrepostos:

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
fonte