Skip to content

API: Revert 57042 - MultiIndex.names|codes|levels returns tuples #57788

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for mor
Other API changes
^^^^^^^^^^^^^^^^^
- 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`)
- :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`)
- :func:`read_table`'s ``parse_dates`` argument defaults to ``None`` to improve consistency with :func:`read_csv` (:issue:`57476`)
- Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`)
- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. Custom styling can still be done using :meth:`Styler.to_excel` (:issue:`54154`)
Expand Down
6 changes: 3 additions & 3 deletions pandas/_libs/index.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ class MaskedUInt8Engine(MaskedIndexEngine): ...
class MaskedBoolEngine(MaskedUInt8Engine): ...

class BaseMultiIndexCodesEngine:
levels: tuple[np.ndarray]
levels: list[np.ndarray]
offsets: np.ndarray # ndarray[uint64_t, ndim=1]

def __init__(
self,
levels: tuple[Index, ...], # all entries hashable
labels: tuple[np.ndarray], # all entries integer-dtyped
levels: list[Index], # all entries hashable
labels: list[np.ndarray], # all entries integer-dtyped
offsets: np.ndarray, # np.ndarray[np.uint64, ndim=1]
) -> None: ...
def get_indexer(self, target: npt.NDArray[np.object_]) -> npt.NDArray[np.intp]: ...
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/groupby/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -5609,7 +5609,7 @@ def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiInde
idx = cast(MultiIndex, idx)
levels = list(idx.levels) + [lev]
codes = [np.repeat(x, nqs) for x in idx.codes] + [np.tile(lev_codes, len(idx))]
mi = MultiIndex(levels=levels, codes=codes, names=list(idx.names) + [None])
mi = MultiIndex(levels=levels, codes=codes, names=idx.names + [None])
else:
nidx = len(idx)
idx_codes = coerce_indexer_dtype(np.arange(nidx), idx)
Expand Down
31 changes: 15 additions & 16 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
disallow_ndim_indexing,
is_valid_positional_slice,
)
from pandas.core.indexes.frozen import FrozenList
from pandas.core.missing import clean_reindex_fill_method
from pandas.core.ops import get_op_result_name
from pandas.core.sorting import (
Expand Down Expand Up @@ -1726,8 +1727,8 @@ def _get_default_index_names(

return names

def _get_names(self) -> tuple[Hashable | None, ...]:
return (self.name,)
def _get_names(self) -> FrozenList:
return FrozenList((self.name,))

def _set_names(self, values, *, level=None) -> None:
"""
Expand Down Expand Up @@ -1821,7 +1822,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('species', 'year'))
names=['species', 'year'])

When renaming levels with a dict, levels can not be passed.

Expand All @@ -1830,7 +1831,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('snake', 'year'))
names=['snake', 'year'])
"""
if level is not None and not isinstance(self, ABCMultiIndex):
raise ValueError("Level must be None for non-MultiIndex")
Expand Down Expand Up @@ -1915,13 +1916,13 @@ def rename(self, name, *, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('kind', 'year'))
names=['kind', 'year'])
>>> idx.rename(["species", "year"])
MultiIndex([('python', 2018),
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('species', 'year'))
names=['species', 'year'])
>>> idx.rename("species")
Traceback (most recent call last):
TypeError: Must pass list-like as `names`.
Expand Down Expand Up @@ -2085,22 +2086,22 @@ def droplevel(self, level: IndexLabel = 0):
>>> mi
MultiIndex([(1, 3, 5),
(2, 4, 6)],
names=('x', 'y', 'z'))
names=['x', 'y', 'z'])

>>> mi.droplevel()
MultiIndex([(3, 5),
(4, 6)],
names=('y', 'z'))
names=['y', 'z'])

>>> mi.droplevel(2)
MultiIndex([(1, 3),
(2, 4)],
names=('x', 'y'))
names=['x', 'y'])

>>> mi.droplevel("z")
MultiIndex([(1, 3),
(2, 4)],
names=('x', 'y'))
names=['x', 'y'])

>>> mi.droplevel(["x", "y"])
Index([5, 6], dtype='int64', name='z')
Expand Down Expand Up @@ -4437,9 +4438,7 @@ def _join_level(
"""
from pandas.core.indexes.multi import MultiIndex

def _get_leaf_sorter(
labels: tuple[np.ndarray, ...] | list[np.ndarray],
) -> npt.NDArray[np.intp]:
def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
"""
Returns sorter for the inner most level while preserving the
order of higher levels.
Expand Down Expand Up @@ -6184,13 +6183,13 @@ def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
array([ True, False, False])

>>> midx = pd.MultiIndex.from_arrays(
... [[1, 2, 3], ["red", "blue", "green"]], names=("number", "color")
... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
... )
>>> midx
MultiIndex([(1, 'red'),
(2, 'blue'),
(3, 'green')],
names=('number', 'color'))
names=['number', 'color'])

Check whether the strings in the 'color' level of the MultiIndex
are in a list of colors.
Expand Down Expand Up @@ -7178,7 +7177,7 @@ def ensure_index_from_sequences(sequences, names=None) -> Index:
>>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
MultiIndex([('a', 'a'),
('a', 'b')],
names=('L1', 'L2'))
names=['L1', 'L2'])

See Also
--------
Expand Down
121 changes: 121 additions & 0 deletions pandas/core/indexes/frozen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
frozen (immutable) data structures to support MultiIndexing

These are used for:

- .names (FrozenList)

"""

from __future__ import annotations

from typing import (
TYPE_CHECKING,
NoReturn,
)

from pandas.core.base import PandasObject

from pandas.io.formats.printing import pprint_thing

if TYPE_CHECKING:
from pandas._typing import Self


class FrozenList(PandasObject, list):
"""
Container that doesn't allow setting item *but*
because it's technically hashable, will be used
for lookups, appropriately, etc.
"""

# Side note: This has to be of type list. Otherwise,
# it messes up PyTables type checks.

def union(self, other) -> FrozenList:
"""
Returns a FrozenList with other concatenated to the end of self.

Parameters
----------
other : array-like
The array-like whose elements we are concatenating.

Returns
-------
FrozenList
The collection difference between self and other.
"""
if isinstance(other, tuple):
other = list(other)
return type(self)(super().__add__(other))

def difference(self, other) -> FrozenList:
"""
Returns a FrozenList with elements from other removed from self.

Parameters
----------
other : array-like
The array-like whose elements we are removing self.

Returns
-------
FrozenList
The collection difference between self and other.
"""
other = set(other)
temp = [x for x in self if x not in other]
return type(self)(temp)

# TODO: Consider deprecating these in favor of `union` (xref gh-15506)
# error: Incompatible types in assignment (expression has type
# "Callable[[FrozenList, Any], FrozenList]", base class "list" defined the
# type as overloaded function)
__add__ = __iadd__ = union # type: ignore[assignment]

def __getitem__(self, n):
if isinstance(n, slice):
return type(self)(super().__getitem__(n))
return super().__getitem__(n)

def __radd__(self, other) -> Self:
if isinstance(other, tuple):
other = list(other)
return type(self)(other + list(self))

def __eq__(self, other: object) -> bool:
if isinstance(other, (tuple, FrozenList)):
other = list(other)
return super().__eq__(other)

__req__ = __eq__

def __mul__(self, other) -> Self:
return type(self)(super().__mul__(other))

__imul__ = __mul__

def __reduce__(self):
return type(self), (list(self),)

# error: Signature of "__hash__" incompatible with supertype "list"
def __hash__(self) -> int: # type: ignore[override]
return hash(tuple(self))

def _disabled(self, *args, **kwargs) -> NoReturn:
"""
This method will not function because object is immutable.
"""
raise TypeError(f"'{type(self).__name__}' does not support mutable operations.")

def __str__(self) -> str:
return pprint_thing(self, quote_strings=True, escape_chars=("\t", "\r", "\n"))

def __repr__(self) -> str:
return f"{type(self).__name__}({self!s})"

__setitem__ = __setslice__ = _disabled # type: ignore[assignment]
__delitem__ = __delslice__ = _disabled
pop = append = extend = _disabled
remove = sort = insert = _disabled # type: ignore[assignment]
Loading