こんにちは。ウォンテッドリーでデータサイエンティストをしている角川(@nogawanogawa)です。
機械学習関係の開発をしていると比較的大きなテーブルデータを扱うこともあり、こうしたテーブルデータの保存が必要になることもあるかと思います。一方で保存コストなどを考えるとファイルサイズは小さいに越したことはありません。
今回はParquetフォーマットを利用してどれだけファイルサイズを小さくできるか試してみた事例についてご紹介したいと思います。
Parquet
Apache Parquetは、大量のデータを効率よく保存・分析するために作られたオープンソースのファイル形式です。

Parquet
一般的なCSVなどの行指向のファイルでは行ごとにデータが保存される一方、Parquetは列ごとにデータをまとめて保存する列指向という方式を採用しています。
テーブルデータの同じ列には似た傾向の値が含まれやすく、このような状況においてはParquetは圧縮の効きが良くファイルサイズを抑えつつ読み込み速度も向上させられます。
エンコーディング
Parquetの公式ドキュメントによると、エンコーディングの方式として7種類がサポートされています。
Encodings
- Plain
- Dictionary Encoding
- Run Length Encoding / Bit-Packing Hybrid
- Delta Encoding
- Delta-length byte array
- Delta Strings
- Byte Stream Split
今回はこのうちRun Length Encoding (RLE)について着目します。
テーブルデータでは列の中に同じ値が登場することは珍しくなく、数億行のテーブルであっても登場する値は数百種類にとどまることもあります。そこで、表データの特定の列について着目したとき、同じ値が繰り返し登場するような状況を考えてみると、値自体を保存するのではなく辞書とキーを用いて表現することでコンパクトに表現出来ます。また、ソートされていて同じ値が連続して登場する場合にはキーが何回繰り返されるかを記録することでさらにコンパクトに表現することが出来ます。
こうした場合にはRLEによってファイルサイズが小さくなることがあります。
圧縮
また、Parquetでは圧縮率と処理コストの異なる範囲をカバーする複数の圧縮方式をサポートしています。Read/Writeの速度などにそれぞれ特徴があり、8種類サポートされています。
Compression
- UNCOMPRESSED
- SNAPPY
- GZIP
- LZO
- BROTLI
- LZ4
- ZSTD
- LZ4_RAW
それぞれRead/Writeの速度や圧縮率など特色があるのですが、今回は可能な限りすべて試してみようと思います。
Polarsを用いて実験する
今回は私が普段使用している特徴量のデータを使って実験してみようと思います。特徴量は先に示したように巨大なテーブルになることがあり、イメージは下記のような形式になっています。
今回はPolarsを使って実際にどれくらい圧縮が可能なのか試してみようと思います。PolarsではEncodingはRLE_DICTIONARYがRead/Write両方サポートされています。
Implementation status
また、圧縮については下記の5種類がRead/Write両方サポートされています。
- BROTLI
- GZIP
- LZ4
- SNAPPY
- ZSTD
実装
実際の機械学習ジョブの中間生成物を使って検証してみようと思います。デフォルトは何もしていないcsvとし、それをParquetで単純に保存するだけのコードを作成します。
今回、sortによってRLEが適用されてデータサイズが削減されることを確認したいので、ファイルにあるid列でsortしたものについても保存してみます。
さらにwrite_parquetできる圧縮方式をすべて試して比較してみようと思います。
実際に使用したコードは下記のようなものになっています。
import polars as pl
INPUT = "default.csv"
df = pl.read_csv(INPUT, infer_schema_length=10000)
# 未ソート版とソート済み版の両方を出力する。
VARIANTS = {
"": df, # 未ソート -> default_<comp>.parquet
"sorted_": df.sort("id"), # id ソート済み -> default_sorted_<comp>.parquet
}
# Polars が書き込みに対応している parquet の圧縮方式を試す。
COMPRESSIONS = ["uncompressed", "snappy", "gzip", "lz4", "zstd", "brotli"]
for prefix, frame in VARIANTS.items():
for comp in COMPRESSIONS:
out = f"default_{prefix}{comp}.parquet"
frame.write_parquet(out, compression=comp)
結果
保存したファイルサイズは下記の様になりました。
オリジナルのcsvファイルに対して、Parquetで保存するだけでも10%ほどファイルサイズが小さくなっています。Parquetの中でも圧縮方式を指定する場合には、そこからさらに1/3 ~ 1/4ほどにファイルサイズは小さくなることが確認できました。
また、すべてのParquetのケースでsortした場合にファイルサイズが小さくなるという結果になりました。今回のケースではgzipで圧縮されているParquetにおいて最終的にもっとも小さくなり、ソートするだけで29.2%ファイルサイズが小さくなっていました。
まとめ
今回はParquetのファイルサイズを小さくする実験について紹介しました。普段使っているParquetであっても、圧縮方式の指定に加えて、事前にソートするだけでもファイルサイズが小さくなることがわかり個人的に面白かったです。ソートするだけでファイルサイズが小さくなるので、手軽にできるデータサイズの圧縮テクニックだと感じました。
ウォンテッドリーでは、ユーザーにとってより良い推薦を届けるために日々開発を行っています。ユーザーファーストの推薦システムを作ることに興味があるという方は、下の募集の「話を聞きに行きたい」ボタンから気軽に話を聞きに来ていただけるとうれしいです!