From a0cdec1d934d0577413533f705c04545e1cf2b79 Mon Sep 17 00:00:00 2001 From: Jeffrey Tratner Date: Sun, 13 Oct 2013 23:39:48 -0400 Subject: [PATCH] BUG/CLN: Clear _tuples on setting MI levels/labels --- doc/source/release.rst | 3 +++ pandas/core/index.py | 4 +++- pandas/tests/test_index.py | 35 +++++++++++++++++++++++++++++++++++ pandas/util/testing.py | 5 +++-- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/doc/source/release.rst b/doc/source/release.rst index fe1af472700ac..b74b1f9252709 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -597,6 +597,9 @@ Bug Fixes - Bug in ``to_datetime`` with a format and ``coerce=True`` not raising (:issue:`5195`) - Bug in ``loc`` setting with multiple indexers and a rhs of a Series that needs broadcasting (:issue:`5206`) + - Fixed bug where inplace setting of levels or labels on ``MultiIndex`` would + not clear cached ``values`` property and therefore return wrong ``values``. + (:issue:`5215`) pandas 0.12.0 ------------- diff --git a/pandas/core/index.py b/pandas/core/index.py index 446c57bcf20bf..773ca4acf80df 100644 --- a/pandas/core/index.py +++ b/pandas/core/index.py @@ -1903,8 +1903,9 @@ def _set_levels(self, levels, copy=False, validate=True): for lev in levels) names = self.names self._levels = levels - if len(names): + if any(names): self._set_names(names) + self._tuples = None def set_levels(self, levels, inplace=False): """ @@ -1947,6 +1948,7 @@ def _set_labels(self, labels, copy=False, validate=True): raise ValueError("Length of labels must match length of levels") self._labels = FrozenList(_ensure_frozen(labs, copy=copy)._shallow_copy() for labs in labels) + self._tuples = None def set_labels(self, labels, inplace=False): """ diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py index c7c9fd1de0fcd..5b2edc31e1fe9 100644 --- a/pandas/tests/test_index.py +++ b/pandas/tests/test_index.py @@ -1319,6 +1319,41 @@ def test_metadata_immutable(self): with assertRaisesRegexp(TypeError, mutable_regex): names[0] = names[0] + def test_inplace_mutation_resets_values(self): + levels = [['a', 'b', 'c'], [4]] + levels2 = [[1, 2, 3], ['a']] + labels = [[0, 1, 0, 2, 2, 0], [0, 0, 0, 0, 0, 0]] + mi1 = MultiIndex(levels=levels, labels=labels) + mi2 = MultiIndex(levels=levels2, labels=labels) + vals = mi1.values.copy() + vals2 = mi2.values.copy() + self.assert_(mi1._tuples is not None) + + # make sure level setting works + new_vals = mi1.set_levels(levels2).values + assert_almost_equal(vals2, new_vals) + # non-inplace doesn't kill _tuples [implementation detail] + assert_almost_equal(mi1._tuples, vals) + # and values is still same too + assert_almost_equal(mi1.values, vals) + + # inplace should kill _tuples + mi1.set_levels(levels2, inplace=True) + assert_almost_equal(mi1.values, vals2) + + # make sure label setting works too + labels2 = [[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]] + exp_values = np.array([(1, 'a')] * 6, dtype=object) + new_values = mi2.set_labels(labels2).values + # not inplace shouldn't change + assert_almost_equal(mi2._tuples, vals2) + # should have correct values + assert_almost_equal(exp_values, new_values) + + # and again setting inplace should kill _tuples, etc + mi2.set_labels(labels2, inplace=True) + assert_almost_equal(mi2.values, new_values) + def test_copy_in_constructor(self): levels = np.array(["a", "b", "c"]) labels = np.array([1, 1, 2, 0, 0, 1, 1]) diff --git a/pandas/util/testing.py b/pandas/util/testing.py index dd59524e90f10..7a37be30f7bf6 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -382,14 +382,15 @@ def assert_almost_equal(a, b, check_less_precise=False): return assert_dict_equal(a, b) if isinstance(a, compat.string_types): - assert a == b, "%s != %s" % (a, b) + assert a == b, "%r != %r" % (a, b) return True if isiterable(a): np.testing.assert_(isiterable(b)) na, nb = len(a), len(b) assert na == nb, "%s != %s" % (na, nb) - if np.array_equal(a, b): + if isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and\ + np.array_equal(a, b): return True else: for i in range(na):