Ефективний розрахунок перекриття діапазону дат у python?


85

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

Відповіді:


174
  • Визначте останню з двох дат початку та найранішу з двох дат закінчення.
  • Обчисліть терміни, віднімаючи їх.
  • Якщо дельта позитивна, це кількість днів перекриття.

Ось приклад розрахунку:

>>> 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 дуже гарне рішення. Хоча, це не зовсім працює на дати, які повністю містяться в іншій. Для простоти в цілих числах: Діапазон (1,4) і Діапазон (2,3) повертає 1
безтемний

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підрахування перекриття (необхідно, оскільки інтервал закритий з обох кінців).
Реймонд Хеттінгер,

О, ви абсолютно праві, здається, я це пропустив. Дякую :)
безтемний

1
Що робити, якщо ви хочете обчислити 2 рази замість 2 дат? @RaymondHettinger
Ерік

Якщо ви використовуєте об'єкти datetime з часом, який ви можете замість .days написати .total_seconds ().
ErikXIII

10

Виклики функцій дорожчі арифметичних операцій.

Найшвидший спосіб зробити це передбачає 2 віднімання та 1 хв ():

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

порівняно з наступним найкращим, який потребує 1 віднімання, 1 хв () та макс. ():

(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

Я отримую неоднозначну помилку в серії, використовуючи перше рівняння. Чи потрібна мені конкретна бібліотека?
Arthur D. Howland

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. Тож ні більше, ні менше. :)
Л.Гутардт

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
глибоке


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

Гаразд, моє рішення трохи хитке, тому що мій df використовує всі серії, але, скажімо, у вас є такі стовпці, 2 з яких зафіксовано, тобто ваш "фінансовий рік". PoP - це "Період виконання", який є вашими змінними даними:

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

Припустимо, що всі дані мають формат дати та часу, тобто -

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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.