PandasのDataFrameをMessagePackに簡単に読み書きできるようになっていた件

大きいCSVデータを扱うときに、一時的に中間ファイルに保存して高速にアクセスしたいケースがある。そんなとき[MessagePack](https://msgpack.org/)はかなり有効なシリアライザーなのだが、いつの間にかPandasでサポートされていたので確かめてみた。

結論：**CSVよりも相当速くなった**。

## 読み書きのやり方
### DataFrame→MessagePackへの書き出し
**df.to_msgpack()**で書き出せる。まだ実験段階なので仕様が変わるかもしれないが、「path_or_buf」の引数がNoneならMessagePackの文字列として返され、引数のパスを指定すればファイルとして保存される。

また、compressの引数に圧縮形式を指定すると圧縮をかけてくれる。現在「zlib」と「blosc」形式がサポートされている。

```python
import pandas as pd

# DataFrameからMessagePack(path_or_buf=Noneなら文字列として返される)
msgpack_str = df.to_msgpack(None)

# DataFrameからMessagePackのファイルに保存
df.to_msgpack("df.bin")

# DataFrameからMessagePackのファイルに保存（zlibで圧縮）
df.to_msgpack("df_zlib.bin", compress="zlib")
```

詳細：https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_msgpack.html

### MessagePack→DataFrameへの読み込み
csvと同じ要領で、**pd.read_msgpack()**で読み込める。まだ実験段階なので仕様が変わるかもしれないが、引数のpathがファイルパスならファイルから読み込み、MessagePackの文字列ならパースをするらしい（ここ変わりそうな気がする）。

```python
# MessagePackの文字列からDataFrame
df_read = pd.read_msgpack(msgpack_str)

# MessagePackのファイルからDataFrameに読み込み
df_read = pd.read_msgpack("df.bin")

# zlibで圧縮したMessagePackのファイルからDataFrameに読み込み（圧縮形式は指定しなくてよい）
df_read = pd.read_msgpack("df_zlib.bin")
```

事前に圧縮をしていても勝手に形式を判断してくれる。

詳細：https://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_msgpack.html

## 速度、ファイルサイズの比較
以下の4ケースで「読み、書きにかかる時間」「ファイルサイズ」を測定する。

1. CSVで読み書き
2. to_pickle / read_pickleで読み書き
3. MessagePack（圧縮なし）で読み書き
4. MessagePack（zlib圧縮）で読み書き

環境：Python3.6.4, pandas:0.23.4, zlib:1.2.11 

対称ファイル：[KaggleのThe Movies Dataset](https://www.kaggle.com/rounakbanik/the-movies-dataset)のratings.csv（解凍済み**676MB**）で実験する。ディスクアクセスのオーバーヘッドを減らすため読み書きはSSD環境で行う。

bloscフォーマットはモジュールをインストールしてもエラーが出て圧縮できなかったので省略。

### 1.CSVで読み書き

```python
import pandas as pd
import time

source_dir = "F:/"

def read_csv():
    return pd.read_csv(source_dir+"ratings.csv")

def test_csv():
    df = read_csv()
    start_time = time.time()
    df.to_csv(source_dir+"ratings_out.csv")
    print("write csv [s]", time.time() - start_time)

    start_time = time.time()
    df = pd.read_csv(source_dir+"ratings_out.csv")
    print("read csv [s]", time.time() - start_time)
```

**write csv [s] 141.75099992752075
read csv [s] 21.151185989379883
ファイルサイズ：889MB**

書き込みで2分オーバー。正直CSVのままやり取りするのはつらい。ファイルサイズが増えているのはエンコードがUTF-8に変わったせいだろう。

###2. to_pickle / read_pickleで読み書き
```python
def test_pickle():
    df = read_csv()
    start_time = time.time()
    df.to_pickle(source_dir+"pickle.bin")
    print("write pickle [s]", time.time() - start_time)

    start_time = time.time()
    df = pd.read_pickle(source_dir+"pickle.bin")
    print("read pickle [s]", time.time() - start_time)
```

**write pickle [s] 3.3524258136749268
read pickle [s] 1.1086409091949463
ファイルサイズ：794MB**

CSVに比べると相当速い。しかし、PythonのPickleには[任意コードを実行されるという脆弱性がある](https://qiita.com/tanuk1647/items/4c2a305c7cc4e12ef99d)。使うのは簡単だけど、そこを知った上で使うべき。

### 3. MessagePack（圧縮なし）で読み書き
```python
def test_raw_msgpack():
    df = read_csv()
    start_time = time.time()
    df.to_msgpack(source_dir+"msgpack.bin")
    print("write msgpack [s]", time.time() - start_time)

    start_time = time.time()
    df = pd.read_msgpack(source_dir+"msgpack.bin")
    print("read msgpack [s]", time.time() - start_time)
```

**write msgpack [s] 3.6599645614624023
read msgpack [s] 2.516319513320923
ファイルサイズ：794MB**

Pickleよりは若干遅いものの、こちらはMessagePackのライブラリがある言語で読み込むことができる。ファイルサイズが一緒なので、内部的なシリアライズ方式はPickleと似ているのかもしれない。しかし、脆弱性の点からはMessagePackのほうが安全な（はず）。

### 4. MessagePack（zlib圧縮）で読み書き
```python
def test_zlib_msgpack():
    df = read_csv()
    start_time = time.time()
    df.to_msgpack(source_dir+"msgpack_zlib.bin", compress="zlib")
    print("write msgpack-zlib [s]", time.time() - start_time)

    start_time = time.time()
    df = pd.read_msgpack(source_dir+"msgpack_zlib.bin")
    print("read msgpack-zlib [s]", time.time() - start_time)
```

**write msgpack-zlib [s] 29.289719343185425
read msgpack-zlib [s] 3.1884050369262695
ファイルサイズ：124MB**

Zlib圧縮をかけるため書き込みに時間がかかるが、読み込みはそこまで遅くなく、非対称な圧縮なのでこれで十分ではないだろうか。データ分析の場合、書き込みよりも読み込みの頻度のほうが多いと思われるので。

### まとめ

| 形式             | 書き込み[s] | 読み込み[s] | 容量[MB] |
|------------------|------------:|------------:|---------:|
| CSV              |       141.8 |        21.2 |      889 |
| Pickle           |         **3.4** |         **1.1** |      794 |
| MessagePack      |         3.7 |         2.5 |      794 |
| MessagePack+zlib |        29.3 |         3.2 |      **124** |

速さ重視ならPickleだが、脆弱性があるのでオレオレシリアライズ専用かもしれない。その点からはMessagePackは安心して使えて、Redisでもサポートされている[^1]。事実MessagePackとPickleはほとんど差がない（特に容量が）。**容量削りたければMessagePack+zlibでほぼFA**では？という感じはする。他に無圧縮MessagePackでかけて、別の軽い圧縮（LZ4とか）をかけるのでも良さそうな気はする。とりあえず**CSVで直に処理するのはない**。

[^1]: MessagePackの公式ページより