Python: Parquetフォーマットファイルを入出力する (Pandasとpyarrow)
今回はテーブルデータをParquetファイルで扱う方法について2つ紹介します。
コードは以下の環境で動作確認してます。
% python --version Python 3.8.5 % pip list Package Version --------------- ------- numpy 1.19.1 pandas 1.1.0 pip 20.2 pyarrow 1.0.0 python-dateutil 2.8.1 pytz 2020.1 setuptools 49.2.0 six 1.15.0
Apache Parquet
Apache Parquet1はApacheプロジェクトの1つで、環境に依存しない列指向のファイルフォーマットを定義・メンテナンスしています。
Parquetは以下の特徴を持ちます。詳細は https://parquet.apache.org/documentation/latest/ を参照ください。
- 列指向フォーマットのため、行指向と比較して、圧縮効率や列に対する集計処理などにおいてアドバンテージを持つ
- Google Researchが2010年に発表した Dremel: Interactive Analysis of Web-Scale Datasets のアルゴリズムを実装
- プログラミング言語、CPUアーキテクチャ等に非依存のため、利用できるプラットフォームが豊富
- Google BigQueryやAmazon AthenaなどもデータソースフォーマットとしてParquetを選択可能
- ネストしたカラムもエンコード可能
サポートされるデータ型
Parquetで利用できるデータ型だけ確認しておきます。文字列はBYTE_ARRAYに変換する必要があります。
- BOOLEAN: 1 bit boolean
- INT32: 32 bit signed ints
- INT64: 64 bit signed ints
- INT96: 96 bit signed ints
- FLOAT: IEEE 32-bit floating point values
- DOUBLE: IEEE 64-bit floating point values
- BYTE_ARRAY: arbitrarily long byte arrays.
Pandas DataFrameを用いたParquetファイルの変換
Pandas DataFrameではParquetのファイルを入出力するためのメソッドとして、to_parquetとread_parquetが実装されています。
DataFrameをParqueに保存・ロードする簡単な例を示します。文字列や日付型、NaNを含むデータも難なく変換できてます。
import pandas as pd from datetime import datetime dt = datetime.now() df = pd.DataFrame({ "id": [1, 2, 3], "name": ["Tanaka", "Suzuki", "Sato"], "rating": [3.5, None, 4.2], "created_at": [dt, dt, dt], }) print(df.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 3 entries, 0 to 2 # Data columns (total 4 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 id 3 non-null int64 # 1 name 3 non-null object # 2 rating 2 non-null float64 # 3 created_at 3 non-null datetime64[ns] # dtypes: datetime64[ns](1), float64(1), int64(1), object(1) # memory usage: 224.0+ bytes # Parquetで保存 df.to_parquet("./df.parquet") # Parquetからロード loaded_df = pd.read_parquet("./df.parquet") print(loaded_df.info()) # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 3 entries, 0 to 2 # Data columns (total 4 columns): # # Column Non-Null Count Dtype # --- ------ -------------- ----- # 0 id 3 non-null int64 # 1 name 3 non-null object # 2 rating 2 non-null float64 # 3 created_at 3 non-null datetime64[ns] # dtypes: datetime64[ns](1), float64(1), int64(1), object(1) # memory usage: 224.0+ bytes print(loaded_df) # id name rating created_at # 0 1 Tanaka 3.5 2020-08-15 13:33:42.224807 # 1 2 Suzuki NaN 2020-08-15 13:33:42.224807 # 2 3 Sato 4.2 2020-08-15 13:33:42.224807
Apache Arrow
Apache ArrowもApacheプロジェクトの1つです。こちらはインメモリの列指向データフォーマットを定義し、ライブラリとして提供してます。
Arrowの特徴はこちらです。詳細は https://arrow.apache.org/overview/ を参照ください。
- 同じ列は同じメモリブロックに含まれるようにレイアウトする
- SIMD (Single Instruction, Multiple Data) アーキテクチャのCPUで高速に入出力できる
- プログラミング言語に依存しないフォーマットで、ネストしたデータ型やユーザ定義の型などもサポート
- Arrowを共通のストレージ (= ハブ) とし、Arrowとのシリアライザ・デシリアライザを実装するだけで、他のプログラミング言語やデータソースとのデータのやり取りができる

pyarrowを用いたParquetファイルの変換
このArrowのPython実装ライブラリの1つがpyarrowです。各種ファイルフォーマットやDataFrameなどに対応しており、例えば、CSVからParquet、ParquetからDataFrameといった変換もpyarrowを仲介することで可能となります。
pip install pyarrowでインストールできます- 内部的にはpyarrow.Tableオブジェクトとして扱われます
CSVファイル -> Arrowテーブル -> Parquetファイル -> Arrowテーブル -> DataFrameオブジェクト という変換を行います。
# 上の続き import pyarrow import pyarrow.parquet import pyarrow.csv df.to_csv("test.csv", index=False) # CSVファイルをArrow形式でロード loaded_table = pyarrow.csv.read_csv("./test.csv") print(loaded_table) # pyarrow.Table # id: int64 # name: string # rating: double # created_at: string # Parquetに変換して保存 pyarrow.parquet.write_table(loaded_table, "./test.parquet") # ParquetファイルをArrow形式でロード loaded_parquet = pyarrow.parquet.read_table("./test.parquet") print(loaded_parquet) # pyarrow.Table # id: int64 # name: string # rating: double # created_at: string # ArrowをDataFrameへ変換 loaded_df = loaded_parquet.to_pandas() print(loaded_df) # id name rating created_at # 0 1 Tanaka 3.5 2020-08-15 14:15:08.543007 # 1 2 Suzuki NaN 2020-08-15 14:15:08.543007 # 2 3 Sato 4.2 2020-08-15 14:15:08.543007
まとめ
今回はParquetファイルをPythonで入出力するための方法を2つ紹介しました。
-
/ˈpɑːki,ˈpɑːkeɪ/ (パーキ、パーケイ) と読むそうです。↩
Python: poetryでパッケージの依存管理
私はこれまでPythonのパッケージ管理として pyenv + pipenv を主に使ってきました。が、最近はpipenvは色々あって使いづらさを感じていました。
pipenv lockやpipenv syncが遅い (気がする)- pipenv自体の更新が怪しかった (参考、今年に入って4月と6月にリリースされている模様)
乗り換える程の理由でもないのですが、代替となるツールは探しておかないとなあとふんわり思っていた頃、同僚がpoetryを使っていて良さそうでしたので、使い方をまとめながら紹介したいと思います。
poetry
poetryは、主にパッケージ依存関係の解決・インストール・更新と仮想環境の構築を行ってくれるコマンドラインツールです。上の通りpipenvなどが競合するツールになります。
インストール
pipで入れてしまうのが一番手軽です。
$ python --version Python 3.8.5 $ pip install poetry
プロジェクト作成
poetry newでpoetryプロジェクトの標準的なディレクトリ構成が作られます。
- 既存のプロジェクトにpoetryで依存関係を管理する場合、newの代わりに
poetry initを使います。
$ poetry new poetry-example
Created package poetry_example in poetry-example
$ cd poetry-example
$ tree
.
├── README.rst
├── poetry_example
│ └── __init__.py
├── pyproject.toml
└── tests
├── __init__.py
└── test_poetry_example.py
このとき作成されるpyproject.tomlに追加するパッケージを記述していきます。
[tool.poetry] name = "poetry-example" version = "0.1.0" description = "" authors = ["ohke <...>"] [tool.poetry.dependencies] python = "^3.8" [tool.poetry.dev-dependencies] pytest = "^5.2" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api"
仮想環境の構築
poetry install または poetry update とします。そうすると仮想環境 (virtualenv) が構築され、パッケージがインストールされます。
- デフォルトではdev-dependenciesのパッケージもインストールされます (
--no-devオプションで除くこともできます)
$ poetry install Creating virtualenv poetry-example-JGBVk9r8-py3.8 in /Users/ohke/Library/Caches/pypoetry/virtualenvs Updating dependencies Resolving dependencies... (0.2s) Writing lock file Package operations: 11 installs, 0 updates, 0 removals - Installing zipp (3.1.0) - Installing importlib-metadata (1.7.0) - Installing pyparsing (2.4.7) - Installing six (1.15.0) - Installing attrs (19.3.0) - Installing more-itertools (8.4.0) - Installing packaging (20.4) - Installing pluggy (0.13.1) - Installing py (1.9.0) - Installing wcwidth (0.2.5) - Installing pytest (5.4.3) - Installing poetry-example (0.1.0)
生成されるpoetry.lockファイルに依存パッケージが出力されます。このあたりはpyenvと同じですね。
$ tree
.
├── README.rst
├── poetry.lock # ★
├── poetry_example
│ └── __init__.py
├── poetry_example.egg-info
│ ├── PKG-INFO
│ ├── SOURCES.txt
│ ├── dependency_links.txt
│ └── top_level.txt
├── pyproject.toml
└── tests
├── __init__.py
└── test_poetry_example.py
デフォルトではユーザホーム以下のキャッシュディレクトリに作成されますが、poetry configで設定を書き換えることでカレントディレクトリに作ることもできます。以下では./.venvに作られています。
# 上で作った環境を削除
$ rm -rf /Users/ohke/Library/Caches/pypoetry/virtualenvs/poetry-example-JGBVk9r8-py3.8
$ poetry config --list
cache-dir = "/Users/ohke/Library/Caches/pypoetry"
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs" # /Users/ohke/Library/Caches/pypoetry/virtualenvs
$ poetry config virtualenvs.in-project true
$ poetry install
Creating virtualenv poetry-example in /Users/ohke/dev/private/poetry-example/.venv
...
仮想環境上での実行
poetry runで仮想環境でPythonコードを実行できます。上でインストールした仮想環境のパッケージがパスに含まれていることが確認できます。
poetry shellで仮想環境上でシェルが立ち上がります
$ cat poetry_example/main.py
import sys
from pprint import pprint
if __name__ == "__main__":
pprint(sys.path)
$ poetry run python poetry_example/main.py
['/Users/ohke/dev/private/poetry-example/poetry_example',
...
'/Users/ohke/dev/private/poetry-example/.venv/lib/python3.8/site-packages',
...]
パッケージのインストール
パッケージを追加する場合、poetry addを使います。これでpyproject.tomlとpoetry.lockが更新されます。
- pyproject.tomlを直接書き換えた場合は、
poetry updateで poetry.lockの更新 + 仮想環境へのインストール を行います - パッケージをアンインストールする場合は、
poetry remove
$ poetry add numpy Using version ^1.19.1 for numpy Updating dependencies Resolving dependencies... (1.6s) Writing lock file Package operations: 1 install, 0 updates, 0 removals - Installing numpy (1.19.1)
まとめ
概ねpipenvと変わらない機能を有することは確認できました。lockやsyncがどれだけ早くなるのか次第なところはありますが、現在pipenvを使っているプロジェクトを移行するまでのメリットは無さそうです。
去年12月に1.0がリリースされて以来、現在までバージョンアップも頻繁に行われているので、新しく作るシステムに関してはpoetryを使っていくのがいいのかな、という所感でした。
Python: 安全・手軽に一時ファイル・一時ディレクトリを作る (tempfile)
一時的に使うファイルやディレクトリを作成して、処理が終わったら削除する、という手続きを実装する機会はしばしばあると思います。
簡単なことではあるのですが、処理の例外ハンドラで削除の実装を忘れてゴミファイルができてしまったり、並列実行したときにプロセス間でファイルパスを競合させてしまったりと、ついハマる落とし穴もあります。
Pythonではこういった一時ファイルを安全・手軽に作成するための標準ライブラリとして tempfile が提供されています。
$ python --version Python 3.8.5
一時ファイル
使い方は通常のファイル操作同様シンプルで、open関数の代わりにTemporaryFileオブジェクトを介して読み書きする点だけが異なります。
- デフォルトモードは "w+b" (mode引数で変更可能)
- dir引数の指定が無い場合、環境変数TMPDIR / TEMP / TMPのディレクトリ内にこのファイルは生成されます
ポイントは2つで、これによって削除忘れやファイルパス競合を意識する必要がなくなります。
- closeメソッドを呼び出す or with句内から抜け出す (or ガベージコレクト対象となる) ことで、ファイルも自動的に削除されます
- TemporaryFileは内部でmkstempを実行しており、重複しないようにランダムな文字列がファイル名に付与されます
import tempfile f = tempfile.TemporaryFile() f.write(b"Hello, world!") print(f.read()) f.close() # dir引数で出力先ディレクトリを指定できる with tempfile.TemporaryFile(dir=".") as f: f.write(b"Hello, world!") f.seek(0) print(f.read())
TemporaryFileは不可視のファイルが生成しますが、NamedTempraryFileでは可視ファイルを生成できます。
f = tempfile.NamedTemporaryFile(prefix="a_", suffix="_b", dir=".") f.write(b"Hello, world!") # 出力された一時ファイルの確認 from glob import glob print(glob("./a_*_b")[0]) # ./a_z91g49js_b
これら2つに加えて、指定のサイズとなるまでメモリにスプールするSpooledTemporaryFileというのもあります。
一時ディレクトリ
一時ディレクトリはTemporaryDirectoryで作成できます。以下のように、コンテキストマネージャ (with句) 内だけでアクセスできるディレクトリが生成され、抜け出すときには削除されます。
import os with tempfile.TemporaryDirectory(prefix="tmp_", dir=".") as dirpath: print(dirpath) # ./tmp_xo0tg2u1 with open(os.path.join(dirpath, "a.txt"), "w") as f: f.write("Hello, world!") print(glob(os.path.join(dirpath, "*"))) # ['./tmp_xo0tg2u1/a.txt'] print(glob("./tmp_*")) # [] (削除済み)