Pythonでの効率的な日付範囲の重複計算?


85

2つの日付範囲があり、各範囲は開始日と終了日によって決定されます(明らかに、datetime.date()インスタンス)。2つの範囲は重複する場合と重複しない場合があります。重なりの日数が必要です。もちろん、両方の範囲内のすべての日付で2つのセットを事前に入力し、セットの共通部分を実行することはできますが、これはおそらく非効率的です...すべてのケースをカバーする長いif-elifセクションを使用する別のソリューションとは別のより良い方法はありますか?

回答:


175
  • 2つの開始日の最も遅い日付と2つの終了日の最も早い日付を決定します。
  • それらを引くことによってタイムデルタを計算します。
  • デルタが正の場合、それはオーバーラップの日数です。

計算例は次のとおりです。

>>> 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を返します
ダークレス2013

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オーバーラップ計算でを逃したと思います(間隔が両端で閉じているために必要です)。
レイモンドヘッティンガー2015年

ああ、あなたは絶対に正しいです、私はそれを逃したようです。ありがとう:)
ダークレス2015

1
2つの日付ではなく2回計算したい場合はどうなりますか?@RaymondHettinger
エリック

1
時刻付きの日時オブジェクトを使用する場合は、.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
このメソッドは常に正しい答えを返すとは限りません。たとえば、1Range = 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を印刷します
tkyass 2016年

最初の方程式を使用すると、あいまいな級数エラーが発生します。特定のライブラリが必要ですか?
アーサーD.ハウランド

6

以下に示すように、TimeRangeクラスを実装しました。

get_overlapped_rangeは、最初に単純な条件によってすべての重複していないオプションを無効にし、次にすべての可能なオプションを考慮して重複範囲を計算します。

日数を取得するには、get_overlapped_rangeから返されたTimeRange値を取得し、期間を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同意しましたが、このソリューションは整理されており、より多くの機能が付属しています
Elad Sofer 2018

1
わかりました...それはより多くの機能がいいですが、実際にはStackOverflowでは答えはOPの指定されたニーズにぴったり合うはずです。したがって、これ以上でもそれ以下でもありません。:)
L. Guthardt 2018

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()

DateTimeRange()内の「2015-01-01T00:00:00 + 0900」は、Timestamp( '2017-08-30 20:36:25')のように日時形式にすることもできます。


1
おかげで、DateTimeRangeパッケージのドキュメントを見てみたところ、is_intersection2つの日付範囲の間に交差があるかどうかに応じて、ブール値(TrueまたはFalse)をネイティブに返すものをサポートしているようです。だから、あなたの例のために:それらが他の場所で交差する場合にtime_range1.is_intersection(time_range2)戻るだろうTrueFalse
深い

3

擬似コード:

 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

私の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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.