大きいCSVデータを扱うときに、一時的に中間ファイルに保存して高速にアクセスしたいケースがある。そんなときMessagePackはかなり有効なシリアライザーなのだが、いつの間にかPandasでサポートされていたので確かめてみた。
結論:CSVよりも相当速くなった。
読み書きのやり方
DataFrame→MessagePackへの書き出し
df.to_msgpack()で書き出せる。まだ実験段階なので仕様が変わるかもしれないが、「path_or_buf」の引数がNoneならMessagePackの文字列として返され、引数のパスを指定すればファイルとして保存される。
また、compressの引数に圧縮形式を指定すると圧縮をかけてくれる。現在「zlib」と「blosc」形式がサポートされている。
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の文字列ならパースをするらしい(ここ変わりそうな気がする)。
# 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ケースで「読み、書きにかかる時間」「ファイルサイズ」を測定する。
- CSVで読み書き
- to_pickle / read_pickleで読み書き
- MessagePack(圧縮なし)で読み書き
- MessagePack(zlib圧縮)で読み書き
環境:Python3.6.4, pandas:0.23.4, zlib:1.2.11
対称ファイル:KaggleのThe Movies Datasetのratings.csv(解凍済み676MB)で実験する。ディスクアクセスのオーバーヘッドを減らすため読み書きはSSD環境で行う。
bloscフォーマットはモジュールをインストールしてもエラーが出て圧縮できなかったので省略。
1.CSVで読み書き
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で読み書き
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には任意コードを実行されるという脆弱性がある。使うのは簡単だけど、そこを知った上で使うべき。
3. MessagePack(圧縮なし)で読み書き
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圧縮)で読み書き
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で直に処理するのはない。
-
MessagePackの公式ページより ↩