Repository: pandas-dev/pandas

Issue:
bool(MultiIndex) seems to always return False
This means that patterns such as:

``` python
if my_frame.index:
    # do something
```

with `my_frame` having a `MultiIndex` will never enter the `if` block.  One can check that `bool(my_frame.index)` returns False.  A single index (`pandas.Index`) will (correctly IMO) raise a `ValueError` warning that the truth value of an array is ambiguous.  However, a `MultiIndex` fails silently.

This is such an awesome source of silent bugs that I'm surprised this hasn't come up before.  Is there any reason why we want `bool` to return `False` every time for a `MultiIndex`?



Discussion:
hmm, actually it should always return `True`. You cannot construct an empty MultiIndex at all. But more to the point, this is a very odd thing to do and is ambiguous (maybe this should ALWAYS raise).. See docs (for Series), here: http://pandas.pydata.org/pandas-docs/stable/gotchas.html

and in reality this should simply raise. its a numpy artifact that it does not.

you should do:

```
if len(my_frame.index):
    pass 
```

or 

```
if len(pandas_object):
    pass
```

as the index can never be None

The case that caused a bug for me was this:

``` python
# Reindex by a common index
new_index = frame_1.index.intersection(frame_2.index)
frame_1 = frame_1.reindex(index=new_index)
frame_2 = frame_2.reindex(index=new_index)
```

The natural thing to do, since you have `new_index` ready, is to check if it is empty.  The natural python way to do this is with `if new_index`.  Since the `levels` array in `new_index` is empty, I would expect `bool(new_index)` to be `False` (or to raise a `ValueError` such as numpy arrays do). 

can u post the starting indexes? or a facsimile ?

My previous comment (about my bug) was confusing.  The bug occurred when there was a non-empty index intersection, and I therefore expected to step into an `if` block.  Since `bool` returned `False`, my code did not step in.

``` python
>>> frame_1 = pandas.DataFrame({'col1': [1, 2]},
                               index= pandas.MultiIndex.from_tuples([('A', 1), ('A', 2)]))
>>> frame_2 = pandas.DataFrame({'col1': [1, 2]},
                               index= pandas.MultiIndex.from_tuples([('A', 1), ('A', 3)]))
>>> common_index = frame_1.index.intersection(frame_2.index)
>>> common_index
MultiIndex(levels=[[u'A'], [1]],
           labels=[[0], [0]],
           sortorder=0)
```

And then

``` python
>>> if common_index:
         print "Stepped in"
        else:
          print "Did not step in"

Did not step in
```

ok I'll have a look

I don't this was defined and relied on numpy behavior. was always false because of the way a MultiIndex is defined

I think it's reasonable to make the **nonzero** simply return the len boolean
but let me see

> This is such an awesome source of silent bugs

+1. IMO doing anything other than raising would be surprising (same argument as Series and DataFrame).

_Wow to the other PR this inspired!_


Resolving patch:
diff --git a/pandas/core/base.py b/pandas/core/base.py
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -313,10 +313,34 @@ def max(self):
         """ The maximum value of the object """
         return nanops.nanmax(self.values)
 
+    def argmax(self, axis=None):
+        """
+        return a ndarray of the maximum argument indexer
+
+        See also
+        --------
+        numpy.ndarray.argmax
+        """
+        return nanops.nanargmax(self.values)
+
     def min(self):
         """ The minimum value of the object """
         return nanops.nanmin(self.values)
 
+    def argmin(self, axis=None):
+        """
+        return a ndarray of the minimum argument indexer
+
+        See also
+        --------
+        numpy.ndarray.argmin
+        """
+        return nanops.nanargmin(self.values)
+
+    def hasnans(self):
+        """ return if I have any nans; enables various perf speedups """
+        return com.isnull(self).any()
+
     def value_counts(self, normalize=False, sort=True, ascending=False,
                      bins=None, dropna=True):
         """
@@ -554,10 +578,11 @@ def argmin(self, axis=None):
         numpy.ndarray.argmin
         """
 
-        ##### FIXME: need some tests (what do do if all NaT?)
         i8 = self.asi8
         if self.hasnans:
             mask = i8 == tslib.iNaT
+            if mask.all():
+                return -1
             i8 = i8.copy()
             i8[mask] = np.iinfo('int64').max
         return i8.argmin()
