Skip to content

Commit 99e3afe

Browse files
authored
API: MultiIndex.names|codes|levels returns tuples (#57042)
* MultiIndex.names|codes|levels returns tuples * Fix typing * Add whatsnew note * Fix stacking * Fix doctest, test * Fix other test * Remove example
1 parent 06ec9a4 commit 99e3afe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+246
-492
lines changed

doc/source/user_guide/groupby.rst

-9
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,6 @@ We could naturally group by either the ``A`` or ``B`` columns, or both:
137137

138138
``df.groupby('A')`` is just syntactic sugar for ``df.groupby(df['A'])``.
139139

140-
If we also have a MultiIndex on columns ``A`` and ``B``, we can group by all
141-
the columns except the one we specify:
142-
143-
.. ipython:: python
144-
145-
df2 = df.set_index(["A", "B"])
146-
grouped = df2.groupby(level=df2.index.names.difference(["B"]))
147-
grouped.sum()
148-
149140
The above GroupBy will split the DataFrame on its index (rows). To split by columns, first do
150141
a transpose:
151142

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for mor
8686
Other API changes
8787
^^^^^^^^^^^^^^^^^
8888
- 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`)
89+
- :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`)
8990
-
9091

9192
.. ---------------------------------------------------------------------------

pandas/_libs/index.pyi

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import numpy as np
22

33
from pandas._typing import npt
44

5-
from pandas import MultiIndex
5+
from pandas import (
6+
Index,
7+
MultiIndex,
8+
)
69
from pandas.core.arrays import ExtensionArray
710

811
multiindex_nulls_shift: int
@@ -70,13 +73,13 @@ class MaskedUInt8Engine(MaskedIndexEngine): ...
7073
class MaskedBoolEngine(MaskedUInt8Engine): ...
7174

7275
class BaseMultiIndexCodesEngine:
73-
levels: list[np.ndarray]
76+
levels: tuple[np.ndarray]
7477
offsets: np.ndarray # ndarray[uint64_t, ndim=1]
7578

7679
def __init__(
7780
self,
78-
levels: list[np.ndarray], # all entries hashable
79-
labels: list[np.ndarray], # all entries integer-dtyped
81+
levels: tuple[Index, ...], # all entries hashable
82+
labels: tuple[np.ndarray], # all entries integer-dtyped
8083
offsets: np.ndarray, # np.ndarray[np.uint64, ndim=1]
8184
) -> None: ...
8285
def get_indexer(self, target: npt.NDArray[np.object_]) -> npt.NDArray[np.intp]: ...

pandas/core/groupby/groupby.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5698,7 +5698,7 @@ def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiInde
56985698
idx = cast(MultiIndex, idx)
56995699
levels = list(idx.levels) + [lev]
57005700
codes = [np.repeat(x, nqs) for x in idx.codes] + [np.tile(lev_codes, len(idx))]
5701-
mi = MultiIndex(levels=levels, codes=codes, names=idx.names + [None])
5701+
mi = MultiIndex(levels=levels, codes=codes, names=list(idx.names) + [None])
57025702
else:
57035703
nidx = len(idx)
57045704
idx_codes = coerce_indexer_dtype(np.arange(nidx), idx)

