Skip to content

Commit 9da121e

Browse files
mtrbeanmtrbean
mtrbean
authored andcommitted
ENH: Add optional level argument to set_names(), set_levels() and set_labels() (GH7792)
1 parent 57cca51 commit 9da121e

File tree

4 files changed

+326
-45
lines changed

4 files changed

+326
-45
lines changed

doc/source/indexing.rst

+11
Original file line numberDiff line numberDiff line change
@@ -2162,6 +2162,17 @@ you can specify ``inplace=True`` to have the data change in place.
21622162
ind.name = "bob"
21632163
ind
21642164
2165+
.. versionadded:: 0.15.0
2166+
2167+
``set_names``, ``set_levels``, and ``set_labels`` also take an optional
2168+
`level`` argument
2169+
2170+
.. ipython:: python
2171+
2172+
index
2173+
index.levels[1]
2174+
index.set_levels(["a", "b"], level=1)
2175+
21652176
Adding an index to an existing DataFrame
21662177
----------------------------------------
21672178

doc/source/v0.15.0.txt

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ API changes
3535
levels aren't all level names or all level numbers. See
3636
:ref:`Reshaping by stacking and unstacking <reshaping.stack_multiple>`.
3737

38+
- :func:`set_names`, :func:`set_labels`, and :func:`set_levels` methods now take an optional ``level`` keyword argument to all modification of specific level(s) of a MultiIndex. Additionally :func:`set_names` now accepts a scalar string value when operating on an ``Index`` or on a specific level of a ``MultiIndex`` (:issue:`7792`)
39+
40+
.. ipython:: python
41+
42+
idx = pandas.MultiIndex.from_product([['a'], range(3), list("pqr")], names=['foo', 'bar', 'baz'])
43+
idx.set_names('qux', level=0)
44+
idx.set_names(['qux','baz'], level=[0,1])
45+
idx.set_levels(['a','b','c'], level='bar')
46+
idx.set_levels([['a','b','c'],[1,2,3]], level=[1,2])
47+
3848
- Raise a ``ValueError`` in ``df.to_hdf`` with 'fixed' format, if ``df`` has non-unique columns as the resulting file will be broken (:issue:`7761`)
3949

4050
- :func:`rolling_min`, :func:`rolling_max`, :func:`rolling_cov`, and :func:`rolling_corr`

pandas/core/index.py

+173-37
Original file line numberDiff line numberDiff line change
@@ -362,36 +362,69 @@ def nlevels(self):
362362
def _get_names(self):
363363
return FrozenList((self.name,))
364364

365-
def _set_names(self, values):
365+
def _set_names(self, values, level=None):
366366
if len(values) != 1:
367367
raise ValueError('Length of new names must be 1, got %d'
368368
% len(values))
369369
self.name = values[0]
370370

371371
names = property(fset=_set_names, fget=_get_names)
372372

373-
def set_names(self, names, inplace=False):
373+
def set_names(self, names, level=None, inplace=False):
374374
"""
375375
Set new names on index. Defaults to returning new index.
376376
377377
Parameters
378378
----------
379-
names : sequence
380-
names to set
379+
names : str or sequence
380+
name(s) to set
381+
level : int or level name, or sequence of int / level names (default None)
382+
If the index is a MultiIndex (hierarchical), level(s) to set (None for all levels)
383+
Otherwise level must be None
381384
inplace : bool
382385
if True, mutates in place
383386
384387
Returns
385388
-------
386389
new index (of same type and class...etc) [if inplace, returns None]
390+
391+
Examples
392+
--------
393+
>>> Index([1, 2, 3, 4]).set_names('foo')
394+
Int64Index([1, 2, 3, 4], dtype='int64')
395+
>>> Index([1, 2, 3, 4]).set_names(['foo'])
396+
Int64Index([1, 2, 3, 4], dtype='int64')
397+
>>> idx = MultiIndex.from_tuples([(1, u'one'), (1, u'two'),
398+
(2, u'one'), (2, u'two')],
399+
names=['foo', 'bar'])
400+
>>> idx.set_names(['baz', 'quz'])
401+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
402+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
403+
names=[u'baz', u'quz'])
404+
>>> idx.set_names('baz', level=0)
405+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
406+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
407+
names=[u'baz', u'bar'])
387408
"""
388-
if not com.is_list_like(names):
409+
if level is not None and self.nlevels == 1:
410+
raise ValueError('Level must be None for non-MultiIndex')
411+
412+
if level is not None and not com.is_list_like(level) and com.is_list_like(names):
413+
raise TypeError("Names must be a string")
414+
415+
if not com.is_list_like(names) and level is None and self.nlevels > 1:
389416
raise TypeError("Must pass list-like as `names`.")
417+
418+
if not com.is_list_like(names):
419+
names = [names]
420+
if level is not None and not com.is_list_like(level):
421+
level = [level]
422+
390423
if inplace:
391424
idx = self
392425
else:
393426
idx = self._shallow_copy()
394-
idx._set_names(names)
427+
idx._set_names(names, level=level)
395428
if not inplace:
396429
return idx
397430

@@ -2218,19 +2251,30 @@ def _verify_integrity(self):
22182251
def _get_levels(self):
22192252
return self._levels
22202253

2221-
def _set_levels(self, levels, copy=False, validate=True,
2254+
def _set_levels(self, levels, level=None, copy=False, validate=True,
22222255
verify_integrity=False):
22232256
# This is NOT part of the levels property because it should be
22242257
# externally not allowed to set levels. User beware if you change
22252258
# _levels directly
22262259
if validate and len(levels) == 0:
22272260
raise ValueError('Must set non-zero number of levels.')
2228-
if validate and len(levels) != len(self._labels):
2229-
raise ValueError('Length of levels must match length of labels.')
2230-
levels = FrozenList(_ensure_index(lev, copy=copy)._shallow_copy()
2231-
for lev in levels)
2261+
if validate and level is None and len(levels) != self.nlevels:
2262+
raise ValueError('Length of levels must match number of levels.')
2263+
if validate and level is not None and len(levels) != len(level):
2264+
raise ValueError('Length of levels must match length of level.')
2265+
2266+
if level is None:
2267+
new_levels = FrozenList(_ensure_index(lev, copy=copy)._shallow_copy()
2268+
for lev in levels)
2269+
else:
2270+
level = [self._get_level_number(l) for l in level]
2271+
new_levels = list(self._levels)
2272+
for l, v in zip(level, levels):
2273+
new_levels[l] = _ensure_index(v, copy=copy)._shallow_copy()
2274+
new_levels = FrozenList(new_levels)
2275+
22322276
names = self.names
2233-
self._levels = levels
2277+
self._levels = new_levels
22342278
if any(names):
22352279
self._set_names(names)
22362280

@@ -2240,15 +2284,17 @@ def _set_levels(self, levels, copy=False, validate=True,
22402284
if verify_integrity:
22412285
self._verify_integrity()
22422286

2243-
def set_levels(self, levels, inplace=False, verify_integrity=True):
2287+
def set_levels(self, levels, level=None, inplace=False, verify_integrity=True):
22442288
"""
22452289
Set new levels on MultiIndex. Defaults to returning
22462290
new index.
22472291
22482292
Parameters
22492293
----------
2250-
levels : sequence
2251-
new levels to apply
2294+
levels : sequence or list of sequence
2295+
new level(s) to apply
2296+
level : int or level name, or sequence of int / level names (default None)
2297+
level(s) to set (None for all levels)
22522298
inplace : bool
22532299
if True, mutates in place
22542300
verify_integrity : bool (default True)
@@ -2257,15 +2303,47 @@ def set_levels(self, levels, inplace=False, verify_integrity=True):
22572303
Returns
22582304
-------
22592305
new index (of same type and class...etc)
2260-
"""
2261-
if not com.is_list_like(levels) or not com.is_list_like(levels[0]):
2262-
raise TypeError("Levels must be list of lists-like")
2306+
2307+
2308+
Examples
2309+
--------
2310+
>>> idx = MultiIndex.from_tuples([(1, u'one'), (1, u'two'),
2311+
(2, u'one'), (2, u'two')],
2312+
names=['foo', 'bar'])
2313+
>>> idx.set_levels([['a','b'], [1,2]])
2314+
MultiIndex(levels=[[u'a', u'b'], [1, 2]],
2315+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
2316+
names=[u'foo', u'bar'])
2317+
>>> idx.set_levels(['a','b'], level=0)
2318+
MultiIndex(levels=[[u'a', u'b'], [u'one', u'two']],
2319+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
2320+
names=[u'foo', u'bar'])
2321+
>>> idx.set_levels(['a','b'], level='bar')
2322+
MultiIndex(levels=[[1, 2], [u'a', u'b']],
2323+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
2324+
names=[u'foo', u'bar'])
2325+
>>> idx.set_levels([['a','b'], [1,2]], level=[0,1])
2326+
MultiIndex(levels=[[u'a', u'b'], [1, 2]],
2327+
labels=[[0, 0, 1, 1], [0, 1, 0, 1]],
2328+
names=[u'foo', u'bar'])
2329+
"""
2330+
if level is not None and not com.is_list_like(level):
2331+
if not com.is_list_like(levels):
2332+
raise TypeError("Levels must be list-like")
2333+
if com.is_list_like(levels[0]):
2334+
raise TypeError("Levels must be list-like")
2335+
level = [level]
2336+
levels = [levels]
2337+
elif level is None or com.is_list_like(level):
2338+
if not com.is_list_like(levels) or not com.is_list_like(levels[0]):
2339+
raise TypeError("Levels must be list of lists-like")
2340+
22632341
if inplace:
22642342
idx = self
22652343
else:
22662344
idx = self._shallow_copy()
22672345
idx._reset_identity()
2268-
idx._set_levels(levels, validate=True,
2346+
idx._set_levels(levels, level=level, validate=True,
22692347
verify_integrity=verify_integrity)
22702348
if not inplace:
22712349
return idx
@@ -2280,27 +2358,42 @@ def set_levels(self, levels, inplace=False, verify_integrity=True):
22802358
def _get_labels(self):
22812359
return self._labels
22822360

2283-
def _set_labels(self, labels, copy=False, validate=True,
2361+
def _set_labels(self, labels, level=None, copy=False, validate=True,
22842362
verify_integrity=False):
2285-
if validate and len(labels) != self.nlevels:
2286-
raise ValueError("Length of labels must match length of levels")
2287-
self._labels = FrozenList(
2288-
_ensure_frozen(labs, copy=copy)._shallow_copy() for labs in labels)
2363+
2364+
if validate and level is None and len(labels) != self.nlevels:
2365+
raise ValueError("Length of labels must match number of levels")
2366+
if validate and level is not None and len(labels) != len(level):
2367+
raise ValueError('Length of labels must match length of levels.')
2368+
2369+
if level is None:
2370+
new_labels = FrozenList(_ensure_frozen(v, copy=copy)._shallow_copy()
2371+
for v in labels)
2372+
else:
2373+
level = [self._get_level_number(l) for l in level]
2374+
new_labels = list(self._labels)
2375+
for l, v in zip(level, labels):
2376+
new_labels[l] = _ensure_frozen(v, copy=copy)._shallow_copy()
2377+
new_labels = FrozenList(new_labels)
2378+
2379+
self._labels = new_labels
22892380
self._tuples = None
22902381
self._reset_cache()
22912382

22922383
if verify_integrity:
22932384
self._verify_integrity()
22942385

2295-
def set_labels(self, labels, inplace=False, verify_integrity=True):
2386+
def set_labels(self, labels, level=None, inplace=False, verify_integrity=True):
22962387
"""
22972388
Set new labels on MultiIndex. Defaults to returning
22982389
new index.
22992390
23002391
Parameters
23012392
----------
2302-
labels : sequence of arrays
2393+
labels : sequence or list of sequence
23032394
new labels to apply
2395+
level : int or level name, or sequence of int / level names (default None)
2396+
level(s) to set (None for all levels)
23042397
inplace : bool
23052398
if True, mutates in place
23062399
verify_integrity : bool (default True)
@@ -2309,15 +2402,46 @@ def set_labels(self, labels, inplace=False, verify_integrity=True):
23092402
Returns
23102403
-------
23112404
new index (of same type and class...etc)
2312-
"""
2313-
if not com.is_list_like(labels) or not com.is_list_like(labels[0]):
2314-
raise TypeError("Labels must be list of lists-like")
2405+
2406+
Examples
2407+
--------
2408+
>>> idx = MultiIndex.from_tuples([(1, u'one'), (1, u'two'),
2409+
(2, u'one'), (2, u'two')],
2410+
names=['foo', 'bar'])
2411+
>>> idx.set_labels([[1,0,1,0], [0,0,1,1]])
2412+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
2413+
labels=[[1, 0, 1, 0], [0, 0, 1, 1]],
2414+
names=[u'foo', u'bar'])
2415+
>>> idx.set_labels([1,0,1,0], level=0)
2416+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
2417+
labels=[[1, 0, 1, 0], [0, 1, 0, 1]],
2418+
names=[u'foo', u'bar'])
2419+
>>> idx.set_labels([0,0,1,1], level='bar')
2420+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
2421+
labels=[[0, 0, 1, 1], [0, 0, 1, 1]],
2422+
names=[u'foo', u'bar'])
2423+
>>> idx.set_labels([[1,0,1,0], [0,0,1,1]], level=[0,1])
2424+
MultiIndex(levels=[[1, 2], [u'one', u'two']],
2425+
labels=[[1, 0, 1, 0], [0, 0, 1, 1]],
2426+
names=[u'foo', u'bar'])
2427+
"""
2428+
if level is not None and not com.is_list_like(level):
2429+
if not com.is_list_like(labels):
2430+
raise TypeError("Labels must be list-like")
2431+
if com.is_list_like(labels[0]):
2432+
raise TypeError("Labels must be list-like")
2433+
level = [level]
2434+
labels = [labels]
2435+
elif level is None or com.is_list_like(level):
2436+
if not com.is_list_like(labels) or not com.is_list_like(labels[0]):
2437+
raise TypeError("Labels must be list of lists-like")
2438+
23152439
if inplace:
23162440
idx = self
23172441
else:
23182442
idx = self._shallow_copy()
23192443
idx._reset_identity()
2320-
idx._set_labels(labels, verify_integrity=verify_integrity)
2444+
idx._set_labels(labels, level=level, verify_integrity=verify_integrity)
23212445
if not inplace:
23222446
return idx
23232447

@@ -2434,18 +2558,30 @@ def __len__(self):
24342558
def _get_names(self):
24352559
return FrozenList(level.name for level in self.levels)
24362560

2437-
def _set_names(self, values, validate=True):
2561+
def _set_names(self, names, level=None, validate=True):
24382562
"""
24392563
sets names on levels. WARNING: mutates!
24402564
24412565
Note that you generally want to set this *after* changing levels, so
2442-
that it only acts on copies"""
2443-
values = list(values)
2444-
if validate and len(values) != self.nlevels:
2445-
raise ValueError('Length of names must match length of levels')
2566+
that it only acts on copies
2567+
"""
2568+
2569+
names = list(names)
2570+
2571+
if validate and level is not None and len(names) != len(level):
2572+
raise ValueError('Length of names must match length of level.')
2573+
if validate and level is None and len(names) != self.nlevels:
2574+
raise ValueError(
2575+
'Length of names must match number of levels in MultiIndex.')
2576+
2577+
if level is None:
2578+
level = range(self.nlevels)
2579+
else:
2580+
level = [self._get_level_number(l) for l in level]
2581+
24462582
# set the name
2447-
for name, level in zip(values, self.levels):
2448-
level.rename(name, inplace=True)
2583+
for l, name in zip(level, names):
2584+
self.levels[l].rename(name, inplace=True)
24492585

24502586
names = property(
24512587
fset=_set_names, fget=_get_names, doc="Names of levels in MultiIndex")

0 commit comments

Comments
 (0)