0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

データに責任を持つ第一歩?:ノートブックを卒業した日 ── 指南書で学んだMLOps技術をKaggleで全部試してみた 2日目

0
Posted at

医療従事者出身、未経験でデータ職を目指して転職活動中のshimodasと申します。
初学者なりにMLOpsについて学び、試行錯誤の末に出来上がったものたちを発信中です。

シリーズ「ノートブックを卒業した日 ── 指南書で学んだMLOps技術をKaggleで全部試してみた」

  1. uv(パッケージ管理))
  2. Pandera(データバリデーション)← 今ここ
  3. Hydra(設定管理)
  4. MLflow(実験管理)
  5. Streamlit(アプリ化・デプロイ)
  6. Dev Container(開発環境コンテナ)
  7. Cookiecutter × リポジトリ設計

はじめに

機械学習を始めたばかりの自分は、データの品質チェックをほとんどしていませんでした。

前処理スクリプトを書いて、pd.read_csv() でデータを読み込んで、そのまま学習に突っ込む。エラーが出なければOKだと思っていました。でも実際には、エラーが出ないまま間違った結果が出続けることがあります。型が違っていても、想定外の値が混入していても、Pythonは黙って処理を続けます。「なんか異様にスコアが低いな」と思って掘り返したら、前処理の段階で誤ったデータを投入していた、という経験を何度かしました。

『先輩データサイエンティストからの指南書』(浅野純季ら著、技術評論社、2025年)に、その解決策として Pandera が紹介されていました。「スキーマを定義してデータフレームを自動検証する」という話で、読んだ瞬間に「すごい」と思いました。目視での確認に頼るのではなく、仕組みとしてデータの質を担保する。それがプロとしてのデータへの向き合い方なのかもしれない、と感じました。

この記事では、Playground Series S6E5(F1ピットストップ予測)のデータパイプラインにPanderaを適用した話を書きます。シリーズ第2回は Pandera です。第1回(uv)はこちら

対象読者: Pythonでモデルを書いていて、「前処理のバグがなぜかエラーにならない」「データの型や範囲が意図通りか自信がない」という方。

そもそもPanderaとは

Pandera(パンデラ)は、Pandasのデータフレームに対するバリデーションライブラリです。データ加工・前処理の際に「入出力が期待通りのスキーマに沿っているか」を自動チェックし、サイレント障害(エラーなく誤った結果が出続ける状態)を防ぎます。

一言でまとめると、こういうことができます。

import pandera as pa
from pandera.typing import Series

class FeatureSchema(pa.DataFrameModel):
    pit_duration: Series[float] = pa.Field(ge=0)
    lap_number: Series[int] = pa.Field(ge=1)
    compound: Series[str] = pa.Field(isin=["SOFT", "MEDIUM", "HARD"])

このクラスを定義するだけで、「pit_duration は0以上のfloat」「compound は3種類のいずれか」というような仕様がコードに書けます。これが意図通りでない場合、SchemaError が即座に出ます。

Panderaを使う3大メリットはこんな感じです。

  1. バグの早期発見:型のズレ・外れ値・NULL混入をデータパイプラインの発生源近くで検知できる
  2. コードが仕様書になる:スキーマを見るだけで入出力の仕様が分かる
  3. リファクタリングの安全網:特徴量を追加・変更した際、スキーマ違反があれば即エラーになる

インストール

uv add pandera

Panderaが解決する問題

例えば、こんなコードがあります。

# preprocess.py
def preprocess(df: pd.DataFrame) -> pd.DataFrame:
    df["pit_duration"] = df["pit_duration"].fillna(0)
    df["lap_number"] = df["lap_number"].astype(int)
    return df

何が問題なのか、という話なんですが。

fillna(0) で欠損を埋めるのは良いとして、pit_duration に負の値が混じっていても気づきません。astype(int) は、"abc" のような変換不能な文字列や NaN が含まれる場合はエラーを出してくれます。しかし、1.5 のような小数点付きの floatint に変換する場合は、エラーも警告も出ずに小数点以下が切り捨てられてしまいます

さらに前処理スクリプトが増えてきたとき、train.pypredict.pyevaluate.py それぞれで微妙に違う前処理が走っていて、「推論時だけデータの形が違う」という事態が起きそうです(そもそも管理が大変)。Kaggleでは学習時と推論時でデータを別々に作ることが多いので、この「学習・推論のスキーマの乖離」問題は特にハマりやすいと思います。

つまり、「データが正しい形かどうか」を確認する仕組みがないことが根本の問題です。

実装:スキーマでデータを守る

ディレクトリ構成

(プロジェクトルート)
├── src/f1_pit_stops/
│   └── schema.py      ← スキーマ定義をここに集約
├── scripts/
│   └── train.py       ← 学習スクリプト
└── ...

