Skip to content

Commit eb320fb

Browse files
author
cloudboat
committed
Add API reference documentation & Implement insert_level using levels/codes operations
1 parent e2334ac commit eb320fb

File tree

4 files changed

+161
-164
lines changed

4 files changed

+161
-164
lines changed

doc/source/reference/indexing.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ MultiIndex components
294294
MultiIndex.copy
295295
MultiIndex.append
296296
MultiIndex.truncate
297+
MultiIndex.insert_level
297298

298299
MultiIndex selecting
299300
~~~~~~~~~~~~~~~~~~~~

pandas/core/indexes/multi.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2710,19 +2710,21 @@ def reorder_levels(self, order) -> MultiIndex:
27102710
result = self._reorder_ilevels(order)
27112711
return result
27122712

2713-
def insert_level(self, position: int, value, name=None):
2713+
def insert_level(
2714+
self, position: int, value, name: Hashable = lib.no_default
2715+
) -> MultiIndex:
27142716
"""
27152717
Insert a new level at the specified position in the MultiIndex.
27162718
27172719
Parameters
27182720
----------
27192721
position : int
27202722
The position at which to insert the new level (0-based).
2721-
value : scalar or array-like
2722-
Value(s) to use for the new level. If scalar, broadcast to all items.
2723-
If array-like, length must match the length of the index.
2724-
name : object, optional
2725-
Name for the new level.
2723+
Must be between 0 and nlevels (inclusive).
2724+
value : array-like
2725+
Values to use for the new level. Length must match the length of the index.
2726+
name : Hashable, default lib.no_default
2727+
Name for the new level. If not provided, the new level will have no name.
27262728
27272729
Returns
27282730
-------
@@ -2732,37 +2734,40 @@ def insert_level(self, position: int, value, name=None):
27322734
Examples
27332735
--------
27342736
>>> idx = pd.MultiIndex.from_tuples([("A", 1), ("B", 2)])
2735-
>>> idx.insert_level(0, "new_value")
2737+
>>> idx.insert_level(0, ["new_value", "new_value"])
27362738
MultiIndex([('new_value', 'A', 1), ('new_value', 'B', 2)], ...)
27372739
27382740
>>> idx.insert_level(1, ["X", "Y"])
27392741
MultiIndex([('A', 'X', 1), ('B', 'Y', 2)], ...)
2740-
2741-
>>> idx.insert_level(0, "new_val", name="new_level")
2742-
MultiIndex([('new_val', 'A', 1), ('new_val', 'B', 2)], ...)
27432742
"""
27442743
if not isinstance(position, int):
27452744
raise TypeError("position must be an integer")
27462745

27472746
if position < 0 or position > self.nlevels:
27482747
raise ValueError(f"position must be between 0 and {self.nlevels}")
27492748

2749+
if name is lib.no_default:
2750+
name = None
2751+
27502752
if not hasattr(value, "__iter__") or isinstance(value, str):
2751-
value = [value] * len(self)
2752-
else:
2753-
value = list(value)
2754-
if len(value) != len(self):
2755-
raise ValueError("Length of values must match length of index")
2753+
raise TypeError("value must be an array-like object")
27562754

2757-
new_tuples = []
2755+
value = list(value)
2756+
if len(value) != len(self):
2757+
raise ValueError("Length of values must match length of index")
27582758

2759-
for i, tup in enumerate(self):
2760-
new_tuple = tup[:position] + (value[i],) + tup[position:]
2761-
new_tuples.append(new_tuple)
2759+
new_level = Index(value)
2760+
new_codes_for_level = new_level.get_indexer(value)
27622761

2762+
new_levels = self.levels[:position] + [new_level] + self.levels[position:]
2763+
new_codes = (
2764+
self.codes[:position] + [new_codes_for_level] + self.codes[position:]
2765+
)
27632766
new_names = self.names[:position] + [name] + self.names[position:]
27642767

2765-
return MultiIndex.from_tuples(new_tuples, names=new_names)
2768+
return MultiIndex(
2769+
levels=new_levels, codes=new_codes, names=new_names, verify_integrity=False
2770+
)
27662771

27672772
def _reorder_ilevels(self, order) -> MultiIndex:
27682773
if len(order) != self.nlevels:

pandas/tests/indexes/multi/test_constructors.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -870,14 +870,3 @@ def test_dtype_representation(using_infer_string):
870870
dtype=object,
871871
)
872872
tm.assert_series_equal(result, expected)
873-
874-
875-
def test_insert_level_integration():
876-
idx = MultiIndex.from_tuples([("A", 1), ("B", 2)])
877-
878-
df = pd.DataFrame({"data": [10, 20]}, index=idx)
879-
new_idx = idx.insert_level(0, "group1")
880-
df_new = df.set_index(new_idx)
881-
882-
assert df_new.index.nlevels == 3
883-
assert len(df_new) == 2

pandas/tests/indexes/multi/test_insert_level.py

