iMind Developers Blog

iMind開発者ブログ

Pythonでparquetの読み書き

概要

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, 記事投稿