@@ -596,10 +621,11 @@ def argmax(self, axis=None):
         numpy.ndarray.argmax
         """
 
-        #### FIXME: need some tests (what do do if all NaT?)
         i8 = self.asi8
         if self.hasnans:
             mask = i8 == tslib.iNaT
+            if mask.all():
+                return -1
             i8 = i8.copy()
             i8[mask] = 0
         return i8.argmax()
diff --git a/pandas/core/index.py b/pandas/core/index.py
--- a/pandas/core/index.py
+++ b/pandas/core/index.py
@@ -560,6 +560,7 @@ def _mpl_repr(self):
 
     @property
     def is_monotonic(self):
+        """ return if the index has monotonic (only equaly or increasing) values """
         return self._engine.is_monotonic
 
     def is_lexsorted_for_tuple(self, tup):
@@ -567,6 +568,7 @@ def is_lexsorted_for_tuple(self, tup):
 
     @cache_readonly(allow_setting=True)
     def is_unique(self):
+        """ return if the index has unique values """
         return self._engine.is_unique
 
     def is_integer(self):
@@ -788,6 +790,7 @@ def _get_level_number(self, level):
 
     @cache_readonly
     def inferred_type(self):
+        """ return a string of the type inferred from the values """
         return lib.infer_dtype(self)
 
     def is_type_compatible(self, typ):
@@ -835,6 +838,13 @@ def __setstate__(self, state):
     def __deepcopy__(self, memo={}):
         return self.copy(deep=True)
 
+    def __nonzero__(self):
+        raise ValueError("The truth value of a {0} is ambiguous. "
+                         "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
+                         .format(self.__class__.__name__))
+
+    __bool__ = __nonzero__
+
     def __contains__(self, key):
         hash(key)
         # work around some kind of odd cython bug
@@ -2143,6 +2153,11 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None, fastpath=False, *
     def inferred_type(self):
         return 'integer'
 
+    @cache_readonly
+    def hasnans(self):
+        # by definition
+        return False
+
     @property
     def asi8(self):
         # do not cache or you'll create a memory leak
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- a/pandas/tseries/index.py
+++ b/pandas/tseries/index.py
@@ -1490,13 +1490,6 @@ def searchsorted(self, key, side='left'):
     def is_type_compatible(self, typ):
         return typ == self.inferred_type or typ == 'datetime'
 
-    def argmin(self):
-        # hack to workaround argmin failure
-        try:
-            return self.values.argmin()
-        except Exception:  # pragma: no cover
-            return self.asi8.argmin()
-
     @property
     def inferred_type(self):
         # b/c datetime is represented as microseconds since the epoch, make


Test patch:
diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py
--- a/pandas/tests/test_base.py
+++ b/pandas/tests/test_base.py
@@ -261,6 +261,27 @@ def test_nanops(self):
                 # check DatetimeIndex non-monotonic path
                 self.assertEqual(getattr(obj, op)(), datetime(2011, 11, 1))
 
+        # argmin/max
+        obj = Index(np.arange(5,dtype='int64'))
+        self.assertEqual(obj.argmin(),0)
+        self.assertEqual(obj.argmax(),4)
+
+        obj = Index([np.nan, 1, np.nan, 2])
+        self.assertEqual(obj.argmin(),1)
+        self.assertEqual(obj.argmax(),3)
+
+        obj = Index([np.nan])
+        self.assertEqual(obj.argmin(),-1)
+        self.assertEqual(obj.argmax(),-1)
+
+        obj = Index([pd.NaT, datetime(2011, 11, 1), datetime(2011,11,2),pd.NaT])
+        self.assertEqual(obj.argmin(),1)
+        self.assertEqual(obj.argmax(),2)
+
+        obj = Index([pd.NaT])
+        self.assertEqual(obj.argmin(),-1)
+        self.assertEqual(obj.argmax(),-1)
+
     def test_value_counts_unique_nunique(self):
         for o in self.objs:
             klass = type(o)
diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py
--- a/pandas/tests/test_index.py
+++ b/pandas/tests/test_index.py
@@ -74,6 +74,15 @@ def test_numeric_compat(self):
                               "cannot perform floor division",
                               lambda : 1 // idx)
 
+    def test_boolean_context_compat(self):
+
+        # boolean context compat
+        idx = self.create_index()
+        def f():
+            if idx:
+                pass
+        tm.assertRaisesRegexp(ValueError,'The truth value of a',f)
+
 class TestIndex(Base, tm.TestCase):
     _holder = Index
     _multiprocess_can_split_ = True
@@ -1656,6 +1665,19 @@ def setUp(self):
     def create_index(self):
         return self.index
 
+    def test_boolean_context_compat2(self):
+
+        # boolean context compat
+        # GH7897
+        i1 = MultiIndex.from_tuples([('A', 1), ('A', 2)])
+        i2 = MultiIndex.from_tuples([('A', 1), ('A', 3)])
+        common = i1.intersection(i2)
+
+        def f():
+            if common:
+                pass
+        tm.assertRaisesRegexp(ValueError,'The truth value of a',f)
+
     def test_hash_error(self):
         with tm.assertRaisesRegexp(TypeError,
                                    "unhashable type: %r" %