Skip to content

Commit 1382aa3

Browse files
committed
Merge pull request #8680 from shoyer/reversed-monotonic-slices
ENH: slicing with decreasing monotonic indexes
2 parents 9d8b3a1 + db6f8fd commit 1382aa3

File tree

11 files changed

+347
-102
lines changed

11 files changed

+347
-102
lines changed

doc/source/api.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,8 @@ Attributes
11661166

11671167
Index.values
11681168
Index.is_monotonic
1169+
Index.is_monotonic_increasing
1170+
Index.is_monotonic_decreasing
11691171
Index.is_unique
11701172
Index.dtype
11711173
Index.inferred_type

doc/source/whatsnew/v0.15.1.txt

+26-2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,29 @@ API changes
146146

147147
s.dt.hour
148148

149+
- support for slicing with monotonic decreasing indexes, even if ``start`` or ``stop`` is
150+
not found in the index (:issue:`7860`):
151+
152+
.. ipython:: python
153+
154+
s = pd.Series(['a', 'b', 'c', 'd'], [4, 3, 2, 1])
155+
s
156+
157+
previous behavior:
158+
159+
.. code-block:: python
160+
161+
In [8]: s.loc[3.5:1.5]
162+
KeyError: 3.5
163+
164+
current behavior:
165+
166+
.. ipython:: python
167+
168+
s.loc[3.5:1.5]
169+
170+
- added Index properties `is_monotonic_increasing` and `is_monotonic_decreasing` (:issue:`8680`).
171+
149172
.. _whatsnew_0151.enhancements:
150173

151174
Enhancements
@@ -208,8 +231,9 @@ Bug Fixes
208231
- Bug in ix/loc block splitting on setitem (manifests with integer-like dtypes, e.g. datetime64) (:issue:`8607`)
209232

210233

211-
212-
234+
- Bug when doing label based indexing with integers not found in the index for
235+
non-unique but monotonic indexes (:issue:`8680`).
236+
- Bug when indexing a Float64Index with ``np.nan`` on numpy 1.7 (:issue:`8980`).
213237

214238

215239

pandas/core/generic.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,7 @@ def xs(self, key, axis=0, level=None, copy=None, drop_level=True):
14611461
name=self.index[loc])
14621462

14631463
else:
1464-
result = self[loc]
1464+
result = self.iloc[loc]
14651465
result.index = new_index
14661466

14671467
# this could be a view

pandas/core/index.py

+28-15
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,22 @@ def _mpl_repr(self):
573573

574574
@property
575575
def is_monotonic(self):
576-
""" return if the index has monotonic (only equaly or increasing) values """
577-
return self._engine.is_monotonic
576+
""" alias for is_monotonic_increasing (deprecated) """
577+
return self._engine.is_monotonic_increasing
578+
579+
@property
580+
def is_monotonic_increasing(self):
581+
""" return if the index is monotonic increasing (only equal or
582+
increasing) values
583+
"""
584+
return self._engine.is_monotonic_increasing
585+
586+
@property
587+
def is_monotonic_decreasing(self):
588+
""" return if the index is monotonic decreasing (only equal or
589+
decreasing values
590+
"""
591+
return self._engine.is_monotonic_decreasing
578592

