LoginSignup
17
11

More than 1 year has passed since last update.

データクラスでpandasデータを設計しよう!pandas-dataclassesの紹介

Last updated at Posted at 2022-12-22

はじめに

これは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はこんな感じになるはずです。

気象情報の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要素なのでそこまで複雑ではないですが、要素が多い場合はコードの可読性と保守性が悪化することが容易に想像できます。

pandasを普通に使った場合
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の標準ライブラリのデータクラスを使って各要素の仕様を定義できるのがこのパッケージの売りです。関数による実装は、以下のようにデータクラスの実装に書き換えることができます。

pandas-dataclassesを使った場合
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)"]
生成されるDataFrame(例)
            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を生成しています。

  1. ユーザが定義したデータクラスのインスタンスを生成する
  2. 生成したインスタンスをDataFrameまたはSeriesに変換する

上の例ではdf = Weather.new(...)でDataFrameを生成していますが、これは以下のコードと等価です。

Weather.newの呼び出しと等価なコード
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に変換する関数です。データクラスに詳しい方であれば、これはasdictastupleの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で利用が進みつつある型ヒントによる静的型付けの恩恵を受けられることです。例えば、mypyPyrightPylanceなどの多くの静的型チェッカーでは、データクラスのインスタンス生成時に引数の一覧を表示してくれますし、引数の型が間違っていれば指摘してくれます。pandas-dataclassesではこれらの静的型チェッカーをサポートし、以下の3つの型チェック関連の機能を提供しています。

  1. 専用の型ヒントによる要素の静的型チェック
  2. Mixinクラスによるクラスメソッドnewの出力型指定
  3. クラスメソッド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)を使用しています。
new-info.png

さいごに

pandas-dataclassesは筆者の個人プロジェクトとして開発しており、もうまもなくメジャーバージョンとなるv1.0がリリースできるところまでやってきました。もしよろしければお試しいただき、機能提案などがあればぜひフィードバックをいただけると嬉しいです。また、pandasの多次元版とも言えるxarrayを対象としたxarray-dataclassesも開発していますので、こちらももし良ければチェックしてみてください。

17
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
11