Skip to content

Commit 44d6b28

Browse files
jbrockmendeljreback
authored andcommitted
REF: delegate attrs for CategoricalIndex, IntervalIndex (#30605)
1 parent 0913ed0 commit 44d6b28

File tree

2 files changed

+72
-139
lines changed

2 files changed

+72
-139
lines changed

pandas/core/indexes/category.py

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pandas._typing import AnyArrayLike
1111
import pandas.compat as compat
1212
from pandas.compat.numpy import function as nv
13-
from pandas.util._decorators import Appender, Substitution, cache_readonly
13+
from pandas.util._decorators import Appender, cache_readonly
1414

1515
from pandas.core.dtypes.common import (
1616
ensure_platform_int,
@@ -26,7 +26,6 @@
2626
from pandas.core import accessor
2727
from pandas.core.algorithms import take_1d
2828
from pandas.core.arrays.categorical import Categorical, _recode_for_categories, contains
29-
from pandas.core.base import _shared_docs
3029
import pandas.core.common as com
3130
import pandas.core.indexes.base as ibase
3231
from pandas.core.indexes.base import Index, _index_shared_docs, maybe_extract_name
@@ -37,6 +36,12 @@
3736
_index_doc_kwargs.update(dict(target_klass="CategoricalIndex"))
3837

3938

39+
@accessor.delegate_names(
40+
delegate=Categorical,
41+
accessors=["codes", "categories", "ordered"],
42+
typ="property",
43+
overwrite=True,
44+
)
4045
@accessor.delegate_names(
4146
delegate=Categorical,
4247
accessors=[
@@ -50,6 +55,12 @@
5055
"as_unordered",
5156
"min",
5257
"max",
58+
"is_dtype_equal",
59+
"tolist",
60+
"_internal_get_values",
61+
"_reverse_indexer",
62+
"searchsorted",
63+
"argsort",
5364
],
5465
typ="method",
5566
overwrite=True,
@@ -147,6 +158,20 @@ class CategoricalIndex(Index, accessor.PandasDelegate):
147158

148159
_typ = "categoricalindex"
149160

161+
_raw_inherit = {
162+
"argsort",
163+
"_internal_get_values",
164+
"tolist",
165+
"codes",
166+
"categories",
167+
"ordered",
168+
"_reverse_indexer",
169+
"searchsorted",
170+
}
171+
172+
codes: np.ndarray
173+
categories: Index
174+
150175
@property
151176
def _engine_type(self):
152177
# self.codes can have dtype int8, int16, int32 or int64, so we need
@@ -365,29 +390,6 @@ def _wrap_setop_result(self, other, result):
365390
name = get_op_result_name(self, other)
366391
return self._shallow_copy(result, name=name)
367392

368-
def _internal_get_values(self):
369-
# override base Index version to get the numpy array representation of
370-
# the underlying Categorical
371-
return self._data._internal_get_values()
372-
373-
def tolist(self):
374-
return self._data.tolist()
375-
376-
@property
377-
def codes(self):
378-
return self._data.codes
379-
380-
@property
381-
def categories(self):
382-
return self._data.categories
383-
384-
@property
385-
def ordered(self):
386-
return self._data.ordered
387-
388-
def _reverse_indexer(self):
389-
return self._data._reverse_indexer()
390-
391393
@Appender(_index_shared_docs["contains"] % _index_doc_kwargs)
392394
def __contains__(self, key) -> bool:
393395
# if key is a NaN, check if any NaN is in self.
@@ -424,9 +426,6 @@ def fillna(self, value, downcast=None):
424426
self._assert_can_do_op(value)
425427
return CategoricalIndex(self._data.fillna(value), name=self.name)
426428

427-
def argsort(self, *args, **kwargs):
428-
return self.values.argsort(*args, **kwargs)
429-
430429
@cache_readonly
431430
def _engine(self):
432431
# we are going to look things up with the codes themselves.
@@ -534,11 +533,6 @@ def get_value(self, series: AnyArrayLike, key: Any):
534533
# we might be a positional inexer
535534
return super().get_value(series, key)
536535

537-
@Substitution(klass="CategoricalIndex")
538-
@Appender(_shared_docs["searchsorted"])
539-
def searchsorted(self, value, side="left", sorter=None):
540-
return self._data.searchsorted(value, side=side, sorter=sorter)
541-
542536
@Appender(_index_shared_docs["where"])
543537
def where(self, cond, other=None):
544538
# TODO: Investigate an alternative implementation with
@@ -741,9 +735,6 @@ def take(self, indices, axis=0, allow_fill=True, fill_value=None, **kwargs):
741735
)
742736
return self._create_from_codes(taken)
743737

744-
def is_dtype_equal(self, other):
745-
return self._data.is_dtype_equal(other)
746-
747738
take_nd = take
748739

749740
@Appender(_index_shared_docs["_maybe_cast_slice_bound"])
@@ -877,10 +868,6 @@ def _concat_same_dtype(self, to_concat, name):
877868
result.name = name
878869
return result
879870

880-
def _codes_for_groupby(self, sort, observed):
881-
""" Return a Categorical adjusted for groupby """
882-
return self.values._codes_for_groupby(sort, observed)
883-
884871
@classmethod
885872
def _add_comparison_methods(cls):
886873
""" add in comparison methods """
@@ -906,13 +893,18 @@ def _evaluate_compare(self, other):
906893
cls.__le__ = _make_compare(operator.le)
907894
cls.__ge__ = _make_compare(operator.ge)
908895

896+
def _delegate_property_get(self, name, *args, **kwargs):
897+
""" method delegation to the ._values """
898+
prop = getattr(self._values, name)
899+
return prop # no wrapping for now
900+
909901
def _delegate_method(self, name, *args, **kwargs):
910902
""" method delegation to the ._values """
911903
method = getattr(self._values, name)
912904
if "inplace" in kwargs:
913905
raise ValueError("cannot use inplace with CategoricalIndex")
914906
res = method(*args, **kwargs)
915-
if is_scalar(res):
907+
if is_scalar(res) or name in self._raw_inherit:
916908
return res
917909
return CategoricalIndex(res, name=self.name)
918910

pandas/core/indexes/interval.py

Lines changed: 39 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from pandas.core.dtypes.generic import ABCSeries
3838
from pandas.core.dtypes.missing import isna
3939

40+
from pandas.core import accessor
4041
from pandas.core.algorithms import take_1d
4142
from pandas.core.arrays.interval import IntervalArray, _interval_shared_docs
4243
import pandas.core.common as com
@@ -181,7 +182,28 @@ def func(intvidx_self, other, sort=False):
181182
),
182183
)
183184
)
184-
class IntervalIndex(IntervalMixin, Index):
185+
@accessor.delegate_names(
186+
delegate=IntervalArray,
187+
accessors=[
188+
"_ndarray_values",
189+
"length",
190+
"size",
191+
"left",
192+
"right",
193+
"mid",
194+
"closed",
195+
"dtype",
196+
],
197+
typ="property",
198+
overwrite=True,
199+
)
200+
@accessor.delegate_names(
201+
delegate=IntervalArray,
202+
accessors=["__array__", "overlaps", "contains"],
203+
typ="method",
204+
overwrite=True,
205+
)
206+
class IntervalIndex(IntervalMixin, Index, accessor.PandasDelegate):
185207
_typ = "intervalindex"
186208
_comparables = ["name"]
187209
_attributes = ["name", "closed"]
@@ -192,6 +214,8 @@ class IntervalIndex(IntervalMixin, Index):
192214
# Immutable, so we are able to cache computations like isna in '_mask'
193215
_mask = None
194216

