はじめに
これはPython Advent Calendar 2022の23日目の記事です。pandasのDataFrameやSeriesの設計・生成を、Pythonの標準ライブラリのデータクラスを使って簡単にできるようにするpandas-dataclassesを紹介します。詳しい使い方はドキュメンテーションに書かれていますがこれは英語なので、日本語の解説記事ということで書いておきたいと思います(が、全ての内容は網羅できていません…)。
ひとこと(ひとコード?)でまとめると、DataFrameやSeriesの設計がこんな感じでできるようになりますよ、というものです。
@dataclass
class Weather(AsFrame):
year: Index[int] # 測定年
month: Index[int] # 測定月
temp: Data[float] # 月平均気温
wind: Data[float] # 月平均風速
pandas-dataclassesはPythonパッケージとして公開されています。最新版(記事公開時点ではv0.11.0)はPython 3.8-3.11に対応します。pipでインストールできますのでお気軽にお試しください!
簡単な紹介(と開発の動機)
pandasは系列データの解析に広く使われるパッケージですので、決まった仕様のDataFrameやSeriesをデータ解析の入出力とすると便利な場面が多くあります。例えば、以下のような仕様表の気象情報のDataFrameを設計することを考えてみましょう。
名前 | データ種別 | データ型 | 説明 |
---|---|---|---|
year | インデックス | int | 測定年 |
month | インデックス | int | 測定月 |
temp | データ | float | 月平均温度 |
wind | データ | float | 月平均風速 |
実際のDataFrameはこんな感じになるはずです。
temp wind
year month
2020 1 7.1 2.4
7 24.3 3.1
2021 1 5.4 2.3
7 25.9 2.4
2022 1 4.9 2.6
このDataFrameをpandasで生成する場合、各要素のデータ種別とデータ型を指定する必要がありますので、例えば以下のような関数を書くことになります。今回の例では4要素なのでそこまで複雑ではないですが、要素が多い場合はコードの可読性と保守性が悪化することが容易に想像できます。
import numpy as np
import pandas as pd
def get_frame(year, month, temp, wind):
return pd.DataFrame(
index=pd.MultiIndex.from_arrays(
[
np.array(year, dtype=int),
np.array(month, dtype=int),
],
names=["year", "month"],
),
data={
"temp": np.array(temp, dtype=float),
"wind": np.array(wind, dtype=float),
},
)
df = get_frame(
[2020, 2020, 2021, 2021, 2022],
[1, 7, 1, 7, 1],
[7.1, 24.3, 5.4, 25.9, 4.9],
[2.4, 3.1, 2.3, 2.4, 2.6],
)
そこで、仕様表の見た目となるべく同じような形でコードを書けるようにしたのが、今回紹介するpandas-dataclassesです。専用の型ヒント(Data
, Index
, ...)を提供することで、Pythonの標準ライブラリのデータクラスを使って各要素の仕様を定義できるのがこのパッケージの売りです。関数による実装は、以下のようにデータクラスの実装に書き換えることができます。
from dataclasses import dataclass
from pandas_dataclasses import AsFrame, Data, Index
@dataclass
class Weather(AsFrame):
year: Index[int] # 測定年
month: Index[int] # 測定月
temp: Data[float] # 月平均気温
wind: Data[float] # 月平均風速
df = Weather.new(
[2020, 2020, 2021, 2021, 2022],
[1, 7, 1, 7, 1],
[7.1, 24.3, 5.4, 25.9, 4.9],
[2.4, 3.1, 2.3, 2.4, 2.6],
)
ここで、AsFrame
はDataFrame生成のためのクラスメソッドnew
を提供しています。new
はデータクラスの__init__
と同じ引数を取るので、Weather(...)
をWeather.new(...)
と置き換えるだけでOKです。このように、データクラスのコードが、仕様表と同じ感覚で読めることが実感できるかと思います。Weather(AsFrame)
やdf = Weather.new(...)
という部分も、英語として違和感なく読めるのではないでしょうか?このように、仕様の設計のしやすさやコードの読みやすさ、そして後で紹介しますが型チェックの観点から、このパッケージに興味を持って使ってもらえると嬉しいです。
詳細の解説
一般的な要素名への対応
データクラスを使うことの問題点の一つとして、要素名にスペースなどの一般的な文字列が使えないことが挙げられます。pandas-dataclassesでは、変数アノテーション(PEP 593)を指定することでこの問題を解決しています。詳しくは、ドキュメンテーションの命名ルールをご参照ください。
from typing import Annotated as Ann # Python 3.9以上の場合
from typing_extensions import Annotated as Ann # Python 3.8の場合
@dataclass
class Weather(AsFrame):
year: Ann[Index[int], "Year"]
month: Ann[Index[int], "Month"]
temp: Ann[Data[float], "Temperature (deg C)"]
wind: Ann[Data[float], "Wind speed (m/s)"]
Temperature (deg C) Wind speed (m/s)
Year Month
2020 1 7.1 2.4
7 24.3 3.1
2021 1 5.4 2.3
7 25.9 2.4
2022 1 4.9 2.6
動作原理
以下の内容はとりあえず使ってみる分には不要です。必要に応じてご確認ください。
pandas-dataclassesでは以下の2ステップでDataFrameやSeriesを生成しています。
- ユーザが定義したデータクラスのインスタンスを生成する
- 生成したインスタンスをDataFrameまたはSeriesに変換する
上の例ではdf = Weather.new(...)
でDataFrameを生成していますが、これは以下のコードと等価です。
from pandas_dataclasses import asframe
obj = Weather(
[2020, 2020, 2021, 2021, 2022],
[1, 7, 1, 7, 1],
[7.1, 24.3, 5.4, 25.9, 4.9],
[2.4, 3.1, 2.3, 2.4, 2.6],
)
df = asframe(obj)
ここで、asframe
はデータクラスのインスタンスをDataFrameに変換する関数です。データクラスに詳しい方であれば、これはasdict
やastuple
のpandas版であることが分かるかと思います。ここで重要なのは、パッケージは最初のステップでインスタンスの生成に一切関与しないということです。これにより、データクラスの諸機能、例えば__post_init__
による初期化後の任意の処理などをフル活用することができます。
asframe
は、インスタンスから(__dataclass_fields__
に格納された)型ヒントを取り出し、各要素のデータ種別やデータ型などの情報を元にDataFrameを生成します。専用の型ヒントは、現時点では以下の4つが提供されています。詳しくはドキュメンテーションの型指定ルールをご参照ください。
型ヒント | 機能 |
---|---|
pandas_dataclasses.Data[T] |
対応する要素をデータ型T で型変換したのち、DataFrameまたはSeriesのデータに渡す。Seriesの場合、2番目以降の要素は存在していても無視される。Data[typing.Any] の場合は型変換されない(dtype=None に対応)。 |
pandas_dataclasses.Index[T] |
対応する要素をデータ型T で型変換したのち、DataFrameまたはSeriesのインデックスに渡す。要素が複数の場合は階層的インデックスになる。Index[typing.Any] の場合は型変換されない(dtype=None に対応)。 |
pandas_dataclasses.Attr[T] |
対応する要素をDataFrameまたはSeriesのアトリビュート(attrs )に渡す。T による型変換は行われないことに注意(型チェックにのみ使われる)。 |
pandas_dataclasses.Column[T] |
カラムの名前の指定に使われる。ここでは詳しく触れないため、ドキュメンテーションの階層的カラムをご参照ください。 |
静的型チェック
以下の内容はとりあえず使ってみる分には不要です。必要に応じてご確認ください。
データクラスを使うメリットの一つは、最近のPythonで利用が進みつつある型ヒントによる静的型付けの恩恵を受けられることです。例えば、mypy・Pyright・Pylanceなどの多くの静的型チェッカーでは、データクラスのインスタンス生成時に引数の一覧を表示してくれますし、引数の型が間違っていれば指摘してくれます。pandas-dataclassesではこれらの静的型チェッカーをサポートし、以下の3つの型チェック関連の機能を提供しています。
- 専用の型ヒントによる要素の静的型チェック
- Mixinクラスによるクラスメソッド
new
の出力型指定 - クラスメソッド
new
呼び出し時の引数の一覧表示
1つ目ですが、専用の型ヒントはそれと等価な(組み込み)型のエイリアスになっています。対応関係は以下の表の通りです。これにより、例えばData[int]
のはずの要素に[1.0, 2.0, ...]
を渡した場合に型チェッカーがエラーを出します。もう少し柔軟な運用の場合、例えば後で強制的に型変換でintにしてしまうからfloatが渡されてもOKなケースなどは、Data[int | float]
と指定することができます。この場合、最初に書かれたintが実行時のデータ型になります。同様に、Data[int] | None
(__post_init__
で処理する場合)など、様々な型ヒントの書き方を許容しつつデータ型を指定することができます。詳しくはドキュメンテーションの型指定ルールをご参照ください。
型ヒント | 等価な(組み込み)型 |
---|---|
pandas_dataclasses.Data[T] |
collections.abc.Collection[T] |
pandas_dataclasses.Index[T] |
collections.abc.Collection[T] |
pandas_dataclasses.Attr[T] |
T |
pandas_dataclasses.Column[T] |
T |
2つ目として、df = Weather.new(...)
と書いたときのdf
の静的型・実行時型を指定できます。対応関係は以下の表の通りです。最後の例は、ユーザが定義したDataFrameまたはSeriesのサブクラス(SubClass
)を指定する書き方です。詳しくは、ドキュメンテーションのカスタムファクトリをご参照ください。
Mixinクラス | 静的型 | 実行時型 |
---|---|---|
pandas_dataclasses.AsFrame |
pandas.DataFrame |
pandas.DataFrame |
pandas_dataclasses.AsSeries |
pandas.Series[Any] |
pandas.Series |
pandas_dataclasses.As[SubClass] |
SubClass |
SubClass |
3つ目ですが、データクラスのインスタンス生成時と同様に引数の名前と型ヒントが表示されます。これにより、データクラスと同じ使い勝手が実現しました。なお、詳しくは触れませんが、実装では構造的部分型(PEP 544)と引数仕様変数(PEP 612)を使用しています。
さいごに
pandas-dataclassesは筆者の個人プロジェクトとして開発しており、もうまもなくメジャーバージョンとなるv1.0がリリースできるところまでやってきました。もしよろしければお試しいただき、機能提案などがあればぜひフィードバックをいただけると嬉しいです。また、pandasの多次元版とも言えるxarrayを対象としたxarray-dataclassesも開発していますので、こちらももし良ければチェックしてみてください。