0Pricing
Pandas & NumPy Academy · レッスン

アサーションによるパイプライン手順のテスト

各段階に行数チェック、NULLに関するアサーション、想定列のガードを追加し、エラーをすぐに発見できるようにします。

「アサーションによるパイプライン手順のテスト」はCoddyKit上の無料Pandas & NumPy Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPandas & NumPy Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

パイプラインの各ステップをテストする理由

エラーなく実行できるデータパイプラインでも、気付かないまま誤った出力を生成することがあります。たとえば、誤ったフィルターによる行の削除、列への誤った係数の乗算、結合キーが一意でないために行が重複するマージなどです。このようなサイレントな失敗を検出する唯一の方法は、パイプラインの各段階でデータが期待どおりの特性を持つことを確認するアサーションを組み込むことです。アサーションにより、論理的なバグを、すぐに認識できる明確なエラーに変えられます。

import pandas as pd
import numpy as np

# A transform that looks correct but has a bug:
def compute_revenue_buggy(df):
    # BUG: unit_price should be multiplied, not added
    df['revenue'] = df['quantity'] + df['unit_price']
    return df

# Without assertions, this runs silently with wrong numbers.

行数チェック

各フィルタリング処理の後で、結果の行数が想定範囲内にあることをアサートします。行数が少なすぎる場合はフィルターが厳しすぎた可能性があり、多すぎる場合は結合によってレコードが重複した可能性があります。想定範囲は入力に対する割合として表します。たとえば、健全なデータでは、null を削除する処理で行の 30 % を超えて削除されることはありません。このガードにより、上流ソースの予期しないデータ品質の変化を検出できます。

def drop_nulls_guarded(df, required_cols, max_drop_fraction=0.3):
    before = len(df)
    result = df.dropna(subset=required_cols)
    after = len(result)
    drop_fraction = (before - after) / before
    assert drop_fraction <= max_drop_fraction, \
        f'dropna removed {drop_fraction:.1%} of rows (limit {max_drop_fraction:.1%})'
    return result

列の存在チェック

各変換関数の後で、想定されるすべての出力列が存在することをアサートします。rename 処理で誤ったキーを使用すると、結果の列が存在しなくなります。そのエラーは、別のステップがその列を使おうとするずっと後になって初めて表面化することがあります。transform の直後にアサーションを置けば、3ステップ後に分かりにくい KeyError が発生するのを待たず、問題の発生源で検出できます。

def compute_revenue(df):
    df = df.assign(revenue=lambda d: d['quantity'] * d['unit_price'])

    # Guard: check that the new column exists and is non-null
    assert 'revenue' in df.columns, 'revenue column not created'
    assert df['revenue'].notna().all(), 'revenue has unexpected NaNs'
    return df

値の範囲に関するアサーション

revenue 列を計算した後は、値が負でないことをアサートします。日付を解析した後は、想定される年の範囲内に収まっていることをアサートします。カテゴリをエンコードした後は、想定外のコードが出現していないことをアサートします。それぞれのアサーションは、期待される動作を記録し、違反を早期に検出するデータ契約です。print 文ではなくアサーションとして記述し、自動化されたパイプライン実行時にエラーが発生するようにします。

def validate_computed_columns(df):
    assert (df['revenue'] >= 0).all(), \
        f'Negative revenue: {df[df["revenue"] < 0]["revenue"].head().tolist()}'
    assert (df['quantity'] > 0).all(), \
        'Non-positive quantity found'
    assert df['revenue'].between(0, 1_000_000).all(), \
        'Revenue out of plausible range'
    print('Value range checks passed.')

マージ後の重複防止ガード

よくあるデータバグの一つに、多対多のマージによって行数が意図せず増える問題があります。各 pd.merge() の後で、行数が想定どおりであることをアサートします。通常、left join では左側の DataFrame の行数を超えていないことを確認します。また、キー列が一意であるべき場合は、そのこともアサートして、意図しないクロス結合をすぐに検出できるようにします。

def safe_merge(left, right, on, how='left'):
    before = len(left)
    result = left.merge(right, on=on, how=how)
    after = len(result)

    if how == 'left':
        assert after == before, \
            f'Left join increased rows from {before} to {after} — check key uniqueness in right df'
    return result

重要なステップ後の null アサーション

パイプラインのどの時点でも null になってはいけない列があります。マージで一部の行が一致せず結合列に NaN が生成される場合や、map() の呼び出しでマッピングされていない値に対して NaN が返される場合など、null が誤って導入される可能性のある各ステップの後で df['key_col'].notna().all() をアサートします。これらの列を扱うすべての変換関数では、このアサーションを最後から 2 行目に置きます。

NOT_NULL_AFTER_TRANSFORM = ['order_id', 'revenue', 'category']

def post_transform_checks(df):
    for col in NOT_NULL_AFTER_TRANSFORM:
        null_count = df[col].isna().sum()
        assert null_count == 0, \
            f'{col}: {null_count} unexpected NaN values after transform'
    print('Null checks passed.')
    return df

パイプラインの各ステップのテストスイート構築

テスト対象のロジックだけを切り出せるよう、手作業で作成した小さな DataFrame を使って各パイプライン関数の単体テストを書きます。各テストでは、最小限の入力を準備し、関数を呼び出し、出力の特性をアサートします。単純なパイプラインでは Python 組み込みの assert で十分です。大規模なプロジェクトでは、pytest を使ってデプロイ前にすべてのテストを自動実行します。

