PERF: PeriodEngine.get_loc accept narrow data type
State: closed

- [x] closes #29038
- [ ] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

xref #28628. 
I want to refactor `PeriodIndex.get_loc`. Because `PeriodIndex.get_loc` compute twice `Int64index.get_loc` and `PeriodEngine.get_loc`. Problem is only `PeriodEngine.get_loc` accepts `Period.ordinal`. 
I try to change `PeriodIndex.get_loc` in two ways.
First one, I try to remove `PeriodEngine.get_loc` in `PeriodIndex.get_loc`. so just compute `.get_loc` once. I change several ways But always stuck in test cases. So I give it up this way.
Second one, I try to change `PeriodEngine.get_loc` to accept `Period` not only `Period.ordinal`. This way is success in that work successfully and pass all test cases. However Problem is most cases users don't use `Period` using `PeriodIndex.get_loc` and `PeriodEngine.get_loc` can't accept "intolerance" argument. Not using `Int64index.get_loc` can't be possible. 
But I send PR. Little improvement is also improvement. 

---------------------------------------------------------------------------------------------------------------------
improvement : memory usage with Period.get_loc 


Labels:
name: Period
description: Period data type

Comments:
Hello @proost! Thanks for updating this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:





There are currently no PEP 8 issues detected in this Pull Request. Cheers! :beers: 

##### Comment last updated at 2020-01-15 16:30:18 UTC
@jreback 
Here is what i intent to change
Is there any simplficiation of PeriodIndex.get_loc that this makes possible?
@jbrockmendel 

Yes. I'm working on it. 


@jbrockmendel 
Could you take a look at it?


It looks like nothing in _libs.index is changed now.  I thought the idea was that something was wrong there that would be fixed, and that would allow simplification in PeriodIndex.get_loc.  Am I confused with something else?
@jbrockmendel 

In terms of simplication, Yes. I was wrong.
How about memory Usage?
Before,
```
In [2]: pi = pd.period_range(start=2010,end=2019,freq="m")                      
In [3]: pi.memory_usage()                                                       
Out[3]: 872
In [4]: pi.get_loc(pi[0])                                                       
Out[4]: 0
In [5]: pi.memory_usage()                                                       
Out[5]: 11984
```
After
```
In [3]: pi = pd.period_range(start=2010,end=2019,freq="m")                                                                                      
In [4]: pi.memory_usage()                                                                                                                       
Out[4]: 872
In [5]: pi.get_loc(pi[0])                           
Out[5]: 0
In [6]: pi.memory_usage()                           
Out[6]: 6864
```

I don't intent to it but i found it running test case. After `.get_loc`, use less memory space 

@proost I think you need to clarify the goal of this PR.  The referenced issue #29038 is about the behavior of PeriodEngine, but this doesn't touch that at all.
@jbrockmendel 
Purpose: Improvement function of `PeriodIndex.get_loc` 
Reason: Inefficient `PeriodIndex.get_loc`. Because `PeriodEngine.get_loc` acceptable parameter type is narrow.  So most case `PeriodIndex.get_loc` compute twice. Both `PeriodEngine` and `Int64Index`
So What i try to : 
-> first one : Change `PeriodEngine.get_loc` to accept `Period` to cover all parameter types. Problem is `PeriodEngine.get_loc` can't accept "tolerance" and "method". So if "method" and "tolerance" are not none, also in this case, `PeriodIndex.get_loc` compute twice. first `PeriodEngine` fails and then `Int64Index`
-> second one: Don't use `PeriodEngine.get_loc` Using `PeriodIndex.get_loc`.

What we can get from this change:
Before: I thought there is improvement in computation time. 
Now: Same speed. But use less memory.

Of course, This PR off track from original issue. In #29038, I narrowed the scope. It was my fault. Sorry not to upload adequate issue.
@proost are there non-trivial performance improvements from this?  If not, you need to clarify what this is accomplishing.
@jbrockmendel 
small improvement is prohibited? If prohibited, I will close this PR. If not, this PR for reducing memory usage.

Small improvements are encouraged!  We just need to communicate clearly what the relevant improvement is.  how much less memory does this use?  You're no longer calling the _engine method at the beginning, which makes me concerned that performance will take a hit.
@jbrockmendel 
from not calling `_engine.get_loc`, period index use less memory after using `get_loc`.
From this change, we can save less memory space about 60% ~ 100% before.

first case: 
```
pi = pd.period_range(start=2000,end=2019,freq="y") 
pi.get_loc('2000')
pi.memory_usage()
```
Before change, period index memory usage: 1600
After change, period index memory usage: 900

second case:
```
pi = pd.period_range(start=2000,end=2019,freq="m") 
pi.get_loc('2000')
pi.memory_usage()
```
Before change, period index memory usage: 24144
After change, period index memory usage: 13904

third case:
```
pi = pd.period_range(start=2010,end=2019,freq="d") 
pi.get_loc('2010')
pi.memory_usage()
```
Before change, period index memory usage: 380288
After change, period index memory usage: 216448

fourth case:
```
pi = pd.period_range(start=2010,end=2019,freq="h") 
pi.get_loc('2010')
pi.memory_usage()
```
Before change, period index memory usage: 6505104
After change, period index memory usage: 3883664

fifth case:
```
pi = pd.period_range(start=2010,end=2019,freq="s") 
pi.get_loc('2010')
pi.memory_usage()
```
Before change, period index memory usage: 4543948816
After change, period index memory usage: 4543948816

if you worry about time performance from this change, no need to worry.
command same above, 

first case:
```
Before Change: 11.7 µs ± 58.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After Change: 10.2 µs ± 42.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

second case:
```
Before Change: 11.7 µs ± 2.17 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After Change: 8.78 µs ± 31.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

third case:
```
Before Change: 11 µs ± 384 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After Change: 9.35 µs ± 40.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

fourth case:
```
Before Change: 10.7 µs ± 330 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After Change: 9.25 µs ± 216 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

fifth case:
```
Before Change: 28.8 µs ± 17.5 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
After Change: 26.8 µs ± 17.4 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

Almost same or slightly better before. So this change of code doesn't exacerbate on time performance.

so i wonder after this if we can blow PeriodEngine away entirely. cc @jbrockmendel 

@proost the memory savings are because we no longer need to instantiate the engine.
@proost if i understand correclty, all the timings you posted were with string inputs.  Can you do the same comparisons for 0, pd.Period("2000"), pd.NaT, pd.Timedelta(days=1)
> so i wonder after this if we can blow PeriodEngine away entirely

It looks like there is some non-trivial stuff in there for `get_indexer`, but we might be able to share more with DatetimeEngine
@jbrockmendel 
If
```
l = ['1970-01-01', '1970-01-02', '1970-01-03', '1970-01-01',
     '1970-01-05', '1970-01-06', '1970-01-07', '1970-01-08',
     '1970-01-09', '1970-01-10', '1970-01-11', '1970-01-12',
     '1970-01-13', '1970-01-14', '1970-01-15', '1970-01-16',
     '1970-01-17', '1970-01-18', '1970-01-19', '1970-01-20',
     '1970-01-21', '1970-01-22', '1970-01-23', '1970-01-24',
     '1970-01-25', '1970-01-26', '1970-01-27', '1970-01-28',
     '1970-01-29', '1970-01-30', '1970-01-31', np.datetime64('NaT')
]
pi = pd.PeriodIndex(l,freq="D")
```

case 1: 
```
p = pd.Period(year=1970, month=1, day=1, freq="D")
pi.get_loc(p)
```
Before:
memory usage: 3072
time taken: 11.8 µs ± 128 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After:
memory usage: 1792
time taken: 14 µs ± 101 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

case 2:
```
pi.get_loc(0)
```
Before: 
memory usage: 1536
time taken:  5.13 µs ± 25 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After:
memory usage: 1792
time taken: 11.2 µs ± 2.36 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)

case 3:
```
pi.get_loc(pd.NaT)
```
Before:
memory usage: 3072
time taken: 10.1 µs ± 348 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 
After:
memory usage: 1792
time taken: 11.7 µs ± 51.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

case 4:
Because "Timedelta" Object can't be converted to Period. So can't work on ".get_loc" 
But if you mean "Timestamp" object,
```
t = pd.Timestamp(year=1970, month=1, day=1)
pi.get_loc(t)
```
Before:
memory usage: 3072
time taken: 13.3 µs ± 533 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
After:
memory Usage: 1792
time taken: 16 µs ± 2.59 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Well, time performance becomes bad before. But i think this much can be acceptable.
except Integer case, all memory usage lower before. And Integer or float type is not expected input from user. Becasue `PeriodIndex.get_loc` parameter type is expected as "Period", "String", "datetime" or datetimelike object from user. So This is not a problem.  
@proost thank you for doing these additional timings.

For the Timedelta case i actually did want to know how long it took to fail.

@jbrockmendel 
Ok. I understand. So this is measurement of time.

```
td =  pd.Timedelta(days=1)
pi.get_loc(td)
```
Before: 13.5 ns ± 0.021 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
After: 15.7 ns ± 0.0219 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)

can you merge master and will look again