pandas/core/indexes/base.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@
179179
disallow_ndim_indexing,
180180
is_valid_positional_slice,
181181
)
182-
from pandas.core.indexes.frozen import FrozenList
183182
from pandas.core.missing import clean_reindex_fill_method
184183
from pandas.core.ops import get_op_result_name
185184
from pandas.core.ops.invalid import make_invalid_op
@@ -1767,8 +1766,8 @@ def _get_default_index_names(
17671766

17681767
return names
17691768

1770-
def _get_names(self) -> FrozenList:
1771-
return FrozenList((self.name,))
1769+
def _get_names(self) -> tuple[Hashable | None, ...]:
1770+
return (self.name,)
17721771

17731772
def _set_names(self, values, *, level=None) -> None:
17741773
"""
@@ -1866,7 +1865,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
18661865
('python', 2019),
18671866
( 'cobra', 2018),
18681867
( 'cobra', 2019)],
1869-
names=['species', 'year'])
1868+
names=('species', 'year'))
18701869
18711870
When renaming levels with a dict, levels can not be passed.
18721871
@@ -1875,7 +1874,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
18751874
('python', 2019),
18761875
( 'cobra', 2018),
18771876
( 'cobra', 2019)],
1878-
names=['snake', 'year'])
1877+
names=('snake', 'year'))
18791878
"""
18801879
if level is not None and not isinstance(self, ABCMultiIndex):
18811880
raise ValueError("Level must be None for non-MultiIndex")
@@ -1959,19 +1958,19 @@ def rename(self, name, inplace: bool = False) -> Self | None:
19591958
19601959
>>> idx = pd.MultiIndex.from_product([['python', 'cobra'],
19611960
... [2018, 2019]],
1962-
... names=['kind', 'year'])
1961+
... names=('kind', 'year'))
19631962
>>> idx
19641963
MultiIndex([('python', 2018),
19651964
('python', 2019),
19661965
( 'cobra', 2018),
19671966
( 'cobra', 2019)],
1968-
names=['kind', 'year'])
1967+
names=('kind', 'year'))
19691968
>>> idx.rename(['species', 'year'])
19701969
MultiIndex([('python', 2018),
19711970
('python', 2019),
19721971
( 'cobra', 2018),
19731972
( 'cobra', 2019)],
1974-
names=['species', 'year'])
1973+
names=('species', 'year'))
19751974
>>> idx.rename('species')
19761975
Traceback (most recent call last):
19771976
TypeError: Must pass list-like as `names`.
@@ -2135,22 +2134,22 @@ def droplevel(self, level: IndexLabel = 0):
21352134
>>> mi
21362135
MultiIndex([(1, 3, 5),
21372136
(2, 4, 6)],
2138-
names=['x', 'y', 'z'])
2137+
names=('x', 'y', 'z'))
21392138
21402139
>>> mi.droplevel()
21412140
MultiIndex([(3, 5),
21422141
(4, 6)],
2143-
names=['y', 'z'])
2142+
names=('y', 'z'))
21442143
21452144
>>> mi.droplevel(2)
21462145
MultiIndex([(1, 3),
21472146
(2, 4)],
2148-
names=['x', 'y'])
2147+
names=('x', 'y'))
21492148
21502149
>>> mi.droplevel('z')
21512150
MultiIndex([(1, 3),
21522151
(2, 4)],
2153-
names=['x', 'y'])
2152+
names=('x', 'y'))
21542153
21552154
>>> mi.droplevel(['x', 'y'])
21562155
Index([5, 6], dtype='int64', name='z')
@@ -4865,7 +4864,9 @@ def _join_level(
48654864
"""
48664865
from pandas.core.indexes.multi import MultiIndex
48674866

4868-
def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
4867+
def _get_leaf_sorter(
4868+
labels: tuple[np.ndarray, ...] | list[np.ndarray]
4869+
) -> npt.NDArray[np.intp]:
48694870
"""
48704871
Returns sorter for the inner most level while preserving the
48714872
order of higher levels.
@@ -6627,7 +6628,7 @@ def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
66276628
MultiIndex([(1, 'red'),
66286629
(2, 'blue'),
66296630
(3, 'green')],
6630-
names=['number', 'color'])
6631+
names=('number', 'color'))
66316632
66326633
Check whether the strings in the 'color' level of the MultiIndex
66336634
are in a list of colors.
@@ -7608,7 +7609,7 @@ def ensure_index_from_sequences(sequences, names=None) -> Index:
76087609
>>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
76097610
MultiIndex([('a', 'a'),
76107611
('a', 'b')],
7611-
names=['L1', 'L2'])
7612+
names=('L1', 'L2'))
76127613
76137614
See Also
76147615
--------

pandas/core/indexes/frozen.py

-120
This file was deleted.

0 commit comments

Comments
 (0)