def test_compute_revenue():
    test_df = pd.DataFrame({
        'quantity': [2, 3],
        'unit_price': [10.0, 5.0]
    })
    result = compute_revenue(test_df)

    assert 'revenue' in result.columns
    assert result['revenue'].tolist() == [20.0, 15.0]
    assert result['revenue'].dtype == float
    print('test_compute_revenue PASSED')

test_compute_revenue()

エッジケースのテスト

適切な入力に対する正常系だけでなく、すべての値が null の入力、空の DataFrame、1 行だけの DataFrame、極端な値を持つ列などのエッジケースも、優れたテストでは扱います。空の DataFrame に対してはエラーではなく空の DataFrame を返すべきです。1 行だけの DataFrame でも正しい結果を計算できる必要があります。特殊なデータが本番環境に届いたときもパイプラインが適切に処理できるよう、これらのケースを明示的にテストします。

def test_empty_df():
    empty = pd.DataFrame({'quantity': [], 'unit_price': []})
    result = compute_revenue(empty)
    assert len(result) == 0, 'Empty input should produce empty output'
    assert 'revenue' in result.columns, 'Revenue column should still be created'
    print('test_empty_df PASSED')

def test_single_row():
    single = pd.DataFrame({'quantity': [1], 'unit_price': [99.0]})
    result = compute_revenue(single)
    assert result['revenue'].iloc[0] == 99.0
    print('test_single_row PASSED')

test_empty_df()
test_single_row()

統合テスト:サンプルデータでパイプライン全体を実行する

個々の関数に対する単体テストに加えて、実際のデータを代表する小規模なサンプルに対してパイプライン全体を実行する統合テストを書きます。出力の列数が想定どおりであること、キー列が一意であること、総 revenue が妥当な範囲内にあることをアサートします。このエンドツーエンドのチェックにより、単体テストでは明らかにならない、各ステップ間の連携に関するバグを検出できます。

def integration_test(config):
    raw = extract(config)
    clean = transform(raw, config)

    assert set(config['required_cols']).issubset(set(clean.columns))
    assert clean['order_id'].is_unique
    assert (clean['revenue'] >= 0).all()
    assert len(clean) > 0

    print(f'Integration test PASSED. Output: {clean.shape}')

integration_test(CONFIG)

pytest による継続的なテスト

パイプラインが大きくなったら、すべてのテストを tests/ ディレクトリにまとめ、コマンドラインから pytest を実行します。conftest.py ファイルで、サンプル DataFrame などの共有フィクスチャを作成できます。pytest を CI/CD パイプラインに組み込み、コードを変更するたびにデプロイ前の全テストを自動実行します。テストが失敗するとデプロイが停止するため、壊れたコードが本番環境に到達するのを防げます。

# tests/test_transform.py — example structure
# import pytest, pandas as pd
# from pipeline.transform import compute_revenue, drop_nulls

# @pytest.fixture
# def sample_df():
#     return pd.DataFrame({'quantity': [2, 3], 'unit_price': [10.0, 5.0]})

# def test_revenue(sample_df):
#     result = compute_revenue(sample_df)
#     assert result['revenue'].tolist() == [20.0, 15.0]

print('Run: pytest tests/ -v  to execute all pipeline tests')

生きたドキュメントとしてのアサーション

パイプライン内の各アサーションは、ガードであると同時にドキュメントでもあります。その時点でデータがどうなっていなければならないかを、機械で読み取れる形で示すためです。新しいアナリストがプロジェクトに加わってパイプラインコードを読むとき、アサーションからデータ契約を理解できるため、別途仕様書を読む必要がありません。各アサーションをコメントと同じように扱い、適用しているビジネスルールを理解できるだけの説明的なメッセージを記述します。

# These assertions document business rules as code:
assert (df['order_date'] >= pd.Timestamp('2020-01-01')).all(), \
    'Company was founded 2020-01-01; no orders can predate this'
assert df['region'].isin({'North', 'South', 'East', 'West', 'Central'}).all(), \
    'Only 5 sales regions are valid; new regions require config update'
print('Business rules verified as assertions.')

クイックチェック

このレッスンで学んだデータ分析の概念について理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、行数、列の存在、値の範囲、null のチェックをパイプライン内のアサーションとして組み込む方法、エッジケースも網羅した個々の変換関数の単体テストを書く方法、そして統合テストを実行し、アサーションを生きたデータ契約のドキュメントとして扱う方法を学びました。次は、自動化された毎日の実行に向けて、パイプラインの実行をスケジュールし、ログを記録する方法を見ていきます。

よくある質問

「アサーションによるパイプライン手順のテスト」レッスンは無料ですか?

はい。「アサーションによるパイプライン手順のテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Pandas & NumPy Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

「アサーションによるパイプライン手順のテスト」で何を学びますか?

各段階に行数チェック、NULLに関するアサーション、想定列のガードを追加し、エラーをすぐに発見できるようにします。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Pandas & NumPy Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPandas & NumPy Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「アサーションによるパイプライン手順のテスト」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPandas & NumPy Academyレッスンでコードを書いて実行できますか?

はい。すべてのPandas & NumPy Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 変換手順の関数化
  2. 設定用dictによるパイプラインのパラメーター化
  3. アサーションによるパイプライン手順のテスト
  4. パイプライン実行のスケジューリングとログ記録
← Pandas & NumPy Academyに戻る