Repository: pandas-dev/pandas

Issue:
Categorical.from_codes should validate ordered argument
I ran into this when I accidentally passed my codes to the `ordered` argument
#### Code Sample, a copy-pastable example if possible

``` python
In [2]: pd.Categorical.from_codes([0, 0, 1], categories=['a', 'b', 'c'], ordered=np.array([0, 1, 2]))
Out[2]: ---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/IPython/core/formatters.py in __call__(self, obj)
    668                 type_pprinters=self.type_printers,
    669                 deferred_pprinters=self.deferred_printers)
--> 670             printer.pretty(obj)
    671             printer.flush()
    672             return stream.getvalue()

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/IPython/lib/pretty.py in pretty(self, obj)
    381                             if callable(meth):
    382                                 return meth(obj, self, cycle)
--> 383             return _default_pprint(obj, self, cycle)
    384         finally:
    385             self.end_group()

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/IPython/lib/pretty.py in _default_pprint(obj, p, cycle)
    501     if _safe_getattr(klass, '__repr__', None) not in _baseclass_reprs:
    502         # A user-provided repr. Find newlines and replace them with p.break_()
--> 503         _repr_pprint(obj, p, cycle)
    504         return
    505     p.begin_group(1, '<')

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/IPython/lib/pretty.py in _repr_pprint(obj, p, cycle)
    692     """A pprint that just redirects to the normal repr function."""
    693     # Find newlines and replace them with p.break_()
--> 694     output = repr(obj)
    695     for idx,output_line in enumerate(output.splitlines()):
    696         if idx:

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/core/base.py in __repr__(self)
     70         Yields Bytestring in Py2, Unicode String in py3.
     71         """
---> 72         return str(self)
     73
     74

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/core/base.py in __str__(self)
     49
     50         if compat.PY3:
---> 51             return self.__unicode__()
     52         return self.__bytes__()
     53

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/core/categorical.py in __unicode__(self)
   1571             result = self._tidy_repr(_maxlen)
   1572         elif len(self._codes) > 0:
-> 1573             result = self._get_repr(length=len(self) > _maxlen)
   1574         else:
   1575             result = ('[], %s' %

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/core/categorical.py in _get_repr(self, length, na_rep, footer)
   1562         formatter = fmt.CategoricalFormatter(self, length=length,
   1563                                              na_rep=na_rep, footer=footer)
-> 1564         result = formatter.to_string()
   1565         return compat.text_type(result)
   1566

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/formats/format.py in to_string(self)
    134         result = [u('[') + result + u(']')]
    135         if self.footer:
--> 136             footer = self._get_footer()
    137             if footer:
    138                 result.append(footer)

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/formats/format.py in _get_footer(self)
    105             footer += "Length: %d" % len(self.categorical)
    106
--> 107         level_info = self.categorical._repr_categories_info()
    108
    109         # Levels are added in a newline

/Users/tom.augspurger/Envs/py3/lib/python3.5/site-packages/pandas/pandas/core/categorical.py in _repr_categories_info(self)
   1540         start = True
   1541         cur_col_len = len(levheader)  # header
-> 1542         sep_len, sep = (3, " < ") if self.ordered else (2, ", ")
   1543         linesep = sep.rstrip() + "\n"  # remove whitespace
   1544         for val in category_strs:

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
```

If you look through the traceback, the error happens in the `repr`, not at construction time.
The regular `Categorical` constructor does catch this (error message could be improved though).
#### Expected Output

`TypeError("`ordered`must be a boolean, got {!r} instead")`
#### output of `pd.show_versions()`

master



Discussion:

Resolving patch:
diff --git a/pandas/core/categorical.py b/pandas/core/categorical.py
--- a/pandas/core/categorical.py
+++ b/pandas/core/categorical.py
@@ -231,6 +231,8 @@ class Categorical(PandasObject):
     def __init__(self, values, categories=None, ordered=False,
                  name=None, fastpath=False):
 
+        self._validate_ordered(ordered)
+
         if fastpath:
             # fast path
             self._codes = _coerce_indexer_dtype(values, categories)
@@ -502,6 +504,25 @@ def _get_labels(self):
 
     _categories = None
 
+    @classmethod
+    def _validate_ordered(cls, ordered):
+        """
+        Validates that we have a valid ordered parameter. If
+        it is not a boolean, a TypeError will be raised.
+
+        Parameters
+        ----------
+        ordered : object
+            The parameter to be verified.
+
+        Raises
+        ------
+        TypeError
+            If 'ordered' is not a boolean.
+        """
+        if not is_bool(ordered):
+            raise TypeError("'ordered' must either be 'True' or 'False'")
+
     @classmethod
     def _validate_categories(cls, categories, fastpath=False):
         """
@@ -588,8 +609,7 @@ def set_ordered(self, value, inplace=False):
            Whether or not to set the ordered attribute inplace or return a copy
            of this categorical with ordered set to the value
         """
-        if not is_bool(value):
-            raise TypeError("ordered must be a boolean value")
+        self._validate_ordered(value)
         cat = self if inplace else self.copy()
         cat._ordered = value
         if not inplace:
diff --git a/pandas/indexes/category.py b/pandas/indexes/category.py
--- a/pandas/indexes/category.py
+++ b/pandas/indexes/category.py
@@ -123,6 +123,7 @@ def _create_categorical(self, data, categories=None, ordered=None):
         Categorical
         """
         if not isinstance(data, ABCCategorical):
+            ordered = False if ordered is None else ordered
             from pandas.core.categorical import Categorical
             data = Categorical(data, categories=categories, ordered=ordered)
         else:


Test patch:
diff --git a/pandas/tests/test_categorical.py b/pandas/tests/test_categorical.py
--- a/pandas/tests/test_categorical.py
+++ b/pandas/tests/test_categorical.py
@@ -398,6 +398,24 @@ def f():
             codes = np.random.choice([0, 1], 5, p=[0.9, 0.1])
             pd.Categorical.from_codes(codes, categories=["train", "test"])
 
+    def test_validate_ordered(self):
+        # see gh-14058
+        exp_msg = "'ordered' must either be 'True' or 'False'"
+        exp_err = TypeError
+
+        # This should be a boolean.
+        ordered = np.array([0, 1, 2])
+
+        with tm.assertRaisesRegexp(exp_err, exp_msg):
+            Categorical([1, 2, 3], ordered=ordered)
+
+        with tm.assertRaisesRegexp(exp_err, exp_msg):
+            Categorical.from_array([1, 2, 3], ordered=ordered)
+
+        with tm.assertRaisesRegexp(exp_err, exp_msg):
+            Categorical.from_codes([0, 0, 1], categories=['a', 'b', 'c'],
+                                   ordered=ordered)
+
     def test_comparisons(self):
 
         result = self.factor[self.factor == 'a']