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

## はじめに

これは[Python Advent Calendar 2022]の23日目の記事です。[pandas]のDataFrameやSeriesの設計・生成を、Pythonの標準ライブラリの[データクラス]を使って簡単にできるようにする[pandas-dataclasses]を紹介します。詳しい使い方は[ドキュメンテーション][pandas-dataclasses]に書かれていますがこれは英語なので、日本語の解説記事ということで書いておきたいと思います（が、全ての内容は網羅できていません…）。

https://github.com/astropenguin/pandas-dataclasses

ひとこと（ひとコード？）でまとめると、DataFrameやSeriesの設計がこんな感じでできるようになりますよ、というものです。

```python
@dataclass
class Weather(AsFrame):
    year: Index[int]  # 測定年
    month: Index[int]  # 測定月
    temp: Data[float]  # 月平均気温
    wind: Data[float]  # 月平均風速
```

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

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

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

```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]）を指定することでこの問題を解決しています。詳しくは、ドキュメンテーションの[命名ルール]をご参照ください。

```python:変数アノテーションによる一般的な要素名の指定
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)"]
```

```plaintext:生成される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
```

### 動作原理

:::note warn
以下の内容はとりあえず使ってみる分には不要です。必要に応じてご確認ください。
:::

pandas-dataclassesでは以下の2ステップでDataFrameやSeriesを生成しています。

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

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

```python: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に変換する関数です。データクラスに詳しい方であれば、これは`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]` | カラムの名前の指定に使われる。ここでは詳しく触れないため、ドキュメンテーションの[階層的カラム]をご参照ください。


### 静的型チェック

:::note warn
以下の内容はとりあえず使ってみる分には不要です。必要に応じてご確認ください。
:::

データクラスを使うメリットの一つは、最近のPythonで利用が進みつつある[型ヒント]による静的型付けの恩恵を受けられることです。例えば、[mypy]・[Pyright]・[Pylance]などの多くの静的型チェッカーでは、データクラスのインスタンス生成時に引数の一覧を表示してくれますし、引数の型が間違っていれば指摘してくれます。pandas-dataclassesではこれらの静的型チェッカーをサポートし、以下の3つの型チェック関連の機能を提供しています。

1. **専用の型ヒントによる要素の静的型チェック**
1. **Mixinクラスによるクラスメソッド`new`の出力型指定**
1. **クラスメソッド`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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44000/2a01ff8f-35bf-0680-393c-f75c63b3111e.png)

## さいごに

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

https://github.com/astropenguin/pandas-dataclasses

<!-- Python docs and PEPs -->
[データクラス]: https://docs.python.org/ja/3/library/dataclasses.html
[型ヒント]: https://docs.python.org/ja/3/library/typing.html
[PEP 544]: https://peps.python.org/pep-0544
[PEP 593]: https://peps.python.org/pep-0593
[PEP 612]: https://peps.python.org/pep-0612

<!-- astropenguin's projects -->
[pandas-dataclasses]: https://github.com/astropenguin/pandas-dataclasses
[xarray-dataclasses]: https://github.com/astropenguin/xarray-dataclasses
[階層的カラム]: https://github.com/astropenguin/pandas-dataclasses#hierarchical-columns
[型指定ルール]: https://github.com/astropenguin/pandas-dataclasses#data-typing-rules
[命名ルール]: https://github.com/astropenguin/pandas-dataclasses#naming-rules
[カスタムファクトリ]: https://github.com/astropenguin/pandas-dataclasses/#custom-pandas-factory

<!-- Other links -->
[pandas]: https://pandas.pydata.org
[xarray]: https://xarray.dev
[mypy]: http://www.mypy-lang.org
[Pyright]: https://github.com/microsoft/pyright
[Pylance]: https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance
[Python Advent Calendar 2022]:https://qiita.com/advent-calendar/2022/python