概要
parquetの読み書きをする用事があったので、PyArrowで実行してみる。
PyArrowの類似のライブラリとしてfastparquetがありこちらの方がpandasとシームレスで若干書きやすい気がするけど、PySparkユーザーなので気分的にPyArrowを選択。
バージョン情報
- Python 3.6.2
- pyarrow==0.11.1
導入
condaを使う
$ conda install pyarrow
parquetについて
ORCと並んで挙げられるカラムナ(列指向)のファイルフォーマット。
情報を行ごとではなく列ごとに持つのでhiveとかSparkで指定行のみSELECTする際に行指向と比べてパフォーマンスが良くなったり、同一列には似た文字列が入っていることが多いので圧縮率が良くなったりする。
ローカルで使う場合もpandasのDataFrameの中から一部のカラムだけ読んで整形するような用事でご利益がある。
parquetファイルの出力
nameとageの2つのカラムを持つpandasのDataFrameを作って、それをPyArrowのTableに変換して、ファイルの出力する例。
import pyarrow as pa import pyarrow.parquet import pandas as pd # 保存用のpandasのDataFrameを作る data = [ {'name': 'tanaka', 'age': 32}, {'name': 'sato', 'age': 28}, {'name': 'yokoyama', 'age': 24} ] df = pd.DataFrame(data, columns=['name', 'age']) # tableに変換して保存する table = pa.Table.from_pandas(df) pa.parquet.write_table(table, 'users.parq')
生成されたファイル(users.parq)をviで開くと割と読める形式になっている。
pandas0.21.0以降は to_parquetが用意されているのでDataFrameから直接出力できる。
df.to_parquet('user.parq')
parquetファイルの出力(圧縮指定あり)
parquetではデフォルトでSnappyで圧縮される。これ以外にも下記のような圧縮指定が可能。
compression : str or dict
Specify the compression codec, either on a general basis or per-column.
Valid values: {'NONE', 'SNAPPY', 'GZIP', 'LZO', 'BROTLI', 'LZ4', 'ZSTD'}
試しに1万行のDataFrameを作って、各圧縮形式で出力してみる。
圧縮用の文字列データを生成する為に、mecabの辞書からNoun.csvを持ってきて1列目だけ取っておく。
$ cut -f1 -d',' Noun.csv > Noun.txt $ head -5 Noun.txt 仕舞い 綺 洋裁 組打ち 畿内
約6万ワードが入っているので、これをランダムで3つ組み合わせて擬似文字列を作る。
import numpy as np with open('Noun.txt') as f: noun_list = [line.strip() for line in f] random_strings = [''.join(rs) for rs in np.random.choice(noun_list, [10000, 3])] random_strings[0:3] #=> ['うつせみ掛り切り天測', '雲脂カーストムッシュー', '金盞花白瓜西暦']
なにやら意味不明な文字列が生成された。
本題から逸れるけどこの手の文字列のランダム生成は意外と面白い言葉ができることが多い。
今回生成された中で気に入ったフレーズは下記あたり。
ちょん髷裁ち切れ風説 ヒラメシガー継父 上目遣いクックブック気病み 呆然マッコリフローティング 馬の脚雲水ボディーガード
作家の人がキャラクター作る時にこういうの見てインスピレーションが降りてくるのを待つ用途で使える気がする。ヒラメシガー継父ってどんなキャラだろう。
話戻して、これをDataFrameに入れて圧縮形式を指定して保存してみる。LZOはcodec not implementedと言われたので割愛。
random_int = np.random.randint(low=0, high=100, size=10000) df = pd.DataFrame({'text': random_strings, 'int': random_int}) table = pa.Table.from_pandas(df) for comp in ['NONE', 'SNAPPY', 'GZIP', 'BROTLI', 'LZ4', 'ZSTD']: pa.parquet.write_table(table, 'users_%s_.parq' % comp, compression=comp)
実行結果(速度順)
圧縮形式 | ファイルサイズ | 実行時間(msec) |
---|---|---|
NONE | 415K | 6.86 |
LZ4 | 328K | 12.02 |
SNAPPY | 320K | 12.69 |
ZSTD | 240K | 14.89 |
GZIP | 215K | 60.53 |
BROTLI | 191K | 64.33 |
実行時間は10回の平均を取っているけどけっこうバラつきがあったので参考程度に。
panasでto_parquetする場合も同様に処理できる。
for comp in ['NONE', 'SNAPPY', 'GZIP', 'BROTLI', 'LZ4', 'ZSTD']: df.to_parquet('users_%s_.parq' % comp, compression=comp)
parquetファイルの読込み
read_tableで出力したファイルを読み込む。
table = pa.parquet.read_table('text_ZSTD_.parq')
df = table.to_pandas()
user_threadにTrueを指定するとマルチスレッドで読み込む。
table = pa.parquet.read_table('text_ZSTD_.parq', use_threads=True)
columnsを指定すると、指定したカラムのみ読み込む。この機能が便利。
table = pa.parquet.read_table('text_ZSTD_.parq', columns=['text'])
pandasで読み込む場合はpd.read_parquet。
df = pd.read_parquet('users_SNAPPY_.parq')
2147483646 bytes,を超えるデータを扱う場合
pyarrowで大きめのparquetファイルをreadすると下記のようなエラーになる。
ArrowIOError: Arrow error: Capacity error: BinaryArray cannot contain more than 2147483646 bytes
最新のバージョンでは直っているらしい(0.13.0で直っていることを確認)。
https://github.com/apache/arrow/pull/3171
古いバージョンの場合はfastparquetを使って回避することも可能。
$ conda install fastparquet
df = pd.read_parquet('big_file', engine='fastparquet')
改定履歴
Author: Masato Watanabe, Date: 2019-06-12, 巨大ファイルread時のエラーについて追記 Author: Masato Watanabe, Date: 2019-06-12, pandasでのread/writeの記述を追加 Author: Masato Watanabe, Date: 2019-01-23, 記事投稿