Skip to content

Commit 6466fc6

Browse files
authored
PERF: MultiIndex slicing (#46040)
1 parent 25322fa commit 6466fc6

File tree

3 files changed

+76
-28
lines changed

3 files changed

+76
-28
lines changed

asv_bench/benchmarks/indexing.py

+61-21
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
CategoricalIndex,
1414
DataFrame,
1515
Float64Index,
16-
IndexSlice,
1716
Int64Index,
1817
IntervalIndex,
1918
MultiIndex,
@@ -200,28 +199,69 @@ def time_take(self, index):
200199

201200

202201
class MultiIndexing:
203-
def setup(self):
204-
mi = MultiIndex.from_product([range(1000), range(1000)])
205-
self.s = Series(np.random.randn(1000000), index=mi)
206-
self.df = DataFrame(self.s)
207202

208-
n = 100000
209-
with warnings.catch_warnings(record=True):
210-
self.mdt = DataFrame(
211-
{
212-
"A": np.random.choice(range(10000, 45000, 1000), n),
213-
"B": np.random.choice(range(10, 400), n),
214-
"C": np.random.choice(range(1, 150), n),
215-
"D": np.random.choice(range(10000, 45000), n),
216-
"x": np.random.choice(range(400), n),
217-
"y": np.random.choice(range(25), n),
218-
}
219-
)
220-
self.idx = IndexSlice[20000:30000, 20:30, 35:45, 30000:40000]
221-
self.mdt = self.mdt.set_index(["A", "B", "C", "D"]).sort_index()
203+
params = [True, False]
204+
param_names = ["unique_levels"]
205+
206+
def setup(self, unique_levels):
207+
self.ndim = 2
208+
if unique_levels:
209+
mi = MultiIndex.from_arrays([range(1000000)] * self.ndim)
210+
else:
211+
mi = MultiIndex.from_product([range(1000)] * self.ndim)
212+
self.df = DataFrame(np.random.randn(len(mi)), index=mi)
213+
214+
self.tgt_slice = slice(200, 800)
215+
self.tgt_null_slice = slice(None)
216+
self.tgt_list = list(range(0, 1000, 10))
217+
self.tgt_scalar = 500
218+
219+
bool_indexer = np.zeros(len(mi), dtype=np.bool_)
220+
bool_indexer[slice(0, len(mi), 100)] = True
221+
self.tgt_bool_indexer = bool_indexer
222+
223+
def time_loc_partial_key_slice(self, unique_levels):
224+
self.df.loc[self.tgt_slice, :]
225+
226+
def time_loc_partial_key_null_slice(self, unique_levels):
227+
self.df.loc[self.tgt_null_slice, :]
228+
229+
def time_loc_partial_key_list(self, unique_levels):
230+
self.df.loc[self.tgt_list, :]
231+
232+
def time_loc_partial_key_scalar(self, unique_levels):
233+
self.df.loc[self.tgt_scalar, :]
234+
235+
def time_loc_partial_bool_indexer(self, unique_levels):
236+
self.df.loc[self.tgt_bool_indexer, :]
237+
238+
def time_loc_all_slices(self, unique_levels):
239+
target = tuple([self.tgt_slice] * self.ndim)
240+
self.df.loc[target, :]
241+
242+
def time_loc_all_null_slices(self, unique_levels):
243+
target = tuple([self.tgt_null_slice] * self.ndim)
244+
self.df.loc[target, :]
245+
246+
def time_loc_all_lists(self, unique_levels):
247+
target = tuple([self.tgt_list] * self.ndim)
248+
self.df.loc[target, :]
249+
250+
def time_loc_all_scalars(self, unique_levels):
251+
target = tuple([self.tgt_scalar] * self.ndim)
252+
self.df.loc[target, :]
253+
254+
def time_loc_all_bool_indexers(self, unique_levels):
255+
target = tuple([self.tgt_bool_indexer] * self.ndim)
256+
self.df.loc[target, :]
257+
258+
def time_loc_slice_plus_null_slice(self, unique_levels):
259+
target = (self.tgt_slice, self.tgt_null_slice)
260+
self.df.loc[target, :]
222261

223-
def time_index_slice(self):
224-
self.mdt.loc[self.idx, :]
262+
def time_loc_null_slice_plus_slice(self, unique_levels):
263+
target = (self.tgt_null_slice, self.tgt_slice)
264+
self.df.loc[target, :]
225265

226266

227267
class IntervalIndexing:

doc/source/whatsnew/v1.5.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Performance improvements
255255
- Performance improvement in :meth:`DataFrame.duplicated` when subset consists of only one column (:issue:`45236`)
256256
- Performance improvement in :meth:`.GroupBy.transform` when broadcasting values for user-defined functions (:issue:`45708`)
257257
- Performance improvement in :meth:`.GroupBy.transform` for user-defined functions when only a single group exists (:issue:`44977`)
258-
- Performance improvement in :meth:`MultiIndex.get_locs` (:issue:`45681`)
258+
- Performance improvement in :meth:`MultiIndex.get_locs` (:issue:`45681`, :issue:`46040`)
259259
- Performance improvement in :func:`merge` when left and/or right are empty (:issue:`45838`)
260260
- Performance improvement in :meth:`DataFrame.join` when left and/or right are empty (:issue:`46015`)
261261
- Performance improvement in :class:`DataFrame` and :class:`Series` constructors for extension dtype scalars (:issue:`45854`)

pandas/core/indexes/multi.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -2762,7 +2762,7 @@ def _partial_tup_index(self, tup: tuple, side="left"):
27622762
if lab not in lev and not isna(lab):
27632763
# short circuit
27642764
try:
2765-
loc = lev.searchsorted(lab, side=side)
2765+
loc = algos.searchsorted(lev, lab, side=side)
27662766
except TypeError as err:
27672767
# non-comparable e.g. test_slice_locs_with_type_mismatch
27682768
raise TypeError(f"Level type mismatch: {lab}") from err
@@ -2771,7 +2771,7 @@ def _partial_tup_index(self, tup: tuple, side="left"):
27712771
raise TypeError(f"Level type mismatch: {lab}")
27722772
if side == "right" and loc >= 0:
27732773
loc -= 1
2774-
return start + section.searchsorted(loc, side=side)
2774+
return start + algos.searchsorted(section, loc, side=side)
27752775

27762776
idx = self._get_loc_single_level_index(lev, lab)
27772777
if isinstance(idx, slice) and k < n - 1:
@@ -2780,13 +2780,21 @@ def _partial_tup_index(self, tup: tuple, side="left"):
27802780
start = idx.start
27812781
end = idx.stop
27822782
elif k < n - 1:
2783-
end = start + section.searchsorted(idx, side="right")
2784-
start = start + section.searchsorted(idx, side="left")
2783+
# error: Incompatible types in assignment (expression has type
2784+
# "Union[ndarray[Any, dtype[signedinteger[Any]]]
2785+
end = start + algos.searchsorted( # type: ignore[assignment]
2786+
section, idx, side="right"
2787+
)
2788+
# error: Incompatible types in assignment (expression has type
2789+
# "Union[ndarray[Any, dtype[signedinteger[Any]]]
2790+
start = start + algos.searchsorted( # type: ignore[assignment]
2791+
section, idx, side="left"
2792+
)
27852793
elif isinstance(idx, slice):
27862794
idx = idx.start
2787-
return start + section.searchsorted(idx, side=side)
2795+
return start + algos.searchsorted(section, idx, side=side)
27882796
else:
2789-
return start + section.searchsorted(idx, side=side)
2797+
return start + algos.searchsorted(section, idx, side=side)
27902798

27912799
def _get_loc_single_level_index(self, level_index: Index, key: Hashable) -> int:
27922800
"""

0 commit comments

Comments
 (0)