スキーマ定義は schema.py として独立したファイルにまとめます。これで複数のスクリプトから使い回せます。

schema.pyの定義 (簡略化)

import pandera.pandas as pa
from pandera.typing import Series


class FeatureSchema(pa.DataFrameModel):
    """7特徴量の共通スキーマ(Train / Inference の基底)"""

    Stint: Series[int] = pa.Field(ge=1, le=8)
    Year: Series[int] = pa.Field(isin=[2022, 2023, 2024, 2025])
    Driver: Series[str]
    Race: Series[str]
    TyreLife: Series[float] = pa.Field(ge=1.0, le=77.0)
    RaceProgress: Series[float] = pa.Field(ge=0.01, le=1.0)
    Compound: Series[str] = pa.Field(
        isin=["HARD", "MEDIUM", "SOFT", "INTERMEDIATE", "WET"]
    )

    class Config:
        strict = True
        coerce = True


class TrainSchema(FeatureSchema):
    """学習用(id + 目的変数 PitNextLap)"""

    id: Series[int] = pa.Field(ge=0)
    PitNextLap: Series[int] = pa.Field(isin=[0, 1])

    class Config(FeatureSchema.Config):
        strict = True
        coerce = True


class InferenceSchema(FeatureSchema):
    """推論用(7特徴量のみ)"""

    class Config(FeatureSchema.Config):
        strict = True
        coerce = True

FeatureSchema に共通の特徴量を定義して、TrainSchemaInferenceSchema がそれぞれ継承します。学習データにしかない target カラムは TrainSchema だけに定義する、という設計です。

strict=True は定義していないカラムが混入していたときにエラーを出す設定です。「なんか余計なカラムが入ってた」という問題を防げます。

coerce=True は型の自動変換を許可する設定です。"1" という文字列を int として定義していれば自動で変換してくれます。ただし変換できない値があればエラーになります。

スクリプトへの組み込み (該当部分のみ)

このプロジェクトでは、デコレータではなく .validate() を明示的に呼び出す方式 も採用しています。

from f1_pit_stops.schema import TrainSchema, InferenceSchema

FEATURE_COLS = ["Stint", "Year", "Driver", "Race", "TyreLife", "RaceProgress", "Compound"]
TRAIN_COLS = ["id"] + FEATURE_COLS + ["PitNextLap"]
TEST_COLS  = ["id"] + FEATURE_COLS

def preprocess_train(raw_path):
    df = pd.read_csv(raw_path)[TRAIN_COLS]
    df["PitNextLap"] = df["PitNextLap"].astype(int)
    # ここに前処理を追記する
    TrainSchema.validate(df)      # 前処理が終わった後に検証
    return df

def preprocess_test(raw_path):
    df = pd.read_csv(raw_path)[TEST_COLS]
    # ここに前処理を追記する
    InferenceSchema.validate(df[FEATURE_COLS])  # id を除いた7特徴量だけ検証
    return df

ポイントは2つあります。

① 前処理の「後」に検証する:加工が終わった DataFrame が正しい状態かを確認したいので、validate() を処理の最後に置いています。デコレータ方式だと「入力 or 出力を自動検証」になりますが、「どのタイミングで検証するか」を自分でコントロールしたい場合は .validate() の方が自然です。

② test データは特徴量だけ検証するpreprocess_test が返す DataFrame には id が含まれますが、スキーマ検証は df[FEATURE_COLS](7特徴量のみ)に対して行っています。InferenceSchemaid を持たない設計なので、id 込みの DataFrame 全体に対してバリデーションするとエラーになります。

**別の書き方:@pa.check_types デコレータ(簡単な例) **

入出力のスキーマが固定されている関数なら、デコレータを使う書き方もあります。

import pandera.pandas as pa
from pandera.typing import DataFrame
from f1_pit_stops.schema import FeatureSchema, TrainSchema

@pa.check_types
def preprocess_train(df: DataFrame[TrainSchema]) -> DataFrame[TrainSchema]:
    df = df.copy()
    df["PitNextLap"] = df["PitNextLap"].astype(int)
    return df

デコレータを1行付けるだけで、関数の入力・出力の両方が自動でバリデーションされます。validate() の呼び出しを書き忘れる心配がなく、関数シグネチャを見るだけで「何を受け取り、何を返すか」がスキーマとして明示されます。

ただし、このプロジェクトでは .validate() を選んでいます。preprocess_testid 付きの DataFrame を返しつつ7特徴量だけを検証する設計になっており、デコレータで返り値全体に InferenceSchema を当てようとするとスキーマが合わないためです。どちらも Pandera として正しい使い方で、制御のしやすさの差です。