217+
_raw_inherit = {"_ndarray_values", "__array__", "overlaps", "contains"}
218+
195219
# --------------------------------------------------------------------
196220
# Constructors
197221

@@ -388,30 +412,6 @@ def to_tuples(self, na_tuple=True):
388412
def _multiindex(self):
389413
return MultiIndex.from_arrays([self.left, self.right], names=["left", "right"])
390414

391-
@property
392-
def left(self):
393-
"""
394-
Return the left endpoints of each Interval in the IntervalIndex as
395-
an Index.
396-
"""
397-
return self._data._left
398-
399-
@property
400-
def right(self):
401-
"""
402-
Return the right endpoints of each Interval in the IntervalIndex as
403-
an Index.
404-
"""
405-
return self._data._right
406-
407-
@property
408-
def closed(self):
409-
"""
410-
Whether the intervals are closed on the left-side, right-side, both or
411-
neither.
412-
"""
413-
return self._data._closed
414-
415415
@Appender(
416416
_interval_shared_docs["set_closed"]
417417
% dict(
@@ -434,25 +434,8 @@ def closed(self):
434434
)
435435
)
436436
def set_closed(self, closed):
437-
if closed not in _VALID_CLOSED:
438-
raise ValueError(f"invalid option for 'closed': {closed}")
439-
440-
# return self._shallow_copy(closed=closed)
441437
array = self._data.set_closed(closed)
442-
return self._simple_new(array, self.name)
443-
444-
@property
445-
def length(self):
446-
"""
447-
Return an Index with entries denoting the length of each Interval in
448-
the IntervalIndex.
449-
"""
450-
return self._data.length
451-
452-
@property
453-
def size(self):
454-
# Avoid materializing ndarray[Interval]
455-
return self._data.size
438+
return self._simple_new(array, self.name) # TODO: can we use _shallow_copy?
456439

457440
def __len__(self) -> int:
458441
return len(self.left)
@@ -468,16 +451,6 @@ def values(self):
468451
def _values(self):
469452
return self._data
470453

471-
@cache_readonly
472-
def _ndarray_values(self) -> np.ndarray:
473-
return np.array(self._data)
474-
475-
def __array__(self, result=None):
476-
"""
477-
The array interface, return my values.
478-
"""
479-
return self._ndarray_values
480-
481454
def __array_wrap__(self, result, context=None):
482455
# we don't want the superclass implementation
483456
return result
@@ -506,13 +479,6 @@ def astype(self, dtype, copy=True):
506479
return self._shallow_copy(new_values.left, new_values.right)
507480
return super().astype(dtype, copy=copy)
508481

509-
@cache_readonly
510-
def dtype(self):
511-
"""
512-
Return the dtype object of the underlying data.
513-
"""
514-
return self._data.dtype
515-
516482
@property
517483
def inferred_type(self) -> str:
518484
"""Return a string of the type inferred from the values"""
@@ -1177,44 +1143,6 @@ def equals(self, other) -> bool:
11771143
and self.closed == other.closed
11781144
)
11791145

1180-
@Appender(
1181-
_interval_shared_docs["contains"]
1182-
% dict(
1183-
klass="IntervalIndex",
1184-
examples=textwrap.dedent(
1185-
"""\
1186-
>>> intervals = pd.IntervalIndex.from_tuples([(0, 1), (1, 3), (2, 4)])
1187-
>>> intervals
1188-
IntervalIndex([(0, 1], (1, 3], (2, 4]],
1189-
closed='right',
1190-
dtype='interval[int64]')
1191-
>>> intervals.contains(0.5)
1192-
array([ True, False, False])
1193-
"""
1194-
),
1195-
)
1196-
)
1197-
def contains(self, other):
1198-
return self._data.contains(other)
1199-
1200-
@Appender(
1201-
_interval_shared_docs["overlaps"]
1202-
% dict(
1203-
klass="IntervalIndex",
1204-
examples=textwrap.dedent(
1205-
"""\
1206-
>>> intervals = pd.IntervalIndex.from_tuples([(0, 1), (1, 3), (2, 4)])
1207-
>>> intervals
1208-
IntervalIndex([(0, 1], (1, 3], (2, 4]],
1209-
closed='right',
1210-
dtype='interval[int64]')
1211-
"""
1212-
),
1213-
)
1214-
)
1215-
def overlaps(self, other):
1216-
return self._data.overlaps(other)
1217-
12181146
@Appender(_index_shared_docs["intersection"])
12191147
@SetopCheck(op_name="intersection")
12201148
def intersection(
@@ -1314,6 +1242,19 @@ def is_all_dates(self) -> bool:
13141242

13151243
# TODO: arithmetic operations
13161244

1245+
def _delegate_property_get(self, name, *args, **kwargs):
1246+
""" method delegation to the ._values """
1247+
prop = getattr(self._data, name)
1248+
return prop # no wrapping for now
1249+
1250+
def _delegate_method(self, name, *args, **kwargs):
1251+
""" method delegation to the ._data """
1252+
method = getattr(self._data, name)
1253+
res = method(*args, **kwargs)
1254+
if is_scalar(res) or name in self._raw_inherit:
1255+
return res
1256+
return type(self)(res, name=self.name)
1257+
13171258

13181259
IntervalIndex._add_logical_methods_disabled()
13191260

0 commit comments

Comments
 (0)