579593
def is_lexsorted_for_tuple(self, tup):
580594
return True
@@ -1988,16 +2002,12 @@ def _get_slice(starting_value, offset, search_side, slice_property,
19882002
slc += offset
19892003

19902004
except KeyError:
1991-
if self.is_monotonic:
1992-
1993-
# we are duplicated but non-unique
1994-
# so if we have an indexer then we are done
1995-
# else search for it (GH 7523)
1996-
if not is_unique and is_integer(search_value):
1997-
slc = search_value
1998-
else:
1999-
slc = self.searchsorted(search_value,
2000-
side=search_side)
2005+
if self.is_monotonic_increasing:
2006+
slc = self.searchsorted(search_value, side=search_side)
2007+
elif self.is_monotonic_decreasing:
2008+
search_side = 'right' if search_side == 'left' else 'left'
2009+
slc = len(self) - self[::-1].searchsorted(search_value,
2010+
side=search_side)
20012011
else:
20022012
raise
20032013
return slc
@@ -2431,10 +2441,13 @@ def __contains__(self, other):
24312441
def get_loc(self, key):
24322442
try:
24332443
if np.all(np.isnan(key)):
2444+
nan_idxs = self._nan_idxs
24342445
try:
2435-
return self._nan_idxs.item()
2436-
except ValueError:
2437-
return self._nan_idxs
2446+
return nan_idxs.item()
2447+
except (ValueError, IndexError):
2448+
# should only need to catch ValueError here but on numpy
2449+
# 1.7 .item() can raise IndexError when NaNs are present
2450+
return nan_idxs
24382451
except (TypeError, NotImplementedError):
24392452
pass
24402453
return super(Float64Index, self).get_loc(key)

pandas/index.pyx

+25-14
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ cdef class IndexEngine:
7777
bint over_size_threshold
7878

7979
cdef:
80-
bint unique, monotonic
80+
bint unique, monotonic_inc, monotonic_dec
8181
bint initialized, monotonic_check, unique_check
8282

8383
def __init__(self, vgetter, n):
@@ -89,7 +89,8 @@ cdef class IndexEngine:
8989
self.monotonic_check = 0
9090

9191
self.unique = 0
92-
self.monotonic = 0
92+
self.monotonic_inc = 0
93+
self.monotonic_dec = 0
9394

9495
def __contains__(self, object val):
9596
self._ensure_mapping_populated()
@@ -134,7 +135,7 @@ cdef class IndexEngine:
134135
if is_definitely_invalid_key(val):
135136
raise TypeError
136137

137-
if self.over_size_threshold and self.is_monotonic:
138+
if self.over_size_threshold and self.is_monotonic_increasing:
138139
if not self.is_unique:
139140
return self._get_loc_duplicates(val)
140141
values = self._get_index_values()
@@ -158,7 +159,7 @@ cdef class IndexEngine:
158159
cdef:
159160
Py_ssize_t diff
160161

161-
if self.is_monotonic:
162+
if self.is_monotonic_increasing:
162163
values = self._get_index_values()
163164
left = values.searchsorted(val, side='left')
164165
right = values.searchsorted(val, side='right')
@@ -210,25 +211,35 @@ cdef class IndexEngine:
210211

211212
return self.unique == 1
212213

213-
property is_monotonic:
214+
property is_monotonic_increasing:
214215

215216
def __get__(self):
216217
if not self.monotonic_check:
217218
self._do_monotonic_check()
218219

219-
return self.monotonic == 1
220+
return self.monotonic_inc == 1
221+
222+
property is_monotonic_decreasing:
223+
224+
def __get__(self):
225+
if not self.monotonic_check:
226+
self._do_monotonic_check()
227+
228+
return self.monotonic_dec == 1
220229

221230
cdef inline _do_monotonic_check(self):
222231
try:
223232
values = self._get_index_values()
224-
self.monotonic, unique = self._call_monotonic(values)
233+
self.monotonic_inc, self.monotonic_dec, unique = \
234+
self._call_monotonic(values)
225235

226236
if unique is not None:
227237
self.unique = unique
228238
self.unique_check = 1
229239

230240
except TypeError:
231-
self.monotonic = 0
241+
self.monotonic_inc = 0
242+
self.monotonic_dec = 0
232243
self.monotonic_check = 1
233244

234245
cdef _get_index_values(self):
@@ -345,7 +356,7 @@ cdef class Int64Engine(IndexEngine):
345356
return _hash.Int64HashTable(n)
346357

347358
def _call_monotonic(self, values):
348-
return algos.is_monotonic_int64(values)
359+
return algos.is_monotonic_int64(values, timelike=False)
349360

350361
def get_pad_indexer(self, other, limit=None):
351362
return algos.pad_int64(self._get_index_values(), other,
@@ -435,7 +446,7 @@ cdef class Float64Engine(IndexEngine):
435446
return result
436447

437448
def _call_monotonic(self, values):
438-
return algos.is_monotonic_float64(values)
449+
return algos.is_monotonic_float64(values, timelike=False)
439450

440451
def get_pad_indexer(self, other, limit=None):
441452
return algos.pad_float64(self._get_index_values(), other,
@@ -489,7 +500,7 @@ cdef class ObjectEngine(IndexEngine):
489500
return _hash.PyObjectHashTable(n)
490501

491502
def _call_monotonic(self, values):
492-
return algos.is_monotonic_object(values)
503+
return algos.is_monotonic_object(values, timelike=False)
493504

494505
def get_pad_indexer(self, other, limit=None):
495506
return algos.pad_object(self._get_index_values(), other,
@@ -506,7 +517,7 @@ cdef class DatetimeEngine(Int64Engine):
506517
return 'M8[ns]'
507518

508519
def __contains__(self, object val):
509-
if self.over_size_threshold and self.is_monotonic:
520+
if self.over_size_threshold and self.is_monotonic_increasing:
510521
if not self.is_unique:
511522
return self._get_loc_duplicates(val)
512523
values = self._get_index_values()
@@ -521,15 +532,15 @@ cdef class DatetimeEngine(Int64Engine):
521532
return self.vgetter().view('i8')
522533

523534
def _call_monotonic(self, values):
524-
return algos.is_monotonic_int64(values)
535+
return algos.is_monotonic_int64(values, timelike=True)
525536

526537
cpdef get_loc(self, object val):
527538
if is_definitely_invalid_key(val):
528539
raise TypeError
529540

530541
# Welcome to the spaghetti factory
531542

532-
if self.over_size_threshold and self.is_monotonic:
543+
if self.over_size_threshold and self.is_monotonic_increasing:
533544
if not self.is_unique:
534545
val = _to_i8(val)
535546
return self._get_loc_duplicates(val)

pandas/src/generate_code.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -539,31 +539,51 @@ def diff_2d_%(name)s(ndarray[%(c_type)s, ndim=2] arr,
539539

540540
is_monotonic_template = """@cython.boundscheck(False)
541541
@cython.wraparound(False)
542-
def is_monotonic_%(name)s(ndarray[%(c_type)s] arr):
542+
def is_monotonic_%(name)s(ndarray[%(c_type)s] arr, bint timelike):
543543
'''
544544
Returns
545545
-------
546-
is_monotonic, is_unique
546+
is_monotonic_inc, is_monotonic_dec, is_unique
547547
'''
548548
cdef:
549549
Py_ssize_t i, n
550550
%(c_type)s prev, cur
551551
bint is_unique = 1
552+
bint is_monotonic_inc = 1
553+
bint is_monotonic_dec = 1
552554
553555
n = len(arr)
554556
555-
if n < 2:
556-
return True, True
557+
if n == 1:
558+
if arr[0] != arr[0] or (timelike and arr[0] == iNaT):
559+
# single value is NaN
560+
return False, False, True
561+
else:
562+
return True, True, True
563+
elif n < 2:
564+
return True, True, True
565+
566+
if timelike and arr[0] == iNaT:
567+
return False, False, None
557568
558569
prev = arr[0]
559570
for i in range(1, n):
560571
cur = arr[i]
572+
if timelike and cur == iNaT:
573+
return False, False, None
561574
if cur < prev:
562-
return False, None
575+
is_monotonic_inc = 0
576+
elif cur > prev:
577+
is_monotonic_dec = 0
563578
elif cur == prev:
564579
is_unique = 0
580+
else:
581+
# cur or prev is NaN
582+
return False, False, None
583+
if not is_monotonic_inc and not is_monotonic_dec:
584+
return False, False, None
565585
prev = cur
566-
return True, is_unique
586+
return is_monotonic_inc, is_monotonic_dec, is_unique
567587
"""
568588

569589
map_indices_template = """@cython.wraparound(False)

0 commit comments

Comments
 (0)