Repository: pandas-dev/pandas

Issue:
ER/DOC: Sorting in multi-index columns: misleading error message, unclear docs
related #739

Have a look at this example:

``` python
import pandas as pd
import numpy as np
from StringIO import StringIO
print "Pandas version %s\n\n" % pd.__version__

data1 = """idx,metric
0,2.1
1,2.5
2,3"""

data2 = """idx,metric
0,2.7
1,2.2
2,2.8"""

df1 = pd.read_csv(StringIO(data1))
df2 = pd.read_csv(StringIO(data2))
concatenated = pd.concat([df1, df2], ignore_index=True)
merged = concatenated.groupby("idx").agg([np.mean, np.std])

print merged
print merged.sort('metric')
```

and its output:

```
$ python test.py 
Pandas version 0.11.0


     metric          
       mean       std
idx                  
0      2.40  0.424264
1      2.35  0.212132
2      2.90  0.141421
Traceback (most recent call last):
  File "test.py", line 22, in <module>
    print merged.sort('metric')
  File "/***/Python-2.7.3/lib/python2.7/site-packages/pandas/core/frame.py", line 3098, in sort
    inplace=inplace)
  File "/***/Python-2.7.3/lib/python2.7/site-packages/pandas/core/frame.py", line 3153, in sort_index
    % str(by))
ValueError: Cannot sort by duplicate column metric
```

The problem here is not that there is a duplicate column `metric` as stated by the error message. The problem is that there are still two sub-levels. The solution in this case is to use

``` python
merged.sort([('metric', 'mean')])
```

for sorting by the mean of the metric. It took myself quite a while to figure this out. First of all, the error message should be more clear in this case. Then, maybe I was too stupid, but I could not find the solution in the docs, but within a thread on StackOverflow. Looks like the error message above is the result of an over-generalized condition around https://github.com/pydata/pandas/blob/v0.12.0rc1/pandas/core/frame.py#L3269



Discussion:
yep....docs/error msg are unclear


Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -2530,7 +2530,7 @@ def _m8_to_i8(x):
     #----------------------------------------------------------------------
     # Sorting
 
-    def sort(self, columns=None, column=None, axis=0, ascending=True,
+    def sort(self, columns=None, axis=0, ascending=True,
              inplace=False):
         """
         Sort DataFrame either by labels (along either axis) or by the values in
@@ -2539,8 +2539,9 @@ def sort(self, columns=None, column=None, axis=0, ascending=True,
         Parameters
         ----------
         columns : object
-            Column name(s) in frame. Accepts a column name or a list or tuple
-            for a nested sort.
+            Column name(s) in frame. Accepts a column name or a list
+            for a nested sort. A tuple will be interpreted as the
+            levels of a multi-index.
         ascending : boolean or list, default True
             Sort ascending vs. descending. Specify list for multiple sort
             orders
@@ -2557,9 +2558,6 @@ def sort(self, columns=None, column=None, axis=0, ascending=True,
         -------
         sorted : DataFrame
         """
-        if column is not None:  # pragma: no cover
-            warnings.warn("column is deprecated, use columns", FutureWarning)
-            columns = column
         return self.sort_index(by=columns, axis=axis, ascending=ascending,
                                inplace=inplace)
 
@@ -2574,8 +2572,9 @@ def sort_index(self, axis=0, by=None, ascending=True, inplace=False,
         axis : {0, 1}
             Sort index/rows versus columns
         by : object
-            Column name(s) in frame. Accepts a column name or a list or tuple
-            for a nested sort.
+            Column name(s) in frame. Accepts a column name or a list
+            for a nested sort. A tuple will be interpreted as the
+            levels of a multi-index.
         ascending : boolean or list, default True
             Sort ascending vs. descending. Specify list for multiple sort
             orders
@@ -2602,7 +2601,7 @@ def sort_index(self, axis=0, by=None, ascending=True, inplace=False,
             if axis != 0:
                 raise ValueError('When sorting by column, axis must be 0 '
                                  '(rows)')
-            if not isinstance(by, (tuple, list)):
+            if not isinstance(by, list):
                 by = [by]
             if com._is_sequence(ascending) and len(by) != len(ascending):
                 raise ValueError('Length of ascending (%d) != length of by'
@@ -2629,6 +2628,13 @@ def trans(v):
                 by = by[0]
                 k = self[by].values
                 if k.ndim == 2:
+
+                    # try to be helpful
+                    if isinstance(self.columns, MultiIndex):
+                        raise ValueError('Cannot sort by column %s in a multi-index'
+                                         '  you need to explicity provide all the levels'
+                                         % str(by))
+
                     raise ValueError('Cannot sort by duplicate column %s'
                                      % str(by))
                 if isinstance(ascending, (tuple, list)):


Test patch:
diff --git a/pandas/tests/test_frame.py b/pandas/tests/test_frame.py
--- a/pandas/tests/test_frame.py
+++ b/pandas/tests/test_frame.py
@@ -9797,6 +9797,17 @@ def test_sort_index_duplicates(self):
             # multi-column 'by' is separate codepath
             df.sort_index(by=['a', 'b'])
 
+        # with multi-index
+        # GH4370
+        df = DataFrame(np.random.randn(4,2),columns=MultiIndex.from_tuples([('a',0),('a',1)]))
+        with assertRaisesRegexp(ValueError, 'levels'):
+            df.sort_index(by='a')
+
+        # convert tuples to a list of tuples
+        expected = df.sort_index(by=[('a',1)])
+        result = df.sort_index(by=('a',1))
+        assert_frame_equal(result, expected)
+
     def test_sort_datetimes(self):
 
         # GH 3461, argsort / lexsort differences for a datetime column