Skip to content

Commit d98e982

Browse files
bkandeljorisvandenbossche
authored andcommitted
BUG: set_levels set illegal levels. (#14236)
`MultiIndex.set_levels`, when passed in illegal level values, raises an error. When `inplace=True`, though, the illegal level values are still accepted. This commit fixes that behavior by checking that the proposed level values are legal before setting them.
1 parent 0407991 commit d98e982

File tree

4 files changed

+55
-14
lines changed

4 files changed

+55
-14
lines changed

doc/source/whatsnew/v0.19.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,6 @@ Bug Fixes
15601560
- Bug in ``.to_string()`` when called with an integer ``line_width`` and ``index=False`` raises an UnboundLocalError exception because ``idx`` referenced before assignment.
15611561
- Bug in ``eval()`` where the ``resolvers`` argument would not accept a list (:issue:`14095`)
15621562
- Bugs in ``stack``, ``get_dummies``, ``make_axis_dummies`` which don't preserve categorical dtypes in (multi)indexes (:issue:`13854`)
1563-
- ``PeridIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`)
1563+
- ``PeriodIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`)
15641564
- Bug in ``df.groupby`` where ``.median()`` returns arbitrary values if grouped dataframe contains empty bins (:issue:`13629`)
15651565
- Bug in ``Index.copy()`` where ``name`` parameter was ignored (:issue:`14302`)

doc/source/whatsnew/v0.19.1.txt

+1
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Bug Fixes
4444

4545

4646
- Bug in ``pd.concat`` where names of the ``keys`` were not propagated to the resulting ``MultiIndex`` (:issue:`14252`)
47+
- Bug in ``MultiIndex.set_levels`` where illegal level values were still set after raising an error (:issue:`13754`)

pandas/indexes/multi.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,27 @@ def __new__(cls, levels=None, labels=None, sortorder=None, names=None,
116116

117117
return result
118118

119-
def _verify_integrity(self):
120-
"""Raises ValueError if length of levels and labels don't match or any
121-
label would exceed level bounds"""
119+
def _verify_integrity(self, labels=None, levels=None):
120+
"""
121+
122+
Parameters
123+
----------
124+
labels : optional list
125+
Labels to check for validity. Defaults to current labels.
126+
levels : optional list
127+
Levels to check for validity. Defaults to current levels.
128+
129+
Raises
130+
------
131+
ValueError
132+
* if length of levels and labels don't match or any label would
133+
exceed level bounds
134+
"""
122135
# NOTE: Currently does not check, among other things, that cached
123136
# nlevels matches nor that sortorder matches actually sortorder.
124-
labels, levels = self.labels, self.levels
137+
labels = labels or self.labels
138+
levels = levels or self.levels
139+
125140
if len(levels) != len(labels):
126141
raise ValueError("Length of levels and labels must match. NOTE:"
127142
" this index is in an inconsistent state.")
@@ -162,6 +177,9 @@ def _set_levels(self, levels, level=None, copy=False, validate=True,
162177
new_levels[l] = _ensure_index(v, copy=copy)._shallow_copy()
163178
new_levels = FrozenList(new_levels)
164179

180+
if verify_integrity:
181+
self._verify_integrity(levels=new_levels)
182+
165183
names = self.names
166184
self._levels = new_levels
167185
if any(names):
@@ -170,9 +188,6 @@ def _set_levels(self, levels, level=None, copy=False, validate=True,
170188
self._tuples = None
171189
self._reset_cache()
172190

173-
if verify_integrity:
174-
self._verify_integrity()
175-
176191
def set_levels(self, levels, level=None, inplace=False,
177192
verify_integrity=True):
178193
"""
@@ -268,13 +283,13 @@ def _set_labels(self, labels, level=None, copy=False, validate=True,
268283
lab, lev, copy=copy)._shallow_copy()
269284
new_labels = FrozenList(new_labels)
270285

286+
if verify_integrity:
287+
self._verify_integrity(labels=new_labels)
288+
271289
self._labels = new_labels
272290
self._tuples = None
273291
self._reset_cache()
274292

275-
if verify_integrity:
276-
self._verify_integrity()
277-
278293
def set_labels(self, labels, level=None, inplace=False,
279294
verify_integrity=True):
280295
"""

pandas/tests/indexes/test_multi.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,14 @@ def test_set_levels(self):
149149
levels = self.index.levels
150150
new_levels = [[lev + 'a' for lev in level] for level in levels]
151151

152-
def assert_matching(actual, expected):
152+
def assert_matching(actual, expected, check_dtype=False):
153153
# avoid specifying internal representation
154154
# as much as possible
155155
self.assertEqual(len(actual), len(expected))
156156
for act, exp in zip(actual, expected):
157157
act = np.asarray(act)
158-
exp = np.asarray(exp, dtype=np.object_)
159-
tm.assert_numpy_array_equal(act, exp)
158+
exp = np.asarray(exp)
159+
tm.assert_numpy_array_equal(act, exp, check_dtype=check_dtype)
160160

161161
# level changing [w/o mutation]
162162
ind2 = self.index.set_levels(new_levels)
@@ -204,6 +204,31 @@ def assert_matching(actual, expected):
204204
assert_matching(ind2.levels, new_levels)
205205
assert_matching(self.index.levels, levels)
206206

207+
# illegal level changing should not change levels
208+
# GH 13754
209+
original_index = self.index.copy()
210+
for inplace in [True, False]:
211+
with assertRaisesRegexp(ValueError, "^On"):
212+
self.index.set_levels(['c'], level=0, inplace=inplace)
213+
assert_matching(self.index.levels, original_index.levels,
214+
check_dtype=True)
215+
216+
with assertRaisesRegexp(ValueError, "^On"):
217+
self.index.set_labels([0, 1, 2, 3, 4, 5], level=0,
218+
inplace=inplace)
219+
assert_matching(self.index.labels, original_index.labels,
220+
check_dtype=True)
221+
222+
with assertRaisesRegexp(TypeError, "^Levels"):
223+
self.index.set_levels('c', level=0, inplace=inplace)
224+
assert_matching(self.index.levels, original_index.levels,
225+
check_dtype=True)
226+
227+
with assertRaisesRegexp(TypeError, "^Labels"):
228+
self.index.set_labels(1, level=0, inplace=inplace)
229+
assert_matching(self.index.labels, original_index.labels,
230+
check_dtype=True)
231+
207232
def test_set_labels(self):
208233
# side note - you probably wouldn't want to use levels and labels
209234
# directly like this - but it is possible.

0 commit comments

Comments
 (0)