Lines changed: 135 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -4,146 +4,148 @@
44
import pandas._testing as tm
55

66

7-
class TestMultiIndexInsertLevel:
8-
@pytest.mark.parametrize(
9-
"position, value, name, expected_tuples, expected_names",
10-
[
11-
(
12-
0,
13-
"new_value",
14-
None,
15-
[("new_value", "A", 1), ("new_value", "B", 2), ("new_value", "C", 3)],
16-
[None, "level1", "level2"],
17-
),
18-
(
19-
1,
20-
"middle",
21-
None,
22-
[("A", "middle", 1), ("B", "middle", 2), ("C", "middle", 3)],
23-
["level1", None, "level2"],
24-
),
25-
(
26-
0,
27-
"new_val",
28-
"new_level",
29-
[("new_val", "A", 1), ("new_val", "B", 2), ("new_val", "C", 3)],
30-
["new_level", "level1", "level2"],
31-
),
32-
(
33-
1,
34-
"middle",
35-
"custom_name",
36-
[("A", "middle", 1), ("B", "middle", 2), ("C", "middle", 3)],
37-
["level1", "custom_name", "level2"],
38-
),
39-
(
40-
0,
41-
"start",
42-
None,
43-
[("start", "A", 1), ("start", "B", 2), ("start", "C", 3)],
44-
[None, "level1", "level2"],
45-
),
46-
(
47-
2,
48-
"end",
49-
None,
50-
[("A", 1, "end"), ("B", 2, "end"), ("C", 3, "end")],
51-
["level1", "level2", None],
52-
),
53-
(
54-
1,
55-
100,
56-
None,
57-
[("A", 100, 1), ("B", 100, 2), ("C", 100, 3)],
58-
["level1", None, "level2"],
59-
),
60-
(
61-
1,
62-
1.5,
63-
None,
64-
[("A", 1.5, 1), ("B", 1.5, 2), ("C", 1.5, 3)],
65-
["level1", None, "level2"],
66-
),
67-
(
68-
1,
69-
None,
70-
None,
71-
[("A", None, 1), ("B", None, 2), ("C", None, 3)],
72-
["level1", None, "level2"],
73-
),
74-
(
75-
1,
76-
["X", "Y", "Z"],
77-
None,
78-
[("A", "X", 1), ("B", "Y", 2), ("C", "Z", 3)],
79-
["level1", None, "level2"],
80-
),
81-
(
82-
0,
83-
"",
84-
"empty_string",
85-
[("", "A", 1), ("", "B", 2), ("", "C", 3)],
86-
["empty_string", "level1", "level2"],
87-
),
88-
(
89-
1,
90-
True,
91-
None,
92-
[("A", True, 1), ("B", True, 2), ("C", True, 3)],
93-
["level1", None, "level2"],
94-
),
95-
],
7+
@pytest.mark.parametrize(
8+
"position, value, name, expected_tuples, expected_names",
9+
[
10+
(
11+
0,
12+
["new_value"] * 3,
13+
None,
14+
[("new_value", "A", 1), ("new_value", "B", 2), ("new_value", "C", 3)],
15+
[None, "level1", "level2"],
16+
),
17+
(
18+
1,
19+
["middle"] * 3,
20+
None,
21+
[("A", "middle", 1), ("B", "middle", 2), ("C", "middle", 3)],
22+
["level1", None, "level2"],
23+
),
24+
(
25+
0,
26+
["new_val"] * 3,
27+
"new_level",
28+
[("new_val", "A", 1), ("new_val", "B", 2), ("new_val", "C", 3)],
29+
["new_level", "level1", "level2"],
30+
),
31+
(
32+
1,
33+
["middle"] * 3,
34+
"custom_name",
35+
[("A", "middle", 1), ("B", "middle", 2), ("C", "middle", 3)],
36+
["level1", "custom_name", "level2"],
37+
),
38+
(
39+
0,
40+
["start"] * 3,
41+
None,
42+
[("start", "A", 1), ("start", "B", 2), ("start", "C", 3)],
43+
[None, "level1", "level2"],
44+
),
45+
(
46+
2,
47+
["end"] * 3,
48+
None,
49+
[("A", 1, "end"), ("B", 2, "end"), ("C", 3, "end")],
50+
["level1", "level2", None],
51+
),
52+
(
53+
1,
54+
[100, 100, 100],
55+
None,
56+
[("A", 100, 1), ("B", 100, 2), ("C", 100, 3)],
57+
["level1", None, "level2"],
58+
),
59+
(
60+
1,
61+
[1.5, 1.5, 1.5],
62+
None,
63+
[("A", 1.5, 1), ("B", 1.5, 2), ("C", 1.5, 3)],
64+
["level1", None, "level2"],
65+
),
66+
(
67+
1,
68+
[None, None, None],
69+
None,
70+
[("A", None, 1), ("B", None, 2), ("C", None, 3)],
71+
["level1", None, "level2"],
72+
),
73+
(
74+
1,
75+
["X", "Y", "Z"],
76+
None,
77+
[("A", "X", 1), ("B", "Y", 2), ("C", "Z", 3)],
78+
["level1", None, "level2"],
79+
),
80+
(
81+
0,
82+
[""] * 3,
83+
"empty_string",
84+
[("", "A", 1), ("", "B", 2), ("", "C", 3)],
85+
["empty_string", "level1", "level2"],
86+
),
87+
(
88+
1,
89+
[True, True, True],
90+
None,
91+
[("A", True, 1), ("B", True, 2), ("C", True, 3)],
92+
["level1", None, "level2"],
93+
),
94+
],
95+
)
96+
def test_insert_level_basic(position, value, name, expected_tuples, expected_names):
97+
simple_idx = pd.MultiIndex.from_tuples(
98+
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
9699
)
97-
def test_insert_level_basic(
98-
self, position, value, name, expected_tuples, expected_names
99-
):
100-
simple_idx = pd.MultiIndex.from_tuples(
101-
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
102-
)
103-
104-
result = simple_idx.insert_level(position, value, name=name)
105-
expected = pd.MultiIndex.from_tuples(expected_tuples, names=expected_names)
106-
tm.assert_index_equal(result, expected)
107-
108-
@pytest.mark.parametrize(
109-
"position, value, expected_error",
110-
[
111-
(5, "invalid", "position must be between"),
112-
(-1, "invalid", "position must be between"),
113-
(1, ["too", "few"], "Length of values must match"),
114-
(3, "value", "position must be between"),
115-
],
100+
101+
result = simple_idx.insert_level(position, value, name=name)
102+
expected = pd.MultiIndex.from_tuples(expected_tuples, names=expected_names)
103+
tm.assert_index_equal(result, expected)
104+
105+
106+
@pytest.mark.parametrize(
107+
"position, value, expected_error",
108+
[
109+
(5, ["invalid"] * 3, "position must be between"),
110+
(-1, ["invalid"] * 3, "position must be between"),
111+
(1, ["too", "few"], "Length of values must match"),
112+
(3, ["value"] * 3, "position must be between"),
113+
(0, "scalar_value", "value must be an array-like object"),
114+
],
115+
)
116+
def test_insert_level_error_cases(position, value, expected_error):
117+
simple_idx = pd.MultiIndex.from_tuples(
118+
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
119+
)
120+
121+
with pytest.raises(ValueError, match=expected_error):
122+
simple_idx.insert_level(position, value)
123+
124+
125+
def test_insert_level_preserves_original():
126+
simple_idx = pd.MultiIndex.from_tuples(
127+
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
116128
)
117-
def test_insert_level_error_cases(self, position, value, expected_error):
118-
simple_idx = pd.MultiIndex.from_tuples(
119-
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
120-
)
121129

122-
with pytest.raises(ValueError, match=expected_error):
123-
simple_idx.insert_level(position, value)
130+
original = simple_idx.copy()
131+
simple_idx.insert_level(1, ["temp"] * 3)
124132

125-
def test_insert_level_preserves_original(self):
126-
simple_idx = pd.MultiIndex.from_tuples(
127-
[("A", 1), ("B", 2), ("C", 3)], names=["level1", "level2"]
128-
)
133+
tm.assert_index_equal(original, simple_idx)
129134

130-
original = simple_idx.copy()
131-
simple_idx.insert_level(1, "temp")
132135

133-
tm.assert_index_equal(original, simple_idx)
136+
def test_insert_level_empty_index():
137+
empty_idx = pd.MultiIndex.from_tuples([], names=["level1", "level2"])
134138

135-
def test_insert_level_empty_index(self):
136-
empty_idx = pd.MultiIndex.from_tuples([], names=["level1", "level2"])
139+
result = empty_idx.insert_level(0, [])
140+
expected = pd.MultiIndex.from_tuples([], names=[None, "level1", "level2"])
141+
tm.assert_index_equal(result, expected)
137142

138-
result = empty_idx.insert_level(0, [])
139-
expected = pd.MultiIndex.from_tuples([], names=[None, "level1", "level2"])
140-
tm.assert_index_equal(result, expected)
141143

142-
def test_insert_level_single_element(self):
143-
single_idx = pd.MultiIndex.from_tuples([("A", 1)], names=["level1", "level2"])
144+
def test_insert_level_single_element():
145+
single_idx = pd.MultiIndex.from_tuples([("A", 1)], names=["level1", "level2"])
144146

145-
result = single_idx.insert_level(1, "middle")
146-
expected = pd.MultiIndex.from_tuples(
147-
[("A", "middle", 1)], names=["level1", None, "level2"]
148-
)
149-
tm.assert_index_equal(result, expected)
147+
result = single_idx.insert_level(1, ["middle"])
148+
expected = pd.MultiIndex.from_tuples(
149+
[("A", "middle", 1)], names=["level1", None, "level2"]
150+
)
151+
tm.assert_index_equal(result, expected)

0 commit comments

Comments
 (0)