Pandas Period の挙動まとめ (freq編)

# Pandas Period の `freq`

Pandasでは日付を `Period` として扱うことができる。
メリットをあげると例えば以下がある。
- 期間を扱えるため、`四半期後` とか `半年後` などの計算が簡単になる。( `+ 1` とかすればいい)
- 期中の中途半端な日付を情報を保存しつつ丸めることが可能

PandasのPeriodは使いこなせれば便利だけど、`datetime` との行き来がめんどくさかったり、期待している挙動をしないことが多いためメモしておく。 今回は `freq` に関して。 (そのうち`datetime` 変換のところもまとめる。)

Periodを使う際に期間の切り方に関するパラメータが `freq` 。
`Q` (クォーター) のデフォルトでは `Q-DEC` になっているため、年末くぎり。 日本の__年度__は4月からなので、それらを指定したいときとかに使う。

## パラメータに関する本家リンク

`freq` で指定できるパラメータはこの辺。 どこで使える使えないの情報は細かく記載されていない。

https://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
https://pandas.pydata.org/pandas-docs/stable/timeseries.html#anchored-offsets


## まとめ

パラメータの指定が全部動くのは `date_range` だけど、それだと `Period` の恩恵を受けられない。
とりあえずの対処法としては期間指定とかを諦めてデフォルトで変換して処理を行い、処理結果を表示するときは `datetime` などに戻すのがよさそう。

特にPeriodの `QS` はBug(enhancement?)っぽいので `issue` あげるなり、原因特定してコミットしたいところ(ちょっとみたところpandas内の `tslib` はCythonだからしんどそう…)。



## 対応表

|                      | Q    | 2Q   | QS   | Q-APR | 2QS  | 2Q-APR | QS-APR | 2QS-APR |
| -------------------- | ---- | ---- | ---- | ----- | ---- | ------ | ------ | ------- |
| Period               | ○    | ○    | ×    | ○     | ×    | ○      | ×      | ×       |
| period_range         | ○    | ○    | ×    | ○     | ×    | ○      | ×      | ×       |
| date_range           | ○    | ○    | ○    | ○     | ○    | ○      | ○      | ○       |
| date_range.to_period | ○    | ×    | ※1   | ○     | ×    | ×      | ※1     | ×       |

※1: エラーははかれないが `freq` がデフォルト( `Q-DEC` )になる



# 出力結果 (エラーなど)

## Pandas のバージョン

```python
In [82]: pd.__version__
Out[82]: '0.23.4'
```



## Period

### `2Q`

```python
In [56]: pd.Period('2018-04-01', freq='2Q')
Out[56]: Period('2018Q2', '2Q-DEC')
```

### `QS`

```python
In [37]: pd.Period('2018-04-01', freq='QS')
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies._period_str_to_code()

KeyError: 'QS-JAN'

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-37-670d308b00be> in <module>()
----> 1 pd.Period('2018-04-01', freq='QS')

pandas/_libs/tslibs/period.pyx in pandas._libs.tslibs.period.Period.__new__()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies.get_freq_code()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies.get_freq_code()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies._period_str_to_code()

ValueError: Invalid frequency: QS-JAN
```



## period_range

### `2Q`

```python
In [26]: pd.period_range('2018-04-01', '2020-01-01', freq='2Q')
Out[26]: PeriodIndex(['2018Q2', '2018Q4', '2019Q2', '2019Q4'], dtype='period[2Q-DEC]', freq='2Q-DEC')
```

### `2Q + 1`

```python
In [27]: pd.period_range('2018-04-01', '2020-01-01', freq='2Q') + 1
Out[27]: PeriodIndex(['2018Q4', '2019Q2', '2019Q4', '2020Q2'], dtype='period[2Q-DEC]', freq='2Q-DEC')
```

### `QS`

```python
In [33]: pd.period_range('2018-04-01', '2020-01-01', freq='QS')
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies._period_str_to_code()

KeyError: 'QS-JAN'

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-33-25a035e5f517> in <module>()
----> 1 pd.period_range('2018-04-01', '2020-01-01', freq='QS')

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in period_range(start, end, periods, freq, name)
   1295
   1296     return PeriodIndex(start=start, end=end, periods=periods,
-> 1297                        freq=freq, name=name)

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in __new__(cls, data, ordinal, freq, start, end, periods, tz, dtype, copy, name, **fields)
    275             else:
    276                 data, freq = cls._generate_range(start, end, periods,
--> 277                                                  freq, fields)
    278             return cls._from_ordinals(data, name=name, freq=freq)
    279

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in _generate_range(cls, start, end, periods, freq, fields)
    331                 raise ValueError('Can either instantiate from fields '
    332                                  'or endpoints, but not both')
--> 333             subarr, freq = _get_ordinal_range(start, end, periods, freq)
    334         elif field_count > 0:
    335             subarr, freq = _range_from_fields(freq=freq, **fields)

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in _get_ordinal_range(start, end, periods, freq, mult)
   1144
   1145     if freq is not None:
-> 1146         _, mult = _gfc(freq)
   1147
   1148     if start is not None:

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies.get_freq_code()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies.get_freq_code()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies.get_freq_code()

pandas/_libs/tslibs/frequencies.pyx in pandas._libs.tslibs.frequencies._period_str_to_code()

ValueError: Invalid frequency: QS-JAN
```

