Repository: pandas-dev/pandas

Issue:
PERF: Period factorization very slow in 0.19.0
``` python
df = DataFrame(data={'data': np.random.randint(0, 100, size=5500000),
                         'date': [dt(2016, 1, 1)] * 5500000})
for period, g in df.groupby(pd.DatetimeIndex(df.date).to_period('D')):
    print(g)

```
#### Expected Output

outputs dataframe
#### Output of `pd.show_versions()`

0.19.0

The output is not the issue, the issue is that in any version before 0.19.0, this was incredibly fast, like ~1 second or less. With 0.19.0, after waiting many minutes I just give up.



Discussion:
Can you simplify the example here to the simplest possible setup? e.g., by removing the `groupby`?

It's not the `to_period` that's slow, so it's probably the groupby.

``` python
In [24]: %time p = pd.DatetimeIndex(df.date).to_period('D')
CPU times: user 339 ms, sys: 6.71 ms, total: 345 ms
Wall time: 350 ms
```

Also, post the actual code you're running if you could (imports too).

Here's a smaller example,

``` python
import time
import pandas as pd

p = pd.period_range('2010-01-01', freq='D', periods=100000)
t0 = time.time()
pd.factorize(p)
t1 = time.time()
print('{}: {:.2f}s'.format(pd.__version__, t1 - t0))
```

Some outputs:
- 0.18.1: 0.01s
- 0.19.0rc1: 2.96s

This is probably due to `np.asarray(PeriodIndex)` not returning an array of integers.

``` python
# 0.18.1
In [5]: np.asarray(p)
Out[5]: array([ 14610,  14611,  14612, ..., 114607, 114608, 114609])
```

``` python
# 0.19
In [4]: np.asarray(p)
Out[4]:
array([Period('2010-01-01', 'D'), Period('2010-01-02', 'D'),
       Period('2010-01-03', 'D'), ..., Period('2283-10-14', 'D'),
       Period('2283-10-15', 'D'), Period('2283-10-16', 'D')], dtype=object)
```

cc @sinhrks I think.

Probably just need a check similar to datetimetz around here to view as an `int64`

https://github.com/pydata/pandas/blob/v0.19.0/pandas/core/algorithms.py#L294

@MattRijk  See http://pandas.pydata.org/pandas-docs/stable/contributing.html

@MattRijk Personally, I use SublimeText, usually just on a laptop. But this is off topic for this issue.

yeah this is a pretty easy fix, IIRC this was in @sinhrks PeriodBlock PR, but must have been backed out...something like

```
if is_period_dtype(values):
    values = values.view('i8')
```

Caused by #13988. I think the logic of period/datetimetz can be merged using `needs_i8_conversion`. 

And the following comment is no longer correct...
- https://github.com/pydata/pandas/blob/v0.19.0/pandas/core/algorithms.py#L292

Looks like a 0.19.1 may be close around the corner...


Resolving patch:
diff --git a/asv_bench/benchmarks/groupby.py b/asv_bench/benchmarks/groupby.py
--- a/asv_bench/benchmarks/groupby.py
+++ b/asv_bench/benchmarks/groupby.py
@@ -548,6 +548,32 @@ def time_groupby_sum(self):
         self.df.groupby(['a'])['b'].sum()
 
 
+class groupby_period(object):
+    # GH 14338
+    goal_time = 0.2
+
+    def make_grouper(self, N):
+        return pd.period_range('1900-01-01', freq='D', periods=N)
+
+    def setup(self):
+        N = 10000
+        self.grouper = self.make_grouper(N)
+        self.df = pd.DataFrame(np.random.randn(N, 2))
+
+    def time_groupby_sum(self):
+        self.df.groupby(self.grouper).sum()
+
+
+class groupby_datetime(groupby_period):
+    def make_grouper(self, N):
+        return pd.date_range('1900-01-01', freq='D', periods=N)
+
+
+class groupby_datetimetz(groupby_period):
+    def make_grouper(self, N):
+        return pd.date_range('1900-01-01', freq='D', periods=N,
+                             tz='US/Central')
+
 #----------------------------------------------------------------------
 # Series.value_counts
 
diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py
--- a/pandas/core/algorithms.py
+++ b/pandas/core/algorithms.py
@@ -285,18 +285,27 @@ def factorize(values, sort=False, order=None, na_sentinel=-1, size_hint=None):
     note: an array of Periods will ignore sort as it returns an always sorted
     PeriodIndex
     """
-    from pandas import Index, Series, DatetimeIndex
-
-    vals = np.asarray(values)
-
-    # localize to UTC
-    is_datetimetz_type = is_datetimetz(values)
-    if is_datetimetz_type:
-        values = DatetimeIndex(values)
-        vals = values.asi8
+    from pandas import Index, Series, DatetimeIndex, PeriodIndex
+
+    # handling two possibilities here
+    # - for a numpy datetimelike simply view as i8 then cast back
+    # - for an extension datetimelike view as i8 then
+    #   reconstruct from boxed values to transfer metadata
+    dtype = None
+    if needs_i8_conversion(values):
+        if is_period_dtype(values):
+            values = PeriodIndex(values)
+            vals = values.asi8
+        elif is_datetimetz(values):
+            values = DatetimeIndex(values)
+            vals = values.asi8
+        else:
+            # numpy dtype
+            dtype = values.dtype
+            vals = values.view(np.int64)
+    else:
+        vals = np.asarray(values)
 
-    is_datetime = is_datetime64_dtype(vals)
-    is_timedelta = is_timedelta64_dtype(vals)
     (hash_klass, vec_klass), vals = _get_data_algo(vals, _hashtables)
 
     table = hash_klass(size_hint or len(vals))
@@ -311,13 +320,9 @@ def factorize(values, sort=False, order=None, na_sentinel=-1, size_hint=None):
         uniques, labels = safe_sort(uniques, labels, na_sentinel=na_sentinel,
                                     assume_unique=True)
 
-    if is_datetimetz_type:
-        # reset tz
-        uniques = values._shallow_copy(uniques)
-    elif is_datetime:
-        uniques = uniques.astype('M8[ns]')
-    elif is_timedelta:
-        uniques = uniques.astype('m8[ns]')
+    if dtype is not None:
+        uniques = uniques.astype(dtype)
+
     if isinstance(values, Index):
         uniques = values._shallow_copy(uniques, name=None)
     elif isinstance(values, Series):


Test patch: