Repository: pandas-dev/pandas

Issue:
Groupby all levels except the specified ones
Inspired by 

> http://stackoverflow.com/questions/25606478/grouping-by-everything-except-for-one-index-column-in-pandas

 I was thinking if it would be useful to add a parameter to the groupby function (e.g. allbut=True) so that pandas will group all levels except the specified ones) .

I suggest so because in a multilevel index with several levels, eliminating just one level requires a quite verbose approach 


Discussion:
Actually this would be quite trivial to implement. We currently support a union (set ops) for ``FrozenList``, e.g. ``df.index.names + ['foo']``, so supporting difference is easy.

This might break other things, so need some tests. Also would be nice to support intersection, ``&`` as well.

```
In [3]: df.groupby(level=df.index.names-['name']).mean()
Out[3]: 
                 accuracy  velocity
condition trial                    
a         1      0.661597  0.277299
          2      0.953187 -0.875767
b         1     -0.836886  0.944629
          2     -0.251443  0.799134

In [4]: quit()
(pandas) bash-3.2$ git diff
diff --git a/pandas/core/base.py b/pandas/core/base.py
index 92ec6bb..b8a4c1d 100644
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -711,6 +711,12 @@ class FrozenList(PandasObject, list):
 
     __iadd__ = __add__
 
+    def __sub__(self, other):
+        if isinstance(other, tuple):
+            other = list(other)
+        return self.__class__([ e for e in self if e not in set(other)])
+
+
     # Python 2 compat
     def __getslice__(self, i, j):
         return self.__class__(super(FrozenList, self).__getslice__(i, j))
```
Great idea. Operations on Frozenlists would be really helpful indeed. 
Thanks for the quick reply
going to merge this soon: https://github.com/pandas-dev/pandas/pull/15477

should provide a bit more runway for expanding / testing these structures.
reverted in https://github.com/pandas-dev/pandas/pull/15566

Resolving patch:
diff --git a/pandas/core/indexes/frozen.py b/pandas/core/indexes/frozen.py
--- a/pandas/core/indexes/frozen.py
+++ b/pandas/core/indexes/frozen.py
@@ -23,15 +23,47 @@ class FrozenList(PandasObject, list):
     because it's technically non-hashable, will be used
     for lookups, appropriately, etc.
     """
-    # Sidenote: This has to be of type list, otherwise it messes up PyTables
-    #           typechecks
+    # Side note: This has to be of type list. Otherwise,
+    #            it messes up PyTables type checks.
 
-    def __add__(self, other):
+    def union(self, other):
+        """
+        Returns a FrozenList with other concatenated to the end of self.
+
+        Parameters
+        ----------
+        other : array-like
+            The array-like whose elements we are concatenating.
+
+        Returns
+        -------
+        diff : FrozenList
+            The collection difference between self and other.
+        """
         if isinstance(other, tuple):
             other = list(other)
-        return self.__class__(super(FrozenList, self).__add__(other))
+        return type(self)(super(FrozenList, self).__add__(other))
+
+    def difference(self, other):
+        """
+        Returns a FrozenList with elements from other removed from self.
+
+        Parameters
+        ----------
+        other : array-like
+            The array-like whose elements we are removing self.
+
+        Returns
+        -------
+        diff : FrozenList
+            The collection difference between self and other.
+        """
+        other = set(other)
+        temp = [x for x in self if x not in other]
+        return type(self)(temp)
 
-    __iadd__ = __add__
+    # TODO: Consider deprecating these in favor of `union` (xref gh-15506)
+    __add__ = __iadd__ = union
 
     # Python 2 compat
     def __getslice__(self, i, j):


Test patch:
diff --git a/pandas/tests/indexes/test_frozen.py b/pandas/tests/indexes/test_frozen.py
--- a/pandas/tests/indexes/test_frozen.py
+++ b/pandas/tests/indexes/test_frozen.py
@@ -11,7 +11,7 @@ class TestFrozenList(CheckImmutable, CheckStringMixin):
     mutable_methods = ('extend', 'pop', 'remove', 'insert')
     unicode_container = FrozenList([u("\u05d0"), u("\u05d1"), "c"])
 
-    def setup_method(self, method):
+    def setup_method(self, _):
         self.lst = [1, 2, 3, 4, 5]
         self.container = FrozenList(self.lst)
         self.klass = FrozenList
@@ -25,13 +25,30 @@ def test_add(self):
         expected = FrozenList([1, 2, 3] + self.lst)
         self.check_result(result, expected)
 
-    def test_inplace(self):
+    def test_iadd(self):
         q = r = self.container
+
         q += [5]
         self.check_result(q, self.lst + [5])
-        # other shouldn't be mutated
+
+        # Other shouldn't be mutated.
         self.check_result(r, self.lst)
 
+    def test_union(self):
+        result = self.container.union((1, 2, 3))
+        expected = FrozenList(self.lst + [1, 2, 3])
+        self.check_result(result, expected)
+
+    def test_difference(self):
+        result = self.container.difference([2])
+        expected = FrozenList([1, 3, 4, 5])
+        self.check_result(result, expected)
+
+    def test_difference_dupe(self):
+        result = FrozenList([1, 2, 3, 2]).difference([2])
+        expected = FrozenList([1, 3])
+        self.check_result(result, expected)
+
 
 class TestFrozenNDArray(CheckImmutable, CheckStringMixin):
     mutable_methods = ('put', 'itemset', 'fill')