## date_range

### `QS-APR`

```python
In [55]: pd.date_range('2018-04-01', '2020-01-01', freq='QS-APR')
Out[55]:
DatetimeIndex(['2018-04-01', '2018-07-01', '2018-10-01', '2019-01-01',
               '2019-04-01', '2019-07-01', '2019-10-01', '2020-01-01'],
              dtype='datetime64[ns]', freq='QS-APR')
```

### `2QS`

```python
In [29]: pd.date_range('2018-04-01', '2020-01-01', freq='2QS')
Out[29]: DatetimeIndex(['2018-04-01', '2018-10-01', '2019-04-01', '2019-10-01'], dtype='datetime64[ns]', freq='2QS-JAN')
```

### `2QS-APR`

```python
In [30]: pd.date_range('2018-04-01', '2020-01-01', freq='2QS-APR')
Out[30]: DatetimeIndex(['2018-04-01', '2018-10-01', '2019-04-01', '2019-10-01'], dtype='datetime64[ns]', freq='2QS-APR')
```

### `2QS-APR + 1`

```python
In [31]: pd.date_range('2018-04-01', '2020-01-01', freq='2QS-APR') + 1
Out[31]: DatetimeIndex(['2018-10-01', '2019-04-01', '2019-10-01', '2020-04-01'], dtype='datetime64[ns]', freq='2QS-APR')
```



## date_range.to_period

### `Q`

```python
In [48]: pd.date_range('2018-04-01', '2020-01-01', freq='Q').to_period()
Out[48]:
PeriodIndex(['2018Q2', '2018Q3', '2018Q4', '2019Q1', '2019Q2', '2019Q3',
             '2019Q4'],
            dtype='period[Q-DEC]', freq='Q-DEC')
```

### `QS`

`QS` の部分はperiodに変換したときに無視されている

```python
In [49]: pd.date_range('2018-04-01', '2020-01-01', freq='QS').to_period()
Out[49]:
PeriodIndex(['2018Q2', '2018Q3', '2018Q4', '2019Q1', '2019Q2', '2019Q3',
             '2019Q4', '2020Q1'],
            dtype='period[Q-DEC]', freq='Q-DEC')
```

### `Q-APR`

`Q-APR` は保存される

```python
In [53]: pd.date_range('2018-04-01', '2020-01-01', freq='Q-APR').to_period()
Out[53]:
PeriodIndex(['2018Q4', '2019Q1', '2019Q2', '2019Q3', '2019Q4', '2020Q1',
             '2020Q2'],
            dtype='period[Q-APR]', freq='Q-APR')
```

### `QS-APR` 

APRも無視される

```python
In [54]: pd.date_range('2018-04-01', '2020-01-01', freq='QS-APR').to_period()
Out[54]:
PeriodIndex(['2018Q2', '2018Q3', '2018Q4', '2019Q1', '2019Q2', '2019Q3',
             '2019Q4', '2020Q1'],
            dtype='period[Q-DEC]', freq='Q-DEC')
```



### `2Q`

なぜかエラー

```python
In [42]: pd.date_range('2018-04-01', '2020-01-01', freq='2Q').to_period()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-42-ab52acd3f100> in <module>()
----> 1 pd.date_range('2018-04-01', '2020-01-01', freq='2Q').to_period()

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/datetimes.py in to_period(self, freq)
   1117             freq = get_period_alias(freq)
   1118
-> 1119         return PeriodIndex(self.values, name=self.name, freq=freq, tz=self.tz)
   1120
   1121     def snap(self, freq='S'):

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in __new__(cls, data, ordinal, freq, start, end, periods, tz, dtype, copy, name, **fields)
    303         # datetime other than period
    304         if is_datetime64_dtype(data.dtype):
--> 305             data = dt64arr_to_periodarr(data, freq, tz)
    306             return cls._from_ordinals(data, name=name, freq=freq)
    307

~/.pyenv/versions/miniconda-latest/envs/test/lib/python3.6/site-packages/pandas/core/indexes/period.py in dt64arr_to_periodarr(data, freq, tz)
     65         raise ValueError('Wrong dtype: %s' % data.dtype)
     66
---> 67     freq = Period._maybe_convert_freq(freq)
     68     base, mult = _gfc(freq)
     69     return period.dt64arr_to_periodarr(data.view('i8'), base, tz)

pandas/_libs/tslibs/period.pyx in pandas._libs.tslibs.period._Period._maybe_convert_freq()

AttributeError: 'NoneType' object has no attribute 'n'
```