ENH: Add `replace` method to `Index` 
State: closed

- [X] closes #19495
- [X] tests added / passed
- [X] passes `black pandas`
- [X] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry
- [x] docstring for the new Index.replace() method
- [X] won't do: type hints
- [ ] merge with `ENH:` commit message

Added a replace method to Index classes, as well as tests.

Labels:
name: Enhancement
name: Index
description: Related to the Index class or subclasses
name: replace
description: replace method

Comments:
Also a question, can any of the new added tests be parameteraize?
Thanks @MomIsBestFriend, helpful indeed. I'll be waiting for a green flag from the core before I turn to docstrings and type annotations. Mainly since they take a lot of time... 😇  namely (1) figuring out the mechanics of docstrings for different subclasses and (2) correct types for type annotations.
@oguzhanogreden ATM you can ignore the CI failure of ```CI/Web And Docs```, we haven't yet resolved this issue. so you can ignore it. (for now at least)

Just be sure to merge master every now and then :)
Regarding the speed point, this seems faster. At the very least, faster more often. Be warned I'm not on top of what's going on `%timeit` mechanics. I chose the arguments below since they seemed reasonable from a measurement perspective. If there's a gotcha about performance, do let me know:

``` python
# Running on a MacBook Pro from 2017, under some use load
# (browsers with many tabs are open etc.)

import numpy as np

import pandas as pd

idx = pd.Index([1, 2, 3, 4, 5, 6])
%timeit -n 10000 -r 50 idx.replace(3, 9)

# 69.9 µs ± 6.67 µs per loop (mean ± std. dev. of 50 runs, 10000 loops each)

def replace(self, *args, **kwargs):
    return pd.Index(self.to_series().replace(*args, **kwargs))
pd.Index.replace = replace

idx = pd.Index([1, 2, 3, 4, 5, 6])
%timeit -n 10000 -r 50 idx.replace(3, 9)

# 348 µs ± 102 µs per loop (mean ± std. dev. of 50 runs, 10000 loops each)
# ^^^ couldn't get higher precision here, also not a much lower point estimate
```

Happy to move this to `asv` if the output is considerably more reliable.

Regarding the complexity point - agreed. Though I think there's some benefit to having these methods available. I found myself in a situation where I "just wanted to be able to do what I know from other parts of the pandas API". It's up to you folks to judge that against maintenance burden, ultimately.

I'd be interested in a contributing to a refactor around these methods. However I can imagine it'll be pretty tricky to abstract things away. Considering the nitty-gritty involved...

I'll survey properly whether i `.to_series()` fails to solve anything in the coming days.

Finally, I just added a few `NotImplementedError`s where this method, as it stands, shouldn't return anything. Eventually I'm happy to address those, at a cost of some more complexity, if the overall judgement is these are useful to have upstream.
Just to be clear, my proposal was not that you use that code (with the `to_series()`) in your own code. But that the implementation in this PR use it instead of all the code you're adding.

So, you'll still get the same functionality in pandas `Index.replace()`, but instead of adding 300 lines of code here, you'll be adding 30. But seems like the difference in performance is quite big.
Thanks. That was clear. I made the point about complexity since it's not clear to me how to weigh the performance difference against growing the code base so much :)

I suggest we follow up once I do the following:

- [ ] check if `to_series()` can handle most of the requirements,
- [ ] try and identify the cause of the performance issue, see if I can address and wrap `.to_series()` to create a solution,
I think we'd be much better off with

```
def replace(self, whatever):
    result = self.to_series().replace(whatever)
    return Index(result)
```
@oguzhanogreden do you have time to merge in master and adapt the implementation @jbrockmendel proposed? Would be beneficial to have a shared implementation to reduce code maintenance.
Sorry, I didn't get to prioritize this for a while. I'll try to get to it in a few days, with the suggested solution.
I added some tests for the "basic functionality". Not quite sure how to strike the balance: one arguments says all the existing tests of Index/MultiIndex -> Series/DataFrame conversions and the relevant `replace()` methods ensures the functionality here. What do you think?
I'll consider type hints out of scope for this PR since it will require a refactor. For instance, mypy will complain for `to_replace.items()` since `to_replace=` can be both a `list` and a `dict`.
if u can merge master and get passing will have a look
Hopefully the shared docs make sense.

Please be aware that the documentation of `replace()` methods contains has examples for DataFrame and Series only. I did _not_ add any more examples since the examples are fairly extensive already.
@jreback, all green.
The failing tests are due to master, AFAICT. If this isn't merged until the master is green, I will re-merge it once I notice the master passing.

(Just to have this noted down: The last commit message should read "~Cache~Catch errors not caught by checks")
@jreback looking at the latest merges, I have impression that the remaining failing test is 'okay'? 😬  
@oguzhanogreden I haven't looked at your PR yet at all, so no idea.
@oguzhanogreden Can you merge master and fix merge conflicts?
@oguzhanogreden can you rebase and address comments; i think this was fairly close to ready
Would be nice to get this over the line, indeed.

I'll need to work in #36178 too, then go on with the final requests above.
(I can't replicate the build errors locally.)
@oguzhanogreden can you do a pre-cursor PR that moves things and doesn't change anything (except for imports and so on), no functionailty at all. Its almost impossible to tell what you are changing otherwise. 
Now that #38561 is done, what's going here is a bit clearer (@jreback). Failing test is also failing on master, not related to changes here.

Let me try to summarise to help with review:

1. The key change is the addition of `.replace()` method to Index classes.
2. pd.Categorical does not allow regex replaces (see https://github.com/pandas-dev/pandas/issues/38447), therefore [we're disabling](https://github.com/pandas-dev/pandas/pull/32542/files#diff-d510328d75f258e42e90cab852f2cd17cc85c7ee10e6c3f69734e1e5bc87801eR640) it.
3. Due to the same reason, I have [**not** added](https://github.com/pandas-dev/pandas/pull/32542/files#r541932202) documentation to CategoricalIndex. This helps keep shared docs simple (i.e. without a conditional for regex stuff). Once the issues with categorical are solved, we can just add the decorator and it's good to go. I'll make an issue and reference it where needed, once this is merged.

Thanks for your patience here! It took too long in part due to my wrong initial approach (huge PR) and in part due to my absence from last March. Looking forward to getting this out of the door and paying back the investment ;)
ok can you merge master and will look
This was looking pretty close but looks like this PR has gotten stale. Going to close but if interested in continuing please ping, merge master, and target this PR for the next release (1.3 as of writing this comment).