Skip to content

Commit 7d93c89

Browse files
committed
Separate MultiIndex names from levels
1 parent 2efb607 commit 7d93c89

File tree

14 files changed

+83
-60
lines changed

14 files changed

+83
-60
lines changed

doc/source/whatsnew/v0.25.0.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@ is respected in indexing. (:issue:`24076`, :issue:`16785`)
250250
df['2019-01-01 12:00:00+04:00':'2019-01-01 13:00:00+04:00']
251251
252252
253+
.. _whatsnew_0250.api_breaking.MultiIndex._names:
254+
255+
256+
``MultiIndex.levels`` do not hold level names any longer
257+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
258+
259+
A :class:`MultiIndex` previously stored the level names as attributes of each of its
260+
:attr:`MultiIndex.levels`. From Pandas 0.25, the names are only accessed through
261+
:attr:`MultiIndex.names` (which was also possible previously). This is done in order to
262+
make :attr:`MultiIndex.levels` more similar to :attr:`CategoricalIndex.categories`.
263+
264+
253265
.. _whatsnew_0250.api_breaking.multi_indexing:
254266

255267

pandas/core/frame.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7797,7 +7797,8 @@ def _count_level(self, level, axis=0, numeric_only=False):
77977797
if isinstance(level, str):
77987798
level = count_axis._get_level_number(level)
77997799

7800-
level_index = count_axis.levels[level]
7800+
level_name = count_axis._names[level]
7801+
level_index = count_axis.levels[level]._shallow_copy(name=level_name)
78017802
level_codes = ensure_int64(count_axis.codes[level])
78027803
counts = lib.count_level_2d(mask, level_codes, len(level_index), axis=0)
78037804

