Skip to content

Commit fd3434a

Browse files
committed
BUG: TimedeltaIndex.intersection
Fixes #17391
1 parent 46856c3 commit fd3434a

File tree

6 files changed

+180
-99
lines changed

6 files changed

+180
-99
lines changed

pandas/core/indexes/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,12 @@ def is_monotonic(self):
12061206
""" alias for is_monotonic_increasing (deprecated) """
12071207
return self.is_monotonic_increasing
12081208

1209+
@property
1210+
def is_strictly_monotonic(self):
1211+
""" Checks if the index is sorted """
1212+
return (self._is_strictly_monotonic_increasing or
1213+
self._is_strictly_monotonic_decreasing)
1214+
12091215
@property
12101216
def is_monotonic_increasing(self):
12111217
"""

pandas/core/indexes/datetimelike.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,108 @@ def _concat_same_dtype(self, to_concat, name):
854854
new_data = np.concatenate([c.asi8 for c in to_concat])
855855
return self._simple_new(new_data, **attribs)
856856

857+
def _slice_by_value(self, start, end):
858+
sliced = self.values[slice(*self.slice_locs(start, end))]
859+
return self._shallow_copy(sliced)
860+
861+
def _intersect_ascending(self, other, **empty_params):
862+
# to make our life easier, "sort" the two ranges
863+
if self[0] <= other[0]:
864+
left, right = self, other
865+
else:
866+
left, right = other, self
867+
868+
end = min(left[-1], right[-1])
869+
start = right[0]
870+
871+
if end < start:
872+
return type(self)(data=[], **empty_params)
873+
return left._slice_by_value(start, end)
874+
875+
def _intersect_descending(self, other, **empty_params):
876+
# this is essentially a flip of _intersect_ascending
877+
if self[0] >= other[0]:
878+
left, right = self, other
879+
else:
880+
left, right = other, self
881+
882+
start = min(left[0], right[0])
883+
end = left[-1]
884+
885+
if end > start:
886+
return type(self)(data=[], **empty_params)
887+
return left._slice_by_value(start, end)
888+
889+
def _offsets_equal(self, other):
890+
offsets_equal = True
891+
self_offset = getattr(self, 'offset', None)
892+
other_offset = getattr(other, 'offset', None)
893+
if self_offset is None or other_offset is None or \
894+
self_offset != other_offset or other_offset.isAnchored():
895+
offsets_equal = False
896+
return offsets_equal
897+
898+
def intersection(self, other):
899+
"""
900+
Specialized intersection for DateTimeIndexOpsMixin objects.
901+
May be much faster than Index.intersection.
902+
903+
Parameters
904+
----------
905+
other : Index or array-like
906+
907+
Returns
908+
-------
909+
Index
910+
A shallow copied intersection between the two things passed in
911+
"""
912+
self._assert_can_do_setop(other)
913+
914+
if self.equals(other):
915+
return self._get_consensus_name(other)
916+
917+
if not isinstance(other, DatetimeIndexOpsMixin):
918+
try:
919+
other = DatetimeIndexOpsMixin(other)
920+
except (TypeError, ValueError):
921+
pass
922+
result = Index.intersection(self, other)
923+
return result
924+
elif (self._offsets_equal(other) or
925+
(not self.is_strictly_monotonic or
926+
not other.is_strictly_monotonic)):
927+
result = Index.intersection(self, other)
928+
tz = getattr(self, 'tz', None)
929+
result = self._shallow_copy(result._values, name=result.name,
930+
tz=tz, freq=None)
931+
if result.freq is None:
932+
result.offset = frequencies.to_offset(result.inferred_freq)
933+
return result
934+
935+
if len(self) == 0:
936+
return self
937+
if len(other) == 0:
938+
return other
939+
940+
# coerce into same order
941+
self_ascending = self.is_monotonic_increasing
942+
if self_ascending != other.is_monotonic_increasing:
943+
other = other.sort_values(ascending=self_ascending)
944+
945+
# Thanks, PeriodIndex
946+
empty_params = {'freq': getattr(self, 'freq', None)}
947+
948+
if self_ascending:
949+
intersected = self._intersect_ascending(other, **empty_params)
950+
else:
951+
intersected = self._intersect_descending(other, **empty_params)
952+
953+
name = self.name
954+
if self.name != other.name:
955+
name = None
956+
intersected.name = name
957+
return intersected
958+
857959

858960
def _ensure_datetimelike_to_i8(other):
859961
""" helper for coercing an input scalar or array to i8 """

pandas/core/indexes/datetimes.py

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,62 +1189,6 @@ def _wrap_union_result(self, other, result):
11891189
raise ValueError('Passed item and index have different timezone')
11901190
return self._simple_new(result, name=name, freq=None, tz=self.tz)
11911191

1192-
def intersection(self, other):
1193-
"""
1194-
Specialized intersection for DatetimeIndex objects. May be much faster
1195-
than Index.intersection
1196-
1197-
Parameters
1198-
----------
1199-
other : DatetimeIndex or array-like
1200-
1201-
Returns
1202-
-------
1203-
y : Index or DatetimeIndex
1204-
"""
1205-
self._assert_can_do_setop(other)
1206-
if not isinstance(other, DatetimeIndex):
1207-
try:
1208-
other = DatetimeIndex(other)
1209-
except (TypeError, ValueError):
1210-
pass
1211-
result = Index.intersection(self, other)
1212-
if isinstance(result, DatetimeIndex):
1213-
if result.freq is None:
1214-
result.offset = to_offset(result.inferred_freq)
1215-
return result
1216-
1217-
elif (other.offset is None or self.offset is None or
1218-
other.offset != self.offset or
1219-
not other.offset.isAnchored() or
1220-
(not self.is_monotonic or not other.is_monotonic)):
1221-
result = Index.intersection(self, other)
1222-
result = self._shallow_copy(result._values, name=result.name,
1223-
tz=result.tz, freq=None)
1224-
if result.freq is None:
1225-
result.offset = to_offset(result.inferred_freq)
1226-
return result
1227-
1228-
if len(self) == 0:
1229-
return self
1230-
if len(other) == 0:
1231-
return other
1232-
# to make our life easier, "sort" the two ranges
1233-
if self[0] <= other[0]:
1234-
left, right = self, other
1235-
else:
1236-
left, right = other, self
1237-
1238-
end = min(left[-1], right[-1])
1239-
start = right[0]
1240-
1241-
if end < start:
1242-
return type(self)(data=[])
1243-
else:
1244-
lslice = slice(*left.slice_locs(start, end))
1245-
left_chunk = left.values[lslice]
1246-
return self._shallow_copy(left_chunk)
1247-
12481192
def _parsed_string_to_bounds(self, reso, parsed):
12491193
"""
12501194
Calculate datetime bounds for parsed time string and its resolution.

