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

+12
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

+2-1
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

+8-7
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

+12-5
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

+4-2
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

+1-1
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

+1-1
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

+5-3
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

+11-14
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

+2-2
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):

pandas/tests/indexes/multi/test_reshape.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ def test_insert(idx):
1515
# key not contained in all levels
1616
new_index = idx.insert(0, ("abc", "three"))
1717

18-
exp0 = Index(list(idx.levels[0]) + ["abc"], name="first")
18+
exp0 = Index(list(idx.levels[0]) + ["abc"])
1919
tm.assert_index_equal(new_index.levels[0], exp0)
20+
assert new_index.names == ["first", "second"]
2021

21-
exp1 = Index(list(idx.levels[1]) + ["three"], name="second")
22+
exp1 = Index(list(idx.levels[1]) + ["three"])
2223
tm.assert_index_equal(new_index.levels[1], exp1)
2324
assert new_index[0] == ("abc", "three")
2425

pandas/tests/reshape/test_concat.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1220,8 +1220,10 @@ def test_concat_keys_specific_levels(self):
12201220
names=["group_key"],
12211221
)
12221222

1223-
tm.assert_index_equal(result.columns.levels[0], Index(level, name="group_key"))
1224-
assert result.columns.names[0] == "group_key"
1223+
tm.assert_index_equal(result.columns.levels[0], Index(level))
1224+
tm.assert_index_equal(result.columns.levels[1], Index([0, 1, 2, 3]))
1225+
1226+
assert result.columns.names == ["group_key", None]
12251227

12261228
def test_concat_dataframe_keys_bug(self, sort):
12271229
t1 = DataFrame(
@@ -1410,10 +1412,8 @@ def test_concat_keys_and_levels(self):
14101412
keys=[("foo", "one"), ("foo", "two"), ("baz", "one"), ("baz", "two")],
14111413
names=["first", "second"],
14121414
)
1413-
assert result.index.names == ("first", "second") + (None,)
1414-
tm.assert_index_equal(
1415-
result.index.levels[0], Index(["baz", "foo"], name="first")
1416-
)
1415+
assert result.index.names == ("first", "second", None)
1416+
tm.assert_index_equal(result.index.levels[0], Index(["baz", "foo"]))
14171417

14181418
def test_concat_keys_levels_no_overlap(self):
14191419
# GH #1406

pandas/tests/reshape/test_reshape.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ def test_reshaping_multi_index_categorical(self):
617617
df.index.names = ["major", "minor"]
618618
df["str"] = "foo"
619619

620-
dti = df.index.levels[0]
620+
dti = df.index.levels[0].set_names(["major"])
621621

622622
df["category"] = df["str"].astype("category")
623623
result = df["category"].unstack()

pandas/tests/test_multilevel.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -960,13 +960,11 @@ def test_count(self):
960960

961961
result = series.count(level="b")
962962
expect = self.series.count(level=1)
963-
tm.assert_series_equal(result, expect, check_names=False)
964-
assert result.index.name == "b"
963+
tm.assert_series_equal(result, expect)
965964

966965
result = series.count(level="a")
967966
expect = self.series.count(level=0)
968-
tm.assert_series_equal(result, expect, check_names=False)
969-
assert result.index.name == "a"
967+
tm.assert_series_equal(result, expect)
970968

971969
msg = "Level x not found"
972970
with pytest.raises(KeyError, match=msg):
@@ -1020,10 +1018,10 @@ def aggf(x):
10201018
# for good measure, groupby detail
10211019
level_index = frame._get_axis(axis).levels[level]
10221020

1023-
tm.assert_index_equal(leftside._get_axis(axis), level_index)
1024-
tm.assert_index_equal(rightside._get_axis(axis), level_index)
1021+
tm.assert_index_equal(leftside._get_axis(axis), level_index, check_names=False)
1022+
tm.assert_index_equal(rightside._get_axis(axis), level_index, check_names=False)
10251023

1026-
tm.assert_frame_equal(leftside, rightside)
1024+
tm.assert_frame_equal(leftside, rightside, check_names=False)
10271025

10281026
def test_stat_op_corner(self):
10291027
obj = Series([10.0], index=MultiIndex.from_tuples([(2, 3)]))
@@ -1609,12 +1607,12 @@ def test_constructor_with_tz(self):
16091607
)
16101608

16111609
result = MultiIndex.from_arrays([index, columns])
1612-
tm.assert_index_equal(result.levels[0], index)
1613-
tm.assert_index_equal(result.levels[1], columns)
1610+
tm.assert_index_equal(result.levels[0], index, check_names=False)
1611+
tm.assert_index_equal(result.levels[1], columns, check_names=False)
16141612

16151613
result = MultiIndex.from_arrays([Series(index), Series(columns)])
1616-
tm.assert_index_equal(result.levels[0], index)
1617-
tm.assert_index_equal(result.levels[1], columns)
1614+
tm.assert_index_equal(result.levels[0], index, check_names=False)
1615+
tm.assert_index_equal(result.levels[1], columns, check_names=False)
16181616

16191617
def test_set_index_datetime(self):
16201618
# GH 3950
@@ -1642,12 +1640,14 @@ def test_set_index_datetime(self):
16421640
expected = expected.tz_localize("UTC").tz_convert("US/Pacific")
16431641

16441642
df = df.set_index("label", append=True)
1645-
tm.assert_index_equal(df.index.levels[0], expected)
1646-
tm.assert_index_equal(df.index.levels[1], Index(["a", "b"], name="label"))
1643+
tm.assert_index_equal(df.index.levels[0], expected, check_names=False)
1644+
tm.assert_index_equal(df.index.levels[1], Index(["a", "b"]))
1645+
assert df.index.names == ["datetime", "label"]
16471646

16481647
df = df.swaplevel(0, 1)
1649-
tm.assert_index_equal(df.index.levels[0], Index(["a", "b"], name="label"))
1650-
tm.assert_index_equal(df.index.levels[1], expected)
1648+
tm.assert_index_equal(df.index.levels[0], Index(["a", "b"]))
1649+
tm.assert_index_equal(df.index.levels[1], expected, check_names=False)
1650+
assert df.index.names == ["label", "datetime"]
16511651

16521652
df = DataFrame(np.random.random(6))
16531653
idx1 = pd.DatetimeIndex(

0 commit comments

Comments
 (0)