operator equal on Index should behavior similarly to Series
State: closed

currently on `master` (after the PR https://github.com/pydata/pandas/issues/9785), we have the following behavior when comparing two Indexes with `==`:

```
In [3]: index_a = Index(['a', 'b', 'c'])
In [4]: index_b = Index(['a', 'b', 'c', 'd'])
In [5]: index_a == index_b
Out[5]: False
```

Firstly, `==` should be element-wise comparison, so it really shouldn't return just a scalar `bool`. That should be the case for `Index.equals()` instead.

Secondly it is also not consistent with the behavior with `Series`

```
In [6]: series_a = Series(['a', 'b', 'c'])
In [7]: series_b = Series(['a', 'b', 'c', 'd'])
In [8]: series_a == series_b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-8-97f84083a0cd> in <module>()
----> 1 series_a == series_b

/Users/mortada_mehyar/code/github/pandas/pandas/core/ops.py in wrapper(self, other)
    576             name = _maybe_match_name(self, other)
    577             if len(self) != len(other):
--> 578                 raise ValueError('Series lengths must match to compare')
    579             return self._constructor(na_op(self.values, other.values),
    580                                      index=self.index, name=name)

ValueError: Series lengths must match to compare
```

This PR makes the behavior consistent by checking the Index lengths. Also fixes a few unit tests which seem to incorrectly assume `.equal()` should return the same output as `==` 


Labels:
name: Indexing
description: Related to indexing on series/frames, not to indexes themselves
name: API Design
name: Error Reporting
description: Incorrect or improved errors from pandas

Comments:
What happens if you compare an Index to a Series? Or to a numpy array? 

with the example above (without applying this PR) we would get:

```
In [8]: index_a == series_a
Out[8]: array([ True,  True,  True], dtype=bool)

In [9]: index_a == series_b
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-3ca722762b7b> in <module>()
----> 1 index_a == series_b

/Users/mortada_mehyar/code/github/pandas/pandas/core/ops.py in wrapper(self, other)
    583         elif isinstance(other, (np.ndarray, pd.Index)):
    584             if len(self) != len(other):
--> 585                 raise ValueError('Lengths must match to compare')
    586             return self._constructor(na_op(self.values, np.asarray(other)),
    587                                      index=self.index).__finalize__(self)

ValueError: Lengths must match to compare
In [10]: index_a == series_b.values
Out[10]: False
```

It seems like the length mismatch will be caught when comparing Index to a Series, but not to a numpy array 

This was done to provide `==` to work on an `Index` vs a `MultiIndex` (when they are the same length). I guess this changed the default behavior, which should be to raise (though this does differ and IMHO is a bit unexpected from `.equals`. But since this is numpy it just raises).

@mortada can you change back w/o breaking the fix in #9875  and maybe add some tests for `Index` vs `Series` as well to validate.

Update the same release note as #9875 and just add this issue number on

@jreback I added tests for `Index` vs `Series` and `np.array`, also added tests for `MultiIndex`, please take a look, thanks 

@jreback 

> I guess this changed the default behavior, which should be to raise 

What do you mean exactly? I think the current behaviour (returning False when comparing non-equal-length Index object/numpy arrays) is the 'default' behaviour for at least going back to 0.12 (and I suppose this is the case as this is the numpy behaviour to return False instead of raise if the two arrays can't broadcast).

Nonetheless, I think it would be good to have a more consistent behaviour across Index and Series. But I also think we should wait with this untill 0.17, as this is a clear and an on-purpose API change.

BTW, unequal length Index and Series don't always raise (so here it seems to follow numpy broadcast behaviour):

```
In [26]: index_a = Index(['a'])

In [27]: index_b = Index(['a', 'b', 'c', 'd'])

In [28]: index_a == index_b
Out[28]: array([ True, False, False, False], dtype=bool)

In [29]: index_a == series_b
Out[29]: array([ True, False, False, False], dtype=bool)

In [30]: index_a == series_b.values
Out[30]: array([ True, False, False, False], dtype=bool)
```

but for two Series it is different:

```
In [40]: index_a.to_series().reset_index(drop=True) == series_b
---
ValueError: Series lengths must match to compare
```

This issue is that this is pretty inconsitent (I guess this has always been the case, if so they should for sure wait for 0.17.0; I thought the related PR changed this).

```
In [1]: Series([1,2])==Series([1])
ValueError: Series lengths must match to compare

In [2]: Index([1,2])==Index([1])
Out[2]: array([ True, False], dtype=bool)
```

@mortada status?

@jreback with this PR the behavior is now consistent between `Series` and `Index`, in that unequal lengths would raise in both cases:

```
In [2]: Series([1,2]) == Series([1])
ValueError: Series lengths must match to compare

In [3]: Index([1,2]) == Index([1])
ValueError: Lengths must match to compare
```

Note that broadcasting still works if the comparison is against a scalar

```
In [4]: Series([1,2]) == 1
Out[4]:
0     True
1    False
dtype: bool

In [5]: Index([1,2]) == 1
Out[5]: array([ True, False], dtype=bool)
```

if this is the behavior we want this PR is already doing the above (although I'm certainly happy to add more test cases etc)

@mortada Can you add to your tests also the case of a index/series/array that is of a broadcastable length? As now you only include of length 3 and 4.

Further:
- can you add a whatsnew section explaining the change (API changes section)?
- this should also be explained somewhere in the docs I think (or is this is explained somewhere, this should be updated)

And to summarize the api changes/differences with numpy, there are two ways pandas differs:
- unequal lenght but not broadcastable: 
  - pandas: raises (with this PR)
  - numpy: just returns False
- unequal length but broadcastable: 
  - pandas: raises (for series, and for index broadcast now but raise after this PR)
  - numpy: broadcast and compare elementwise

Ideally, I think we stay as close as possible to the numpy behaviour. Although for the unequal length, I agree it is very strange to just return False and I like raising an error more. But for the broadcast issue, there is maybe less a reason to deviate from numpy behaviour?

numpy is wrong in returning `False` at the very least it should be `NotImplementedError` so the caller can do something. So this is a bug IMHO (though not really sure why they do this).

Futher, unequal lengths but can broadcast is an API issue in numpy in that they allow things like wrap-around (e.g. they will broadcast even if it 'wraps' around to the beginning of the array BY DEFAULT). I don't really remember the example.

We should follow numpy rationale behavior. Both of these case are irrational IMHO. (though to be fair, these prob aren't much of an issue in numpy the way you use it).

@jorisvandenbossche I've added a section to explain the change in `basics.rst` and also the whatsnew for `v0.17.0` 

I think we all agree that returning `False` is a very strange behavior, especially in the unbroadcastable case. In the broadcastable case I feel less strongly about it, however I think it's still slightly more consistent to raise

@jorisvandenbossche forgot to say, also added more test cases, please take a look 

Yes, maybe that was not fully clear, but I also agree completely we want to raise instead of just returning False. 
It was only for the broadcastable case that I was doubting a bit. But no strong opinion on that. In any case, it will only for very minor cases, namely a 1-element Index (as Indexes are always 1D). So OK to raise then as well.

Nice docs and whatsnew note!

@jorisvandenbossche Thanks! I've updated according to your comments, please take a look

@mortada ping when updated / green. I have a PR that I am going to push in after this one.

@jreback sure will ping you soon

@jreback updated, will ping you when green 

@jreback it's green now 

Thanks!

@mortada 

these tests should really be under the `Base` object in index tests, so that they tests for ALL sub-classes as this is a general operation.

Can you do a PR to move (this will involve changing the way the tests happen a bit).

@jreback sure I'll do that in another PR 

@mortada thanks. FYI, I just merged #10569  which touched some of this (the issue was I think the comparisons used `np.testing.assert_array_equal` which bomb when `nans` are there (and is not a standard pandas test method), so now use `assert_numpy_array_equivalent`.