🐼

pandasのDataFrameをどうテストするか(pandas.testingの話)

に公開

この記事でやったこと

pandasのDataFrame同士が同じかどうかをテストするときに、pandas.testingを使うと便利です。
なぜpandas.testingを使った方がいいのかについて実例とともに紹介します。

背景

pandasのDataFrameを使って、表形式データの加工を行いたいと思いました。

しかし、加工ロジックが複雑になってくると、テストがないと不安になりますよね。

import pandas as pd
from typing import Callable

func_type = Callable[[pd.DataFrame], pd.DataFrame]

def run(df: pd.DataFrame) -> pd.DataFrame:
    df = some_process_1(df)
    df = some_process_2(df)
    df = some_process_3(df)
    df = some_process_4(df)
    df = some_process_5(df)
    return df

def main():
    df = pd.read_csv("sample.csv")
    result_df = run(df)
    # ↑ このresult_dfに正しい処理がされているか不安😢

ただ、pandasのDataFrameは直接==で比較し、Assertすることはできません。

sample_1 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])
sample_2 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])
assert sample_1 == sample_2

# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
#    assert sample_1 == sample_2
#           ^^^^^^^^^^^^^^^^^^^^
#  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/core/generic.py", line 1513, in __bool__
#    raise ValueError(
#    ...<2 lines>...
#    )
#ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

ValueErrorになってしまいますね。

これは、DataFrameに対する==は、bool値を返さずDataFrameを返し、assert は真偽値を要求するため、ValueErrorになってしまいます。

余談: DataFrame同士の == の挙動

ちなみに、DataFrame同士で==を行うと以下のような結果が返ります。便利ですね。

>>> sample_1 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])
>>> sample_2 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])
>>> sample_1 == sample_2
      a     b     c
0  True  True  True
1  True  True  True

では、dictに変換して確認してみましょうか。

sample_1 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])
sample_2 = pd.DataFrame([{"a": 1, "b": 2, "c": 3},{"a": 1, "b": 2, "c": 3}])

assert sample_1.to_dict() == sample_2.to_dict()

これでも同じことは確認できました。。

しかしながら、この方法だと、以下のようなTrue1のように、python上で==で評価されたときにTrueを返してしまうようなケースだと正しく判別できません。


sample_3 = pd.DataFrame({"a": [1, 1], "b": [2, 2], "c": [True, True]})
#                                                  違う↑↓ ^^^^^^^^^^^
sample_4 = pd.DataFrame({"a": [1, 1], "b": [2, 2], "c": [1, 1]})

print("output: ", sample_3.to_dict() == sample_4.to_dict())

# output: True

True と 1で値が異なるのにTrueとされてしまいました。

これはpython自体の仕様のため仕方がない部分もありますが、ユースケースによってはこれらを厳密に分離したいこともあるでしょう。

そこで、今回はpandas.testingを紹介します。

pandas.testing

pandas.testingは、pandasの中に含まれる、pandas.DataFrameや、pandas.Series同士が正しいことなどを確認するためのライブラリです。

公式ドキュメント: https://pandas.pydata.org/docs/reference/testing.html

このpandas.testingに含まれる、assert_frame_equalを用いることによって、今回のような列のデータ型が異なるようなケースを異なるものとしてassertすることができます。

import pandas as pd

sample_3 = pd.DataFrame({"a": [1, 1], "b": [2, 2], "c": [True, True]})
sample_4 = pd.DataFrame({"a": [1, 1], "b": [2, 2], "c": [1, 1]})

pd.testing.assert_frame_equal(sample_3, sample_4)

結果

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    assert_frame_equal(sample_3, sample_4)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/util/_decorators.py", line 220, in wrapper
    return func(*args, **kwargs)
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 1347, in assert_frame_equal
    assert_series_equal(
    ~~~~~~~~~~~~~~~~~~~^
        lcol,
        ^^^^^
    ...<12 lines>...
        check_flags=False,
        ^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/util/_decorators.py", line 193, in wrapper
    return func(*args, **kwargs)
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 1029, in assert_series_equal
    assert_attr_equal("dtype", left, right, obj=f"Attributes of {obj}")
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 448, in assert_attr_equal
    raise_assert_detail(obj, msg, left_attr, right_attr)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 619, in raise_assert_detail
    raise AssertionError(msg)
AssertionError: Attributes of DataFrame.iloc[:, 2] (column name="c") are different

Attribute "dtype" are different
[left]:  bool
[right]: int64

assert_frame_equalを使うことで、判別ができるようになり、想定通り異なるDataFrameとして判定してくれています。

さらに、assert_frame_equalを使うことで、エラーメッセージを詳しく出してくれるようになりました。

今回は

AssertionError: Attributes of DataFrame.iloc[:, 2] (column name="c") are different

Attribute "dtype" are different
[left]:  bool
[right]: int64

とあることから、cというカラムのデータ型が異なることが一目瞭然です。

他にも、assert_frame_equalには、indexを無視するかどうかや、浮動小数点の誤差をいい感じに扱ってくれたりなどの、便利機能があります。

特に、assert_frame_equalは、最初に挙げたようなデータ型についても検査してくれるという利点以外にも、assertのメッセージが丁寧なのでDataFrame同士でテストを行いたいときには、積極的に使った方がいいですね。

↓ 値が異なる例

>>> sample_5 = pd.DataFrame({"a": [1, 1], "b": [2, 3], "c": [1, 1]})
>>> sample_6 = pd.DataFrame({"a": [1, 1], "b": [2, 1], "c": [1, 1]})
>>> assert_frame_equal(sample_5, sample_6)

結果

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    assert_frame_equal(sample_5, sample_6)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/util/_decorators.py", line 220, in wrapper
    return func(*args, **kwargs)
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 1347, in assert_frame_equal
    assert_series_equal(
    ~~~~~~~~~~~~~~~~~~~^
        lcol,
        ^^^^^
    ...<12 lines>...
        check_flags=False,
        ^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/util/_decorators.py", line 193, in wrapper
    return func(*args, **kwargs)
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 1051, in assert_series_equal
    assert_numpy_array_equal(
    ~~~~~~~~~~~~~~~~~~~~~~~~^
        lv,
        ^^^
    ...<3 lines>...
        index_values=left.index,
        ^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 695, in assert_numpy_array_equal
    _raise(left, right, err_msg)
    ~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 689, in _raise
    raise_assert_detail(obj, msg, left, right, index_values=index_values)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/*****/.venv/lib/python3.13/site-packages/pandas/_testing/asserters.py", line 619, in raise_assert_detail
    raise AssertionError(msg)
AssertionError: DataFrame.iloc[:, 1] (column name="b") are different

DataFrame.iloc[:, 1] (column name="b") values are different (50.0 %)
[index]: [0, 1]
[left]:  [2, 3]
[right]: [2, 1]

おしまい!

GitHubで編集を提案

Discussion