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つで、環境に依存しない列指向のファイルフォーマットを定義・メンテナンスしています。

github.com

Parquetは以下の特徴を持ちます。詳細は https://parquet.apache.org/documentation/latest/ を参照ください。

  • 列指向フォーマットのため、行指向と比較して、圧縮効率や列に対する集計処理などにおいてアドバンテージを持つ
  • プログラミング言語、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.apache.org

Arrowの特徴はこちらです。詳細は https://arrow.apache.org/overview/ を参照ください。

  • 同じ列は同じメモリブロックに含まれるようにレイアウトする
    • SIMD (Single Instruction, Multiple Data) アーキテクチャのCPUで高速に入出力できる
  • プログラミング言語に依存しないフォーマットで、ネストしたデータ型やユーザ定義の型などもサポート
    • Arrowを共通のストレージ (= ハブ) とし、Arrowとのシリアライザ・デシリアライザを実装するだけで、他のプログラミング言語やデータソースとのデータのやり取りができる

f:id:ohke:20200815120437p:plain

pyarrowを用いたParquetファイルの変換

このArrowのPython実装ライブラリの1つがpyarrowです。各種ファイルフォーマットやDataFrameなどに対応しており、例えば、CSVからParquet、ParquetからDataFrameといった変換もpyarrowを仲介することで可能となります。

  • pip install pyarrow でインストールできます
  • 内部的にはpyarrow.Tableオブジェクトとして扱われます

arrow.apache.org

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つ紹介しました。


  1. /ˈpɑːki,ˈpɑːkeɪ/ (パーキ、パーケイ) と読むそうです。

Python: poetryでパッケージの依存管理

私はこれまでPythonのパッケージ管理として pyenv + pipenv を主に使ってきました。が、最近はpipenvは色々あって使いづらさを感じていました。

  • pipenv lockpipenv syncが遅い (気がする)
  • pipenv自体の更新が怪しかった (参考、今年に入って4月と6月にリリースされている模様)

乗り換える程の理由でもないのですが、代替となるツールは探しておかないとなあとふんわり思っていた頃、同僚がpoetryを使っていて良さそうでしたので、使い方をまとめながら紹介したいと思います。

poetry

poetryは、主にパッケージ依存関係の解決・インストール・更新と仮想環境の構築を行ってくれるコマンドラインツールです。上の通りpipenvなどが競合するツールになります。

python-poetry.org

github.com

インストール

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 が提供されています。

docs.python.org

$ 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_*"))  # [] (削除済み)