Эффективный расчет перекрытия диапазонов дат в Python?

85

У меня есть два диапазона дат, каждый из которых определяется датой начала и окончания (очевидно, экземпляры datetime.date ()). Эти два диапазона могут перекрываться или нет. Мне нужно количество дней перекрытия. Конечно, я могу предварительно заполнить два набора всеми датами в обоих диапазонах и выполнить пересечение наборов, но это, возможно, неэффективно ... есть ли лучший способ отдельно от другого решения, используя длинный раздел if-elif, охватывающий все случаи?

Андреас Юнг
источник

Ответы:

175
  • Определите самую позднюю из двух дат начала и самую раннюю из двух дат окончания.
  • Вычислите timedelta, вычтя их.
  • Если дельта положительная, это количество дней перекрытия.

Вот пример расчета:

>>> 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 очень красивое решение. Хотя это не совсем подходит для дат, которые полностью содержатся в другом. Для простоты в целых числах: Range (1,4) и Range (2,3) возвращает 1
darkless
3
@darkless На самом деле он возвращает 2, что правильно . Попробуйте эти входы r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3)). Я думаю, вы пропустили +1расчет перекрытия (необходимо, потому что интервал закрыт с обоих концов).
Раймонд Хеттингер
О, вы совершенно правы, кажется, я это упустил. Спасибо :)
darkless
1
Что, если вы хотите посчитать 2 раза вместо 2 дат? @RaymondHettinger
Эрик
1
Если вы используете объекты datetime со временем, вы можете вместо .days написать .total_seconds ().
ErikXIII
10

Вызов функций дороже, чем арифметические операции.

Самый быстрый способ сделать это - 2 вычитания и 1 min ():

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

по сравнению со следующим лучшим, для которого требуется 1 вычитание, 1 min () и max ():

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

Конечно, с обоими выражениями вам все равно нужно проверить положительное перекрытие.

Джон Мачин
источник
1
Этот метод не всегда возвращает правильный ответ. например 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, напечатает 4, где предполагалось напечатать 1
tkyass
Я получаю неоднозначную ошибку ряда, используя первое уравнение. Нужна ли мне конкретная библиотека?
Артур Д. Хауленд
6

Я реализовал класс TimeRange, как вы можете видеть ниже.

Get_overlapped_range сначала отменяет все неперекрывающиеся параметры простым условием, а затем вычисляет перекрывающийся диапазон, рассматривая все возможные варианты.

Чтобы получить количество дней, вам нужно взять значение TimeRange, которое было возвращено из get_overlapped_range, и разделить продолжительность на 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 Согласен, но это решение организовано и обладает большей функциональностью
Элад Софер
1
Хорошо ... это приятно, чем больше функциональности, но на самом деле в StackOverflow ответ должен просто соответствовать указанным потребностям OP. Так что не больше и не меньше. :)
L. Guthardt
5

Вы можете использовать пакет 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» внутри DateTimeRange () также может быть форматом даты и времени, например Timestamp ('2017-08-30 20:36:25').

Сунхуа Ху
источник
1
Спасибо, только что просмотрел документацию по DateTimeRangeпакету, и кажется, что они поддерживают, is_intersectionкоторая изначально возвращает логическое значение (True или False) в зависимости от того, есть ли пересечение между двумя диапазонами дат. Итак, для вашего примера: time_range1.is_intersection(time_range2)вернется, Trueесли они пересекутся ещеFalse
Deep
3

Псевдокод:

 1 + max( -1, min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart) )
ypercubeᵀᴹ
источник
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
источник
0

Хорошо, мое решение немного шаткое, потому что мой df использует все серии, но допустим, у вас есть следующие столбцы, 2 из которых являются фиксированными, что является вашим «финансовым годом». PoP - это «период эффективности», который является вашими переменными данными:

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

Предположим, что все данные находятся в формате datetime, т.е.

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

Попробуйте следующие уравнения, чтобы найти количество дней, в которых пересекаются:

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)
Артур Д. Хауленд
источник