Skip to content

Commit 505fc8c

Browse files
committed
PERF: speed up CategoricalIndex.get_loc
1 parent 8963218 commit 505fc8c

File tree

6 files changed

+70
-30
lines changed

6 files changed

+70
-30
lines changed

asv_bench/benchmarks/indexing_engines.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import numpy as np
22

3-
from pandas._libs.index import (Int64Engine, UInt64Engine, Float64Engine,
4-
ObjectEngine)
3+
from pandas._libs import index as li
4+
5+
6+
def _get_numeric_engines():
7+
engine_names = [
8+
('Int64Engine', np.int64), ('Int32Engine', np.int32),
9+
('Int16Engine', np.int16), ('Int8Engine', np.int8),
10+
('UInt64Engine', np.uint64), ('UInt32Engine', np.uint32),
11+
('UInt16engine', np.uint16), ('UInt8Engine', np.uint8),
12+
('Float64Engine', np.float64), ('Float32Engine', np.float32),
13+
]
14+
return [(getattr(li, engine_name), dtype)
15+
for engine_name, dtype in engine_names if hasattr(li, engine_name)]
516

617

718
class NumericEngineIndexing(object):
819

920
goal_time = 0.2
10-
params = [[Int64Engine, UInt64Engine, Float64Engine],
11-
[np.int64, np.uint64, np.float64],
21+
22+
params = [_get_numeric_engines(),
1223
['monotonic_incr', 'monotonic_decr', 'non_monotonic'],
1324
]
14-
param_names = ['engine', 'dtype', 'index_type']
25+
param_names = ['engine_and_dtype', 'index_type']
1526

16-
def setup(self, engine, dtype, index_type):
27+
def setup(self, engine_and_dtype, index_type):
28+
engine, dtype = engine_and_dtype
1729
N = 10**5
1830
values = list([1] * N + [2] * N + [3] * N)
1931
arr = {
@@ -27,7 +39,7 @@ def setup(self, engine, dtype, index_type):
2739
# code belows avoids populating the mapping etc. while timing.
2840
self.data.get_loc(2)
2941

30-
def time_get_loc(self, engine, dtype, index_type):
42+
def time_get_loc(self, engine_and_dtype, index_type):
3143
self.data.get_loc(2)
3244

3345

@@ -46,7 +58,7 @@ def setup(self, index_type):
4658
'non_monotonic': np.array(list('abc') * N, dtype=object),
4759
}[index_type]
4860

49-
self.data = ObjectEngine(lambda: arr, len(arr))
61+
self.data = li.ObjectEngine(lambda: arr, len(arr))
5062
# code belows avoids populating the mapping etc. while timing.
5163
self.data.get_loc('b')
5264

doc/source/whatsnew/v0.24.0.txt

+4-3
Original file line numberDiff line numberDiff line change
@@ -759,9 +759,10 @@ Removal of prior version deprecations/changes
759759
Performance Improvements
760760
~~~~~~~~~~~~~~~~~~~~~~~~
761761

762-
- Very large improvement in performance of slicing when the index is a :class:`CategoricalIndex`,
763-
both when indexing by label (using .loc) and position(.iloc).
764-
Likewise, slicing a ``CategoricalIndex`` itself (i.e. ``ci[100:200]``) shows similar speed improvements (:issue:`21659`)
762+
- Slicing Series and Dataframes with an monotonically increasing :class:`CategoricalIndex`
763+
is now very fast and has speed comparable to slicing with an ``Int64Index``.
764+
The speed increase is both when indexing by label (using .loc) and position(.iloc) (:issue:`20395`)
765+
- Slicing a :class:`CategoricalIndex` itself (i.e. ``ci[1000:2000]``) shows similar speed improvements as above (:issue:`21659`)
765766
- Improved performance of :func:`Series.describe` in case of numeric dtpyes (:issue:`21274`)
766767
- Improved performance of :func:`pandas.core.groupby.GroupBy.rank` when dealing with tied rankings (:issue:`21237`)
767768
- Improved performance of :func:`DataFrame.set_index` with columns consisting of :class:`Period` objects (:issue:`21582`, :issue:`21606`)

pandas/_libs/algos.pyx

+7-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ from libc.math cimport fabs, sqrt
1010
import numpy as np
1111
cimport numpy as cnp
1212
from numpy cimport (ndarray,
13-
NPY_INT64, NPY_UINT64, NPY_INT32, NPY_INT16, NPY_INT8,
13+
NPY_INT64, NPY_INT32, NPY_INT16, NPY_INT8,
14+
NPY_UINT64, NPY_UINT32, NPY_UINT16, NPY_UINT8,
1415
NPY_FLOAT32, NPY_FLOAT64,
1516
NPY_OBJECT,
1617
int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t,
@@ -359,8 +360,10 @@ ctypedef fused algos_t:
359360
float64_t
360361
float32_t
361362
object
362-
int32_t
363363
int64_t
364+
int32_t
365+
int16_t
366+
int8_t
364367
uint64_t
365368
uint8_t
366369

@@ -866,6 +869,8 @@ is_monotonic_float32 = is_monotonic["float32_t"]
866869
is_monotonic_object = is_monotonic["object"]
867870
is_monotonic_int64 = is_monotonic["int64_t"]
868871
is_monotonic_int32 = is_monotonic["int32_t"]
872+
is_monotonic_int16 = is_monotonic["int16_t"]
873+
is_monotonic_int8 = is_monotonic["int8_t"]
869874
is_monotonic_uint64 = is_monotonic["uint64_t"]
870875
is_monotonic_bool = is_monotonic["uint8_t"]
871876

pandas/_libs/index.pyx

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import cython
55

66
import numpy as np
77
cimport numpy as cnp
8-
from numpy cimport (ndarray, float64_t, int32_t,
9-
int64_t, uint8_t, uint64_t, intp_t,
8+
from numpy cimport (ndarray, intp_t,
9+
float64_t, float32_t,
10+
int64_t, int32_t, int16_t, int8_t,
11+
uint64_t, uint32_t, uint16_t, uint8_t,
1012
# Note: NPY_DATETIME, NPY_TIMEDELTA are only available
1113
# for cimport in cython>=0.27.3
1214
NPY_DATETIME, NPY_TIMEDELTA)

pandas/_libs/index_class_helper.pxi.in

+22-13
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@ WARNING: DO NOT edit .pxi FILE directly, .pxi is generated from .pxi.in
1010

1111
{{py:
1212

13-
# name, dtype, ctype
14-
dtypes = [('Float64', 'float64', 'float64_t'),
15-
('UInt64', 'uint64', 'uint64_t'),
16-
('Int64', 'int64', 'int64_t'),
17-
('Object', 'object', 'object')]
13+
# name, dtype, ctype, hashtable_name, hashtable_dtype
14+
dtypes = [('Float64', 'float64', 'float64_t', 'Float64', 'float64'),
15+
('Float32', 'float32', 'float32_t', 'Float64', 'float64'),
16+
('Int64', 'int64', 'int64_t', 'Int64', 'int64'),
17+
('Int32', 'int32', 'int32_t', 'Int64', 'int64'),
18+
('Int16', 'int16', 'int16_t', 'Int64', 'int64'),
19+
('Int8', 'int8', 'int8_t', 'Int64', 'int64'),
20+
('UInt64', 'uint64', 'uint64_t', 'UInt64', 'uint64'),
21+
('UInt32', 'uint32', 'uint32_t', 'UInt64', 'uint64'),
22+
('UInt16', 'uint16', 'uint16_t', 'UInt64', 'uint64'),
23+
('UInt8', 'uint8', 'uint8_t', 'UInt64', 'uint64'),
24+
('Object', 'object', 'object', 'PyObject', 'object'),
25+
]
1826
}}
1927

20-
{{for name, dtype, ctype in dtypes}}
28+
{{for name, dtype, ctype, hashtable_name, hashtable_dtype in dtypes}}
2129

2230

2331
cdef class {{name}}Engine(IndexEngine):
@@ -34,13 +42,9 @@ cdef class {{name}}Engine(IndexEngine):
3442
other, limit=limit)
3543

3644
cdef _make_hash_table(self, n):
37-
{{if name == 'Object'}}
38-
return _hash.PyObjectHashTable(n)
39-
{{else}}
40-
return _hash.{{name}}HashTable(n)
41-
{{endif}}
45+
return _hash.{{hashtable_name}}HashTable(n)
4246

43-
{{if name != 'Float64' and name != 'Object'}}
47+
{{if name not in {'Float64', 'Float32', 'Object'} }}
4448
cdef _check_type(self, object val):
4549
hash(val)
4650
if util.is_bool_object(val):
@@ -50,6 +54,11 @@ cdef class {{name}}Engine(IndexEngine):
5054
{{endif}}
5155

5256
{{if name != 'Object'}}
57+
cpdef _call_map_locations(self, values):
58+
# self.mapping is of type {{hashtable_name}}HashTable,
59+
# so convert dtype of values
60+
self.mapping.map_locations(algos.ensure_{{hashtable_dtype}}(values))
61+
5362
cdef _get_index_values(self):
5463
return algos.ensure_{{dtype}}(self.vgetter())
5564

@@ -60,7 +69,7 @@ cdef class {{name}}Engine(IndexEngine):
6069
ndarray[{{ctype}}] values
6170
int count = 0
6271

63-
{{if name != 'Float64'}}
72+
{{if name not in {'Float64', 'Float32'} }}
6473
if not util.is_integer_object(val):
6574
raise KeyError(val)
6675
{{endif}}

pandas/core/indexes/category.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,17 @@ class CategoricalIndex(Index, accessor.PandasDelegate):
8484
"""
8585

8686
_typ = 'categoricalindex'
87-
_engine_type = libindex.Int64Engine
87+
88+
@property
89+
def _engine_type(self):
90+
# self.codes can have dtype int8, int16, int32 or int64, so we need
91+
# to return the corresponding engine type (libindex.Int8Engine, etc.).
92+
return {np.int8: libindex.Int8Engine,
93+
np.int16: libindex.Int16Engine,
94+
np.int32: libindex.Int32Engine,
95+
np.int64: libindex.Int64Engine,
96+
}[self.codes.dtype.type]
97+
8898
_attributes = ['name']
8999

90100
def __new__(cls, data=None, categories=None, ordered=None, dtype=None,
@@ -382,7 +392,7 @@ def argsort(self, *args, **kwargs):
382392
def _engine(self):
383393

384394
# we are going to look things up with the codes themselves
385-
return self._engine_type(lambda: self.codes.astype('i8'), len(self))
395+
return self._engine_type(lambda: self.codes, len(self))
386396

387397
# introspection
388398
@cache_readonly
@@ -450,6 +460,7 @@ def get_loc(self, key, method=None):
450460
array([False, True, False, True], dtype=bool)
451461
"""
452462
code = self.categories.get_loc(key)
463+
code = self.codes.dtype.type(code)
453464
try:
454465
return self._engine.get_loc(code)
455466
except KeyError:

0 commit comments

Comments
 (0)