方式 向いている場面
.validate() パイプラインの任意タイミングで検証、部分検証(カラムを絞って検証)、スクリプト
@pa.check_types 入出力が固定の関数・ライブラリAPI

SchemaErrorが出たら何が分かるか

スキーマと合わない場合、こういうエラーが出ます。
Cursor_BqhiWZVPjq.png

インデックス42行目の compound カラムに "INTER" という値が入っていたことが分かります。「どこで壊れたか」を特定するのに、print() でデバッグする必要がありません。

これがPanderaを使う前との一番大きな差だと思います。エラーが出た時点で「どのカラムの、何行目の、どんな値が問題か」が一発で分かります。

pa.Fieldのオプション一覧

よく使うオプションをまとめます。

引数 説明
ge pa.Field(ge=0) 指定値以上
le pa.Field(le=100) 指定値以下
in_range pa.Field(in_range={"min_value": 0, "max_value": 6}) 指定範囲内
isin pa.Field(isin=["A", "B"]) 指定リストのいずれか
nullable pa.Field(nullable=True) NULLを許可するか
unique pa.Field(unique=True) 値がユニーク

in_rangege + le の組み合わせをまとめて書ける感じです。F1ピットストップ予測では、lap_number の上限(レースの最大周回数)をここで指定しました。

カスタムチェック:カラム間の整合性

組み込みオプションでは表現できない「カラム間の関係」は @pa.dataframe_check で書けます。

class TrainSchema(FeatureSchema):
    target: Series[int] = pa.Field(isin=[0, 1])

    @pa.dataframe_check
    def target_consistent_with_pit_duration(cls, df: pd.DataFrame) -> Series[bool]:
        """pit_durationが異常に長い場合はtarget=1になっているべき"""
        long_pit = df["pit_duration"] > 60
        return ~long_pit | (df["target"] == 1)

カラム間の整合性チェックがスキーマの中に書けるので、「このチェックどこに書いたっけ」問題がなくなります。

注意点:coerce=Trueの落とし穴

coerce=True は便利ですが、2つの罠があります。

罠1:小数点以下がサイレントに切り捨てられる

class SampleSchema(pa.DataFrameModel):
    val: Series[int]
    class Config:
        coerce = True

# 1.5 → int に coerce すると、エラーなく 1 になる
df = pd.DataFrame({"val": [1.5, 2.5, 3.5]})
SampleSchema.validate(df)  # 成功。val は [1, 2, 3] になっている

floatint スキーマに coerce=True で流すと、小数点以下が切り捨てられてサイレントに変換されます。自分はこれで一度ハマりました(pit_durationint と誤記したまま気づかなかった)。

罠2:NaNがあるとエラーになる

# NaN を含む float を int に coerce するとエラー
df = pd.DataFrame({"val": [1.0, float("nan"), 3.0]})
SampleSchema.validate(df)  # SchemaErrors: DATATYPE_COERCION

Python の int 型は NaN を表現できないため、欠損値が混入していると変換に失敗してエラーになります。欠損値を含む可能性があるカラムを int で定義したい場合は、pa.Field(nullable=True) を指定するか、Pandas の Int64(nullable integer型)を使います。

import pandas as pd
from pandera.typing import Series

class TrainSchema(pa.DataFrameModel):
    PitNextLap: Series[pd.Int64Dtype()] = pa.Field(isin=[0, 1], nullable=True) 
     # NaN を許容する int + 値の範囲の指定

coerce=True を使うときは、変換後の値をあわせてチェックする pa.Field のオプションも指定しておくと安心です。

まとめ

  • Panderaは pa.DataFrameModel を継承したスキーマクラスで、データフレームの型・範囲・値を宣言的に定義できます
  • .validate() で任意のタイミングに検証を挟め、@pa.check_types で入出力の自動検証も書けます
  • strict=True で余計なカラムの混入を防ぎ、coerce=True で型の自動変換を許可します
  • スキーマ継承(TrainSchemaInferenceSchema)で学習・推論の乖離を防げます
  • エラーが出たとき「どのカラムの何行目が問題か」が即座に分かるのが最大の価値です

「データが正しい形かどうか確認する仕組みがない」という状態は、静かにスコアを下げ続けます。Panderaを入れてから、「なんかおかしい」の調査時間が明らかに減りました。

実装を通じて気づいたのは、スキーマを定義する段階で「どの特徴量を使うか」「値の範囲はどこまでか」「欠損を許容するか」を明確に決める必要があるということです。目視確認に頼らず仕組みでデータの質を担保することが、データに責任を持つということだと実感しました。

次回(第3回)は Hydra の設定管理を詳しく書く予定です。

参考文献・リンク

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?