いまさらpandas0.24について

2019/01/25にリリースされた`pandas v0.24.0`ですが、
非常によいアップデートなので、いまさらながら紹介しようかなと思います。

# 注目ポイント
- Integer型のNaN対応！
- Extension Types中心の設計！
- ~(チルダ)問題解決
- タイムゾーン問題解決

# 0.24というバージョンについて

はじめに言っておきたいのが、この`0.24`というバージョン。
神アプデです。

今までのpandasの「ウーン。。。🤔」ポイントが解決されていますので、まだ使ってないという方は下にあげるような機能を中心に、是非触ってみて欲しいです。

# 注目ポイントピックアップ

## 1. ついにInt64のNaNに対応

早速みてみましょう。

まずは、従来の動き。

```python
>>> # Int型のA列と、Float型のB列を持つデータフレーム。
>>> df = pandas.DataFrame({
        'A': [1,2,3],
        'B': [1.1, 1.2, 1.3]
    })
>>> df
	A	B
0	1	1.1
1	2	1.2
2	3	1.3

>>> # A, Bともに、3行目をNaNにしていく。
>>> df.loc[2, 'A'] = None
>>> df.loc[2, 'B'] = None
>>> df
	A	B
0	1.0	1.1
1	2.0	1.2
2	NaN	NaN

```
いやいやいや。`1.0`ってなんですか？
というのが従来のpandas。

続いて、`0.24`の動き。

```python
>>> # Int64, Float64を明示してデータフレーム生成。
>>> # Int64は、pandas Extension Type。
>>> df2 = pandas.DataFrame({
        'A': pandas.Series([1, 2, 3], dtype='Int64'),
        'B': pandas.Series([1.1, 1.2, 1.3], dtype='Float64')
    })
>>> df2
	A	B
0	1	1.1
1	2	1.2
2	3	1.3

>>> # A, Bともに、3行目をNaNにしていく。
>>> df2.loc[2, 'A'] = None
>>> df2.loc[2, 'B'] = None
>>> df2
	A	B
0	1	1.1
1	2	1.2
2	NaN	NaN

>>> df2.A
0      1
1      2
2    NaN
Name: A, dtype: Int64

```

おー！確かに、`Int64`が維持されています！
さすが`0.24`！

ちなみに、メリットを感じるのは、こんなケース。

|  | value |
|--:|--:|
| 0 | 0.0 |
| 1 | 1.0 |
| 2 | 2.0 |
| 3 | 3.0 |
| 4 | NaN |
| 5 | 5.0 |
| 6 | 6.0 |
| 7 | 6.999999999999999 |
| 8 | 8.0 |
| 9 | 9.000000000000002 |
| 10 | 10.0 |

こういうの、意外とあるんですよね。

## 2. Extension Types

この**Extension Types**なる存在、実は`0.23`から入ってます。
[公式の解説ページ](https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extension-types)にあるとおり、`numpy.dtype`のようなカスタム型を`pandas`に持ち込む仕組みです。

`0.24`では、`ExtensionArray`なる機能が追加され、カスタム型に対応した`numpy.array`のような立ち位置で使用可能なようです。

さて、このExtension Typesですが、「実際役に立つんか？」と疑問を感じる方が多いと思います。
そんな方のために、Extension Typesが役に立っているシーン・役に立つであろうシーンの例を挙げておきます。

- 最近対応した`Categorical`型は実は`ExtensionDtype`
- 上記の`NaN`対応`Int64`型は実は`ExtensionDtype`
- `0.24.0`にて追加の`Interval`型も`ExtensionDtype`
- 緯度経度を扱う`Geo`型など、役立ちそう
- 多角形などの図形を扱う`Polygon`型など、役立ちそう
- カテゴリカルデータの組み合わせを表現する`CategoricalCombination`型など、役立ちそう

## 3. ~(チルダ)対応

> DataFrame.to_json(), DataFrame.to_csv(), DataFrame.to_pickle(), and other export methods now support tilde(~) in path argument. (GH23473)

っていうね。なんで今まで`~`使えなかったんだよっ！
なんども忘れて`FileNotFound`に苦しめられましたが、これにて解放です。

## 4. タイムゾーン対応

今まで、`to_datetime`や`DatetimeIndex`など一部機能にて、タイムゾーンをガン無視するスタイルのpandasでしたが、ちゃんと`tz`がキープされるようになりました。これは、うれしい人が多いのでは？