pandas/core/indexes/multi.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def __new__(
259259
result._set_levels(levels, copy=copy, validate=False)
260260
result._set_codes(codes, copy=copy, validate=False)
261261

262+
result._names = [None for _ in levels]
262263
if names is not None:
263264
# handles name validation
264265
result._set_names(names)
@@ -1176,7 +1177,7 @@ def __len__(self):
11761177
return len(self.codes[0])
11771178

11781179
def _get_names(self):
1179-
return FrozenList(level.name for level in self.levels)
1180+
return FrozenList(self._names)
11801181

11811182
def _set_names(self, names, level=None, validate=True):
11821183
"""
@@ -1222,7 +1223,7 @@ def _set_names(self, names, level=None, validate=True):
12221223
level = [self._get_level_number(l) for l in level]
12231224

12241225
# set the name
1225-
for l, name in zip(level, names):
1226+
for lev, name in zip(level, names):
12261227
if name is not None:
12271228
# GH 20527
12281229
# All items in 'names' need to be hashable:
@@ -1232,7 +1233,7 @@ def _set_names(self, names, level=None, validate=True):
12321233
self.__class__.__name__
12331234
)
12341235
)
1235-
self.levels[l].rename(name, inplace=True)
1236+
self._names[lev] = name
12361237

12371238
names = property(
12381239
fset=_set_names, fget=_get_names, doc="""\nNames of levels in MultiIndex\n"""
@@ -1546,13 +1547,13 @@ def _get_level_values(self, level, unique=False):
15461547
values : ndarray
15471548
"""
15481549

1549-
values = self.levels[level]
1550+
lev = self.levels[level]
15501551
level_codes = self.codes[level]
1552+
name = self._names[level]
15511553
if unique:
15521554
level_codes = algos.unique(level_codes)
1553-
filled = algos.take_1d(values._values, level_codes, fill_value=values._na_value)
1554-
values = values._shallow_copy(filled)
1555-
return values
1555+
filled = algos.take_1d(lev._values, level_codes, fill_value=lev._na_value)
1556+
return lev._shallow_copy(filled, name=name)
15561557

15571558
def get_level_values(self, level):
15581559
"""

pandas/core/reshape/reshape.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,13 @@ def get_new_values(self):
260260
def get_new_columns(self):
261261
if self.value_columns is None:
262262
if self.lift == 0:
263-
return self.removed_level
263+
lev = self.removed_level._shallow_copy()
264+
lev.name = self.removed_name
265+
return lev
264266

265-
lev = self.removed_level
266-
return lev.insert(0, lev._na_value)
267+
lev = self.removed_level.insert(0, item=self.removed_level._na_value)
268+
lev.name = self.removed_name
269+
return lev
267270

268271
stride = len(self.removed_level) + self.lift
269272
width = len(self.value_columns)
@@ -302,7 +305,9 @@ def get_new_index(self):
302305
lev, lab = self.new_index_levels[0], result_codes[0]
303306
if (lab == -1).any():
304307
lev = lev.insert(len(lev), lev._na_value)
305-
return lev.take(lab)
308+
new_index = lev.take(lab)
309+
new_index.name = self.new_index_names[0]
310+
return new_index
306311

307312
return MultiIndex(
308313
levels=self.new_index_levels,
@@ -658,7 +663,9 @@ def _convert_level_number(level_num, columns):
658663
new_names = this.columns.names[:-1]
659664
new_columns = MultiIndex.from_tuples(unique_groups, names=new_names)
660665
else:
661-
new_columns = unique_groups = this.columns.levels[0]
666+
new_columns = this.columns.levels[0]._shallow_copy()
667+
new_columns.name = this.columns.names[0]
668+
unique_groups = new_columns
662669

663670
# time to ravel the values
664671
new_data = {}

pandas/io/json/table_schema.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,10 @@ def build_table_schema(data, index=True, primary_key=None, version=True):
243243

244244
if index:
245245
if data.index.nlevels > 1:
246-
for level in data.index.levels:
247-
fields.append(convert_pandas_type_to_json_field(level))
246+
for level, name in zip(data.index.levels, data.index.names):
247+
new_field = convert_pandas_type_to_json_field(level)
248+
new_field["name"] = name
249+
fields.append(new_field)
248250
else:
249251
fields.append(convert_pandas_type_to_json_field(data.index))
250252

pandas/tests/frame/test_alter_axes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ def test_reset_index(self, float_frame):
979979
):
980980
values = lev.take(level_codes)
981981
name = names[i]
982-
tm.assert_index_equal(values, Index(deleveled[name]))
982+
tm.assert_index_equal(values, Index(deleveled[name]), check_names=False)
983983

984984
stacked.index.names = [None, None]
985985
deleveled2 = stacked.reset_index()

pandas/tests/indexes/multi/test_astype.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def test_astype(idx):
1111
actual = idx.astype("O")
1212
assert_copy(actual.levels, expected.levels)
1313
assert_copy(actual.codes, expected.codes)
14-
assert [level.name for level in actual.levels] == list(expected.names)
14+
assert actual.names == list(expected.names)
1515

1616
with pytest.raises(TypeError, match="^Setting.*dtype.*object"):
1717
idx.astype(np.dtype(int))

pandas/tests/indexes/multi/test_constructor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_constructor_single_level():
1717
levels=[["foo", "bar", "baz", "qux"]], codes=[[0, 1, 2, 3]], names=["first"]
1818
)
1919
assert isinstance(result, MultiIndex)
20-
expected = Index(["foo", "bar", "baz", "qux"], name="first")
20+
expected = Index(["foo", "bar", "baz", "qux"])
2121
tm.assert_index_equal(result.levels[0], expected)
2222
assert result.names == ["first"]
2323

@@ -292,8 +292,9 @@ def test_from_arrays_empty():
292292
# 1 level
293293
result = MultiIndex.from_arrays(arrays=[[]], names=["A"])
294294
assert isinstance(result, MultiIndex)
295-
expected = Index([], name="A")
295+
expected = Index([])
296296
tm.assert_index_equal(result.levels[0], expected)
297+
assert result.names == ["A"]
297298

298299
# N levels
299300
for N in [2, 3]:
@@ -426,8 +427,9 @@ def test_from_product_empty_zero_levels():
426427

427428
def test_from_product_empty_one_level():
428429
result = MultiIndex.from_product([[]], names=["A"])
429-
expected = pd.Index([], name="A")
430+
expected = pd.Index([])
430431
tm.assert_index_equal(result.levels[0], expected)
432+
assert result.names == ["A"]
431433

432434

433435
@pytest.mark.parametrize(

pandas/tests/indexes/multi/test_names.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,25 @@ def test_index_name_retained():
2727

2828

2929
def test_changing_names(idx):
30-
31-
# names should be applied to levels
32-
level_names = [level.name for level in idx.levels]
33-
check_level_names(idx, idx.names)
30+
assert [level.name for level in idx.levels] == [None, None]
3431

3532
view = idx.view()
3633
copy = idx.copy()
3734
shallow_copy = idx._shallow_copy()
3835

39-
# changing names should change level names on object
36+
# changing names should not change level names on object
4037
new_names = [name + "a" for name in idx.names]
4138
idx.names = new_names
42-
check_level_names(idx, new_names)
39+
check_level_names(idx, [None, None])
4340

44-
# but not on copies
45-
check_level_names(view, level_names)
46-
check_level_names(copy, level_names)
47-
check_level_names(shallow_copy, level_names)
41+
# and not on copies
42+
check_level_names(view, [None, None])
43+
check_level_names(copy, [None, None])
44+
check_level_names(shallow_copy, [None, None])
4845

4946
# and copies shouldn't change original
5047
shallow_copy.names = [name + "c" for name in shallow_copy.names]
51-
check_level_names(idx, new_names)
48+
check_level_names(idx, [None, None])
5249

5350

5451
def test_take_preserve_name(idx):
@@ -84,7 +81,8 @@ def test_names(idx, index_names):
8481
# names are assigned in setup
8582
names = index_names
8683
level_names = [level.name for level in idx.levels]
87-
assert names == level_names
84+
assert names == ["first", "second"]
85+
assert level_names == [None, None]
8886

8987
# setting bad names on existing
9088
index = idx
@@ -111,9 +109,8 @@ def test_names(idx, index_names):
111109

112110
# names are assigned
113111
index.names = ["a", "b"]
114-
ind_names = list(index.names)
115112
level_names = [level.name for level in index.levels]
116-
assert ind_names == level_names
113+
assert level_names == [None, None]
117114

118115

119116
def test_duplicate_level_names_access_raises(idx):

pandas/tests/indexes/multi/test_reindex.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ def check_level_names(index, names):
1313
def test_reindex(idx):
1414
result, indexer = idx.reindex(list(idx[:4]))
1515
assert isinstance(result, MultiIndex)
16-
check_level_names(result, idx[:4].names)
16+
check_level_names(result, [None, None])
1717

1818
result, indexer = idx.reindex(list(idx))
1919
assert isinstance(result, MultiIndex)
2020
assert indexer is None
21-
check_level_names(result, idx.names)
21+
check_level_names(result, [None, None])
2222

2323

2424
def test_reindex_level(idx):

0 commit comments

Comments
 (0)