Pandera 0.22は pandas・Polars・Modin・Dask・PySpark の各バックエンドに対応し、単一のスキーマ定義を複数エンジンで再利用できます。
DataFrameSchema(命令型)と DataFrameModel(宣言型・Pydantic風)の2つの定義スタイルがあり、後者は mypy と組み合わせて静的型チェックが可能です。
Check による値域・正規表現・統計的アサーション、lazy=True による全エラー一括収集、hypothesis 連携によるプロパティテストが標準で揃っています。
pytest fixtureとして組み込めば、CI/CDでdbtソーステーブルや中間モデルの「データ契約」を毎回検証できます。
パフォーマンスが懸念される場合は pandera.polars バックエンドの遅延評価、または本番ではサンプリング検証(sample=0.05)が有効です。
目次
Panderaとは何か:DataFrame版のスキーマ検証
インストールとPandera 0.22の最小構成
DataFrameSchemaで命令型スキーマを定義する
DataFrameModelで宣言型・型ヒント連携
Checkとカスタムバリデーター:統計的アサーション
PanderaとPolarsを連携させる
pytestとCIで毎回データ契約を検証する
PanderaとPydanticの違いは?
大規模データでのパフォーマンス最適化
よくある質問
Panderaとは何か:DataFrame版のスキーマ検証
Panderaは、pandas・Polars・Modin・Dask・PySparkのDataFrameに対して、カラムの存在・データ型・Null許容・値域・統計的性質を実行時に検証するためのライブラリです。SQLでいうところのNOT NULL制約やCHECK制約をDataFrameのレベルで表現でき、ETLパイプラインの境界(ソース読み込み直後、変換後、シンクへの書き込み前)に挿入することで、データ品質の保証ラインを宣言的に定義できます。
私自身、dbtでウェアハウス側のテストは網羅していても、そこに流し込む前のPython側パイプラインで「数値カラムに突然"N/A"文字列が混ざる」「アップストリームが日付フォーマットをYYYY-MM-DDからMM/DD/YYYYに黙って変えた」といったインシデントを何度も踏んできました。Panderaを入口に置くだけで、こうした変更は「本番で深夜に通知が鳴る前」に止められます。Pandera公式ドキュメント でも、こうしたデータ契約(data contract)パターンが推奨されています。
類似ツールとしてGreat Expectations がありますが、Panderaの強みは「Pythonコードとしてスキーマを書ける」「DataFrameModelを型ヒントとして使える」「依存が軽い」の3点です。Great Expectationsが「データ品質プラットフォーム」だとすれば、Panderaは「単体テスト用のpytestアサーション」のような立ち位置で、より開発者寄りです。
インストールとPandera 0.22の最小構成
2026年6月時点の最新は Pandera 0.22.x で、Python 3.10〜3.13をサポートします。バックエンドごとにextrasが分かれているので、必要なものだけ入れます。
# pandasバックエンドのみ
pip install "pandera[pandas]==0.22.*"
# Polars統合(pandera.polars モジュール)
pip install "pandera[polars]==0.22.*"
# Hypothesis連携によるプロパティテスト
pip install "pandera[hypotheses]==0.22.*"
# 全部入り
pip install "pandera[all]==0.22.*"
最小のスキーマ検証は次の通りです。pandas DataFrameに対して、user_idが整数で正の値、emailが非Null、signup_atがdatetime型であることを契約として宣言します。
import pandas as pd
import pandera.pandas as pa
from pandera import Column, Check, DataFrameSchema
schema = DataFrameSchema({
"user_id": Column(int, Check.greater_than(0)),
"email": Column(str, Check.str_matches(r"^[^@]+@[^@]+\.[^@]+$")),
"signup_at": Column("datetime64[ns, UTC]"),
})
df = pd.DataFrame({
"user_id": [1, 2, 3],
"email": ["[email protected] ", "[email protected] ", "[email protected] "],
"signup_at": pd.to_datetime(["2026-01-01", "2026-02-01", "2026-03-01"], utc=True),
})
validated = schema.validate(df)
# 失敗時は SchemaError が送出される
補足: 0.20以降、pandasバックエンドは pandera.pandas サブモジュールから import するのが推奨スタイルになりました。import pandera as pa の従来形式も動きますが、Polarsと混在するプロジェクトでは衝突回避のため新スタイルに統一することをお勧めします。
DataFrameSchemaで命令型スキーマを定義する
DataFrameSchemaは辞書的にカラムを定義する命令型APIです。動的にスキーマを組み立てる必要があるケース(例:設定YAMLから生成する、テスト用に部分的に上書きする)に向いています。
import pandera.pandas as pa
from pandera import Column, Check, Index, DataFrameSchema
orders_schema = DataFrameSchema(
columns={
"order_id": Column(int, unique=True, nullable=False),
"customer_id": Column(int, nullable=False),
"amount_jpy": Column(
float,
checks=[
Check.greater_than_or_equal_to(0),
Check.less_than(10_000_000),
],
),
"status": Column(
str,
Check.isin(["pending", "paid", "refunded", "cancelled"]),
),
"created_at": Column("datetime64[ns, UTC]", nullable=False),
},
index=Index(int, name="row_id"),
strict=True, # スキーマ未定義のカラムが混入していたらエラー
coerce=True, # 可能な範囲で型を強制変換
)
重要なオプションは次の3つです。
strict=True :未定義カラムを許さない。アップストリームが勝手に新カラムを追加した瞬間に検出できます。
coerce=True :型変換を試みる。CSV由来の"123"をintに寄せたいときに便利。
lazy=True (validate呼び出し時):最初のエラーで止めず全エラーを収集し、SchemaErrorsとして一括レポートします。
try:
orders_schema.validate(df, lazy=True)
except pa.errors.SchemaErrors as exc:
# exc.failure_cases は失敗行のDataFrame
print(exc.failure_cases.head())
print(exc.schema_errors)
CIでパイプラインの入力データをチェックする場面では、lazy=Trueでまとめてエラー列挙する方が圧倒的に運用しやすいです。最初のエラーで例外を投げる挙動は対話的なノートブックでは便利でも、毎回1件ずつ修正→再実行を繰り返す羽目になります。
DataFrameModelで宣言型・型ヒント連携
PydanticのBaseModelに慣れている人にはDataFrameModelの方が読みやすいはずです。クラスとしてスキーマを宣言し、関数引数の型ヒントとしてそのまま使えるので、mypyでDataFrameを「ただのpd.DataFrame」ではなく構造を持つ型として扱えます。
from typing import Annotated
import pandas as pd
import pandera.pandas as pa
from pandera.typing import DataFrame, Series
class OrdersModel(pa.DataFrameModel):
order_id: Series[int] = pa.Field(unique=True, ge=1)
customer_id: Series[int] = pa.Field(ge=1)
amount_jpy: Series[float] = pa.Field(ge=0, lt=10_000_000)
status: Series[str] = pa.Field(isin=["pending", "paid", "refunded", "cancelled"])
created_at: Series[pd.DatetimeTZDtype] = pa.Field(
dtype_kwargs={"unit": "ns", "tz": "UTC"}
)
class Config:
strict = True
coerce = True
@pa.check_types(lazy=True)
def enrich_orders(orders: DataFrame[OrdersModel]) -> DataFrame[OrdersModel]:
orders = orders.copy()
orders["amount_usd"] = orders["amount_jpy"] / 155.0
return orders
@pa.check_typesデコレータは、関数の引数と戻り値の両方をモデルに照らして検証します。dbtでいう「上流テスト」と「下流テスト」を、Python関数の境界で同時に張れるイメージです。型ヒントとしてのDataFrame[OrdersModel]はIDEの補完にも効くため、リファクタ時に「このカラム名、もう存在しない」と即座に気づけます。
ヒント: 既存のpandas DataFrameからモデルを自動生成するには pa.infer_schema(df).to_script() が便利です。出力されたコードを DataFrameModel に書き直すことで、レガシーETLにも段階的にスキーマを導入できます。
Checkとカスタムバリデーター:統計的アサーション
PanderaのCheckは値域や正規表現だけでなく、集計レベルの統計的アサーション を表現できます。これがGreat Expectationsに近い使い方を可能にする部分で、「平均は500±50に収まるはず」「ユニーク率は95%以上」のような契約を書けます。
import pandera.pandas as pa
from pandera import Check, Column
# カラム値が条件を満たすかを各行で評価
row_check = Check.greater_than(0)
# 集計レベルのアサーション(element_wise=False)
agg_check = Check(
lambda s: s.mean() > 100,
element_wise=False,
error="平均が100を下回っています — 入力分布の異常を疑ってください",
)
# 複数カラムにまたがるDataFrameレベルのチェック
df_check = Check(
lambda df: (df["paid_at"] >= df["created_at"]).all(),
error="paid_at は created_at 以降である必要があります",
)
schema = pa.DataFrameSchema(
columns={
"amount": Column(float, checks=[row_check, agg_check]),
"created_at": Column("datetime64[ns, UTC]"),
"paid_at": Column("datetime64[ns, UTC]", nullable=True),
},
checks=[df_check],
)
分布の妥当性を確認したい場合は、PanderaをHypothesis と組み合わせるとプロパティベーステストが書けます。スキーマからサンプルデータを生成し、変換関数が常に契約を満たすことを検証する流れです。
from hypothesis import given
from pandera.typing import DataFrame
@given(OrdersModel.strategy(size=100))
def test_enrich_orders_preserves_schema(df: DataFrame[OrdersModel]):
result = enrich_orders(df)
assert "amount_usd" in result.columns
assert (result["amount_usd"] >= 0).all()
PanderaとPolarsを連携させる
Pandera 0.18でPolarsバックエンドが安定化し、0.22では遅延評価(LazyFrame)にも対応しました。Polars入門ガイド:pandasユーザーが今すぐ始める次世代データフレーム で扱った高速処理の文脈に、データ品質保証のレイヤーを重ねるイメージです。
import polars as pl
import pandera.polars as pa
from pandera.polars import DataFrameModel, Field
from pandera.typing.polars import Series
class EventModel(DataFrameModel):
event_id: Series[pl.Int64] = Field(unique=True)
user_id: Series[pl.Int64] = Field(ge=1)
event_type: Series[pl.Utf8] = Field(isin=["click", "view", "purchase"])
occurred_at: Series[pl.Datetime] = Field(dtype_kwargs={"time_unit": "us", "time_zone": "UTC"})
class Config:
strict = True
lf = pl.scan_parquet("events/*.parquet")
validated = EventModel.validate(lf, lazy=True) # LazyFrameのまま検証契約を適用
result = validated.collect()
Polarsの遅延評価と組み合わせると、検証ロジックがクエリプランの一部としてプッシュダウンされるため、I/Oをかけずにスキーマ違反を早期に弾けます。大規模Parquetをスキャンするケースでは、pandasバックエンドで全件読み込んでから検証するより1桁速いことも珍しくありません。
pytestとCIで毎回データ契約を検証する
Panderaの真価は、スキーマをCIで毎回回すようにしてから出ます。「pipeline tests are non-negotiable」と言いたいのはまさにここで、dbtのschema.ymlと並べて、Python側パイプラインのテストとしてPanderaのスキーマ検証をpytestに乗せます。
# tests/test_orders_pipeline.py
import pandas as pd
import pytest
from src.pipeline import enrich_orders
from src.schemas import OrdersModel
@pytest.fixture
def sample_orders() -> pd.DataFrame:
return pd.read_csv("tests/fixtures/orders_sample.csv", parse_dates=["created_at"])
def test_enrich_orders_input_contract(sample_orders):
# 入力データそのものが契約を満たすか
OrdersModel.validate(sample_orders, lazy=True)
def test_enrich_orders_output_contract(sample_orders):
enriched = enrich_orders(sample_orders)
# 出力も同じ契約を満たし、追加カラムが存在するか
OrdersModel.validate(enriched, lazy=True)
assert "amount_usd" in enriched.columns
本番のスケジュール実行では、ETLジョブの先頭でschema.validate(raw_df, lazy=True)を呼び、SchemaErrorsをキャッチしてSlackやPagerDutyに「契約違反の件数と先頭5行」を送るパターンが運用しやすいです。pandasのpipe()とメソッドチェーンで作る再利用可能なデータクリーニングパイプライン と組み合わせれば、変換チェーンの各ステップ後に検証を差し込めます。
注意: CIで毎回フル件数を検証するとビルドが重くなります。1億レコード以上のテーブルは sample=0.01(1%サンプル)か、最新パーティションのみに絞ったスモークテストにとどめ、本番ジョブ側で全件検証を非同期に走らせる構成が現実的です。
PanderaとPydanticの違いは?
Pydanticは「単一オブジェクト(行)」の検証に最適化されており、Panderaは「列方向の集合・統計的性質」を含むDataFrame全体の検証に最適化されています。API設計のリクエスト/レスポンスにはPydantic、データパイプラインの中間テーブルにはPandera、という使い分けが王道です。
観点 Pandera 0.22 Pydantic 2.x Great Expectations 1.x
主な対象 DataFrame(pandas/Polars/Spark) 単一オブジェクト・ネストJSON データウェアハウス全般
列レベルの統計チェック 標準対応 非対応(個別実装) 標準対応
静的型ヒント連携 DataFrame[Model]でmypy可強力 限定的
導入コスト 低(pipで完結) 非常に低 高(プロジェクト構造を要求)
ドキュメント自動生成 限定的 OpenAPI連携あり Data Docsを自動生成
適した用途 ETL/ELTのPython境界 API・設定ファイル BIテーブル・組織横断品質
個人的には、dbtでカバーできるテストはdbtで書き、dbtの外(Python ETL、ML特徴量生成、Lambda/Step Functions)はPanderaで張る、というレイヤー分担を採っています。DuckDBとPythonの分析実践 でDuckDB経由でデータを取り出すような場面でも、入口にPanderaを置くだけで型ドリフトを早期検出できます。
Panderaの検証は基本的に線形時間ですが、行レベルのCheckを多数つけると無視できないオーバーヘッドになります。以下の打ち手で大半のケースは現実的なレイテンシに収まります。
サンプリング :schema.validate(df, sample=0.05, random_state=42)で5%だけ検証。CIでは十分。
element_wise=Falseの集計チェックに寄せる :lambda s: (s > 0).all()はpandasのベクトル演算なので、行ループより1桁速い。
Polarsバックエンドへの切り替え :列指向のため大規模Parquetで顕著に有利。
strict=False + 必要カラムのみ宣言 :100カラムのうち契約したいのは10カラムだけ、という現実的なケース。
本番では非同期検証 :ETLの完了後にCloud Run/Lambdaでスキーマ検証ジョブを走らせ、結果をメトリクスとしてPrometheusのテキスト形式 でエクスポートする。
PolarsのLazyFrameを使う場合、Pandera 0.22では検証条件がクエリプランに統合され、不要な列の読み出しがスキップされます。1億行のParquetでも、strict=Trueのスキーマ違反は数百ms以内に検出できる体感です。
よくある質問
Panderaはどんなときに使うべきですか?
外部から流入するデータ(CSV、API、上流のdbtモデル、S3のParquet)をPythonで処理するパイプラインの入口・出口で使うのが定石です。型ドリフトやNull混入を本番障害ではなくCIテストの段階で検出したい場面に最適です。
PanderaとGreat Expectationsはどちらを選ぶべきですか?
開発者主導でコードベースに閉じ込めたいならPandera、データチーム横断で品質を可視化したいならGreat Expectationsが向きます。両方併用し、Python層をPandera、ウェアハウス層をGreat Expectationsで分担する構成も一般的です。
PanderaはPolarsで動きますか?
動きます。pandera.polarsモジュールからDataFrameModelとFieldをimportし、LazyFrameに対しても遅延評価のままvalidate()を呼べます。Pandera 0.22で本番運用に耐える品質になっています。
スキーマ違反が発生したときの良い扱い方は?
lazy=TrueでSchemaErrorsを捕捉し、failure_cases DataFrameを構造化ログに出力するのが基本です。重大度に応じてジョブを失敗させるか、隔離テーブル(quarantine)に逃がしてアラートだけ送る運用が現実的です。
既存のDataFrameからスキーマを自動生成できますか?
はい。pandera.infer_schema(df).to_script()でPythonコードとして出力できます。生成されたDataFrameSchemaを叩き台に、業務上の制約(値域・列挙)を手で足していくのが効率的なワークフローです。