[公式](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v0.24.0.html#parsing-datetime-strings-with-timezone-offsets)からのコピペですが、以前は

```python
In [2]: pd.to_datetime("2015-11-18 15:30:00+05:30")
Out[2]: Timestamp('2015-11-18 10:00:00')

In [3]: pd.Timestamp("2015-11-18 15:30:00+05:30")
Out[3]: Timestamp('2015-11-18 15:30:00+0530', tz='pytz.FixedOffset(330)')

# Different UTC offsets would automatically convert the datetimes to UTC (without a UTC timezone)
In [4]: pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"])
Out[4]: DatetimeIndex(['2015-11-18 10:00:00', '2015-11-18 10:00:00'], dtype='datetime64[ns]', freq=None)
```

のように、勝手に`utc`に変えちゃうマンでしたが、今後は、

```python
In [56]: pd.to_datetime("2015-11-18 15:30:00+05:30")
Out[56]: Timestamp('2015-11-18 15:30:00+0530', tz='pytz.FixedOffset(330)')

In [57]: pd.Timestamp("2015-11-18 15:30:00+05:30")
Out[57]: Timestamp('2015-11-18 15:30:00+0530', tz='pytz.FixedOffset(330)')

In [63]: pd.to_datetime(["2015-11-18 15:30:00+05:30",
   ....:                 "2015-11-18 16:30:00+06:30"], utc=True)
   ....: 
Out[63]: DatetimeIndex(['2015-11-18 10:00:00+00:00', '2015-11-18 10:00:00+00:00'], dtype='datetime64[ns, UTC]', freq=None)
```
タイムゾーンを維持したり、指定のタイムゾーンに変換したりできるようになります。
また、タイムゾーンが混合しているCSVファイルの読み込み時にも、個別にタイムゾーンを維持してくれるようです。

```python
In [64]: import io

In [65]: content = """\
   ....: a
   ....: 2000-01-01T00:00:00+05:00
   ....: 2000-01-01T00:00:00+06:00"""
   ....: 

In [66]: df = pd.read_csv(io.StringIO(content), parse_dates=['a'])

In [67]: df.a
Out[67]: 
0    2000-01-01 00:00:00+05:00
1    2000-01-01 00:00:00+06:00
Name: a, Length: 2, dtype: object
```

# 今後の期待感

## 1. カスタムJOINできるようにしてほしい

今後の期待機能を語る上で外せない最重要ポイント。それは`JOIN`処理(`pandas.merge`, `pandas.DataFrame.join`)です。
なぜか。例えば、こんなケース。

```python
>>> class A():
        def __init__(self, v):
            self.v = v
        
        def __eq__(self, u):
            return self.v % u == 0
    
        def __repr__(self):
            return f'A(v={self.v})'
>>> df = pandas.DataFrame({'a': [A(5), A(10), A(15)]})
>>> df
	a
0	A(v=5)
1	A(v=10)
2	A(v=15)

>>> df2 = pandas.DataFrame({'b': [A(3), A(5)], 'c': [3, 5]})
>>> df2
	b		c
0	A(v=3)	3
1	A(v=5)	5

```

クラス`A`は、割り切れる値に対して、同値と判定します。
感覚的には、`JOIN`処理を通して、

```python
	a		b		c
0	A(v=5)	A(v=5)	5
1	A(v=10)	A(v=5)	5
2	A(v=15)	A(v=3)	3
3	A(v=15)	A(v=5)	5
```
こんな結果が出てきて欲しい。そんなケース。
さっそくやってみよう！

```python
>>> pandas.merge(df, df2, left_on=['a'], right_on=['b'])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-131-00ec1523c45d> in <module>()
----> 1 pandas.merge(df, df2, left_on=['a'], right_on=['b'])

...

~/.pyenv/versions/3.6.1/lib/python3.6/site-packages/pandas/core/reshape/merge.py in _factorize_keys(lk, rk, sort)
   1659     rizer = klass(max(len(lk), len(rk)))
   1660 
-> 1661     llab = rizer.factorize(lk)
   1662     rlab = rizer.factorize(rk)
   1663 

pandas/_libs/hashtable.pyx in pandas._libs.hashtable.Factorizer.factorize()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_labels()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable._unique()

TypeError: unhashable type: 'A'
```

というような感じで、`hash`使いたいマンなわけです。同値関係をカスタムできません。
`ExtensionDtype`を使っても、JOINの際にはうまく動いてくれないです。（勘違いならすみません。。。知ってる人がいたら教えてください。。。）
てな訳で、`JOIN`処理もカスタムできるようにして欲しい。

## 2. ExtensionDtypeのメモリ

```python
In [2]: # 'Int64' ExtensionDtype使う場合
         df = pandas.DataFrame({'a': [1,2,3,4,5], 
                                'b': pandas.Series([1,2,3,4,5], dtype='Int64'), 
                                'c': ['aa', 'bb', 'cc', 'dd', 'ee']})
         asizeof.asizeof(df)
Out[2]: 10800

In [3]: # 使わない場合
         df = pandas.DataFrame({'a': [1,2,3,4,5], 
                                'b': pandas.Series([1,2,3,4,5]), 
                                'c': ['aa', 'bb', 'cc', 'dd', 'ee']})
         asizeof.asizeof(df)
Out[3]: 9768

In [4]: # そのそも列がない場合
         df = pandas.DataFrame({'a': [1,2,3,4,5], 
                                'c': ['aa', 'bb', 'cc', 'dd', 'ee']})
         asizeof.asizeof(df)
Out[4]: 8192

In [5]: # str型のc列がない場合
        df = pandas.DataFrame({'a': [1,2,3,4,5]})
        asizeof.asizeof(df)
Out[5]: 5712

In [6]: # 空のデータフレーム
        df = pandas.DataFrame()
        asizeof.asizeof(df)
Out[6]: 2920
```

という感じで、サイズ変化をまとめると、こんな感じ。

| 変数 | サイズ変化 |
| --- | --- |
| 空のデータフレーム | 2920 |
| str型のc列(5要素) | 2792 |
| builtin-int型のb列 | 1576 |
| Int64型のb列 | 2608 |

約1.5倍ほどメモリを使用しています。
理由は、`ExtensionDtype`は、内部的には`Object`型とみなされているから。
仕方ないことなんですけどね、本音を言えばなんとかして欲しい。

# まとめ

#### 重要な新機能

- NaNを保持できるInt64型
- `ExtensionDtype`と`ExtensionArray`でカスタム型対応

#### 重要な修正機能

- パスに~をつけてもOKになった
- タイムゾーンを正しく扱うようになった

##### 今後の期待

- ハッシュベースでないJOIN
- ExtensionDtype型やObject型の省メモリ化

という感じで、`pandas0.24.0`のリリースと今後の期待について書きました。
「お、アップデートしとこ！」と思ってくれた方がいたら、幸いです。

今後も、データ分析ライブラリのリリースをまとめたり、小技をまとめたりする予定です。
これ、誰が読むんやろ。