pandas/core/indexes/period.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ def _maybe_cast_slice_bound(self, label, side, kind):
843843
Value of `side` parameter should be validated in caller.
844844
845845
"""
846-
assert kind in ['ix', 'loc', 'getitem']
846+
assert kind in ['ix', 'loc', 'getitem', None]
847847

848848
if isinstance(label, datetime):
849849
return Period(label, freq=self.freq)

pandas/core/indexes/timedeltas.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -596,48 +596,6 @@ def _wrap_union_result(self, other, result):
596596
name = self.name if self.name == other.name else None
597597
return self._simple_new(result, name=name, freq=None)
598598

599-
def intersection(self, other):
600-
"""
601-
Specialized intersection for TimedeltaIndex objects. May be much faster
602-
than Index.intersection
603-
604-
Parameters
605-
----------
606-
other : TimedeltaIndex or array-like
607-
608-
Returns
609-
-------
610-
y : Index or TimedeltaIndex
611-
"""
612-
self._assert_can_do_setop(other)
613-
if not isinstance(other, TimedeltaIndex):
614-
try:
615-
other = TimedeltaIndex(other)
616-
except (TypeError, ValueError):
617-
pass
618-
result = Index.intersection(self, other)
619-
return result
620-
621-
if len(self) == 0:
622-
return self
623-
if len(other) == 0:
624-
return other
625-
# to make our life easier, "sort" the two ranges
626-
if self[0] <= other[0]:
627-
left, right = self, other
628-
else:
629-
left, right = other, self
630-
631-
end = min(left[-1], right[-1])
632-
start = right[0]
633-
634-
if end < start:
635-
return type(self)(data=[])
636-
else:
637-
lslice = slice(*left.slice_locs(start, end))
638-
left_chunk = left.values[lslice]
639-
return self._shallow_copy(left_chunk)
640-
641599
def _maybe_promote(self, other):
642600
if other.inferred_type == 'timedelta':
643601
other = TimedeltaIndex(other)

pandas/tests/indexes/timedeltas/test_setops.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,74 @@ def test_intersection_bug_1708(self):
7474
result = index_1 & index_2
7575
expected = timedelta_range('1 day 01:00:00', periods=3, freq='h')
7676
tm.assert_index_equal(result, expected)
77+
78+
79+
def test_intersection_intersects_ascending():
80+
idx1 = pd.to_timedelta(range(2, 6), unit='s')
81+
idx2 = pd.to_timedelta(range(3), unit='s')
82+
result = idx1.intersection(idx2)
83+
assert result.equals(TimedeltaIndex(['00:00:02']))
84+
85+
# test that it works both ways
86+
idx1 = pd.to_timedelta(range(3), unit='s')
87+
idx2 = pd.to_timedelta(range(2, 6), unit='s')
88+
result = idx1.intersection(idx2)
89+
assert result.equals(TimedeltaIndex(['00:00:02']))
90+
91+
92+
def test_intersection_intersects_descending():
93+
# GH 17391
94+
idx1 = pd.to_timedelta(range(6, 3, -1), unit='s')
95+
idx2 = pd.to_timedelta(range(5, 1, -1), unit='s')
96+
result = idx1.intersection(idx2)
97+
expected = TimedeltaIndex(['00:00:05', '00:00:04'],
98+
dtype='timedelta64[ns]')
99+
assert result.equals(expected)
100+
101+
# test it works both ways
102+
idx1 = pd.to_timedelta(range(5, 1, -1), unit='s')
103+
idx2 = pd.to_timedelta(range(6, 3, -1), unit='s')
104+
result = idx1.intersection(idx2)
105+
assert result.equals(expected)
106+
107+
108+
def test_intersection_intersects_descending_no_intersect():
109+
idx1 = pd.to_timedelta(range(6, 4, -1), unit='s')
110+
idx2 = pd.to_timedelta(range(4, 1, -1), unit='s')
111+
result = idx1.intersection(idx2)
112+
assert len(result) == 0
113+
114+
115+
def test_intersection_intersects_len_1():
116+
idx1 = pd.to_timedelta(range(1, 2), unit='s')
117+
idx2 = pd.to_timedelta(range(1, 0, -1), unit='s')
118+
intersection = idx1.intersection(idx2)
119+
expected = TimedeltaIndex(['00:00:01'],
120+
dtype='timedelta64[ns]')
121+
tm.assert_index_equal(intersection, expected)
122+
123+
124+
def test_intersection_can_intersect_self():
125+
idx = pd.to_timedelta(range(1, 2), unit='s')
126+
result = idx.intersection(idx)
127+
tm.assert_index_equal(idx, result)
128+
129+
130+
def test_intersection_not_sorted():
131+
idx1 = pd.to_timedelta((1, 3, 2, 5, 4), unit='s')
132+
idx2 = pd.to_timedelta((1, 2, 3, 5, 4), unit='s')
133+
result = idx1.intersection(idx2)
134+
expected = idx1
135+
tm.assert_index_equal(result, expected)
136+
137+
138+
def test_intersection_not_unique():
139+
idx1 = pd.to_timedelta((1, 2, 2, 3, 3, 5), unit='s')
140+
idx2 = pd.to_timedelta((1, 2, 3, 4), unit='s')
141+
result = idx1.intersection(idx2)
142+
expected = pd.to_timedelta((1, 2, 2, 3, 3), unit='s')
143+
tm.assert_index_equal(result, expected)
144+
145+
result = idx2.intersection(idx1)
146+
expected = pd.to_timedelta((1, 2, 2, 3, 3), unit='s')
147+
tm.assert_index_equal(result, expected)

0 commit comments

Comments
 (0)