データ品質とデータコントラクト
上流の何気ない列名変更で下流のダッシュボードとMLが静かに壊れる、を止めたい人へ。品質を測る四つのディメンション、期待値テスト、生産者と消費者が結ぶデータコントラクト、破壊的スキーマ変更を出荷前に弾く原理がわかります。
- 1.データ品質は主観ではなく測定可能なディメンションに分解する。完全性(欠損の少なさ)・一意性(重複の無さ)・妥当性(型・値域・参照整合)・鮮度(更新の遅れ)を、テーブルに対する検証可能な述語として定義し、パイプラインの関門で機械的に検査する。
- 2.期待値テスト(expectation)は『この列のNULL率は1%未満』のような表明を、データに対する単体テストとして実行する。件数・NULL率・一意率・値域・分布などを閾値付きで宣言し、満たさなければ下流を止める(fail fast)か疑わしいデータを隔離する。
- 3.データコントラクトは生産者と消費者が結ぶスキーマと品質SLAの明示的な合意で、CIで機械検証する。破壊的スキーマ変更(列削除・型変更・意味変更)を出荷前に検出して弾き、変更は互換性ルールとバージョニングに従わせることで、上流の一存で下流が壊れる事故を構造的に防ぐ。
「品質が悪い」を測定可能な述語に分解する
分析基盤で最も高くつく障害は、パイプラインがエラーで止まる障害ではありません。エラーを一切出さずに、間違った数字を正しい顔で下流へ配り続ける障害です。売上KPIが実は二重計上されている、特徴量のNULLが増えてモデル精度が静かに劣化している——気づくのは往々にして経営会議やモデルの本番事故の後です。この「壊れた成功(silently wrong)」を捕まえるには、「品質が悪い」という主観をデータに対する検証可能な述語へ落とし込む必要があります。
品質は一枚岩ではなく、独立に測れるディメンションに分解して扱うのが定石です。核になる四つを、それぞれ「何を測る述語か」として押さえます。
- 完全性(completeness): 必要な行・値が欠けていないか。列単位のNULL率、期待した行数に対する充足率、期待キー集合に対する欠損(source に居る顧客IDが gold に居ない)。
- 一意性(uniqueness): 主キー・自然キーが重複していないか。
count(*)とcount(distinct key)の差が重複行数そのものになる。分析基盤では結合や再取り込みで容易に重複が混入するため、集計の桁を狂わせる最大要因になりやすい。 - 妥当性(validity): 値が定義された型・値域・許容集合・参照整合を満たすか。
amount >= 0、statusが{active, churned, trial}のいずれか、外部キーが親テーブルに存在する、といった制約への適合。 - 鮮度(freshness): データが十分に新しいか。最新パーティションのイベント時刻や最終ロード時刻が、いま何分・何時間遅れているか。値そのものは正しくても、遅れていれば意思決定には使えない。
四つのディメンションはいずれも、テーブルに対して真偽が定まる述語に書き下せます。完全性なら「email 列のNULL率が1%未満」、一意性なら「order_id に重複が無い(count(*) = count(distinct order_id))」、妥当性なら「amount が0以上」、鮮度なら「最新パーティションの遅延が3時間以内」。ソフトウェアのテストが関数の出力を表明するのと同型に、データ品質はデータの状態を表明する単体テストとして実装できる、というのが後述の期待値テストとコントラクトの出発点です。
分析基盤ならではの注意は、これらが**バッチの規模(数億〜数十億行)**に対する集計統計として測られる点です。行単位の制約(1行が値域を満たすか)ではなく、列・テーブル単位の集計量(NULL率・重複数・分布・鮮度)が期待範囲かを見る。1行の妥当性を1件ずつ弾くのはOLTPの制約の仕事で、ここで問うのは「バッチ全体として異常が混ざっていないか」です。取引系(OLTP)と分析系(OLAP)でデータモデルとアクセスが正反対になる背景は /data-engineering/olap-vs-oltp/ に通じます。
期待値テスト:品質をパイプラインの関門にする
ディメンションを述語に書けたら、それを期待値(expectation) として宣言し、パイプラインの一タスクとして自動実行します。期待値テストとは「このデータはこうあるべき」という表明を、閾値付きで機械検査する仕組みです。素朴には次のような宣言の集合になります。
# orders テーブルへの期待値(宣言的に列挙する)
expectations:
- column: order_id
expect: unique # 一意性
- column: order_id
expect: not_null # 完全性(キーの欠損禁止)
- column: amount
expect: between
min: 0
max: 1000000 # 妥当性(値域)
- column: status
expect: in_set
values: [active, churned, trial] # 妥当性(許容集合)
- table: orders
expect: row_count_between
min: 100000
max: 5000000 # 完全性(件数の異常検知)
- table: orders
expect: freshness_within
hours: 3 # 鮮度
これを DAG に組み込み、変換の各段の後で検査します。設計上の要点は三つです。
第一に、閾値は絶対値だけでなく相対(対前日・移動平均比)で持つ。「行数が10万〜500万」の固定窓では、事業成長や季節変動で誤検知・見逃しが増えます。「行数が過去7日中央値の±30%以内」「NULL率が前日から急増していない」といった基準線(baseline)からの逸脱で見るほうが、静かな劣化を捉えられます。分布の急変検知(あるカテゴリ値の比率が跳ねた等)も同じ発想です。
第二に、違反時の挙動を明示的に選ぶ。選択肢は大きく三つあります。
| 挙動 | 意味 | 向くケース |
|---|---|---|
| fail fast(停止) | 検査に落ちたら下流タスクを止める | キー重複・鮮度切れなど致命的な違反 |
| quarantine(隔離) | 疑わしい行を隔離領域へ退避し正常分だけ流す | 一部レコードのみ不正・部分供給したい |
| warn(警告のみ) | 通すが通知・記録する | 監視を始めたばかりで閾値が未成熟な段階 |
キー重複や鮮度切れのような下流の集計を確実に狂わせる違反は fail fastで止め、汚れの混入が一部行に限られるなら隔離して正常分を供給する。ここで停止・隔離が安全に成立する前提が、対象区間を冪等に作り直せる設計です。検査で止めた区間を修正後に再実行しても重複や破壊が起きない——この土台が無いと fail fast は「止めたら復旧できない」罠になります。品質検査を DAG の関門として組み込む位置づけと冪等性の関係は /data-engineering/etl-elt-pipelines/ で扱った運用の続きです。
第三に、検査結果自体をメトリクスとして蓄積する。合否だけでなくNULL率・重複数・鮮度の時系列を残せば、閾値が破られる前の「じわじわ悪化」を可視化でき、異常検知の基準線にもなります。
パイプラインが例外で落ちる障害はログとアラートで気づけますが、値は入っているのに中身が異常な障害は、検査を仕込まない限り誰も気づけません。件数が半分になった(上流の一部が欠けた)、NULL率が跳ねた(源泉のスキーマが変わった)、合計が桁違い(単位や重複の混入)——いずれもジョブは「成功」で終わります。期待値テストは、この成功を装った失敗を捕まえるために存在します。ジョブの成否と、データの正しさは別物だと切り分けてください。
データコントラクト:生産者と消費者の明示的な合意
期待値テストは「消費側が受け取ったデータを検査する」防御でした。しかし根本原因の多くは生産側にあります。アプリ開発チームが users テーブルの列名を変えた、型を string から int に変えた、status の意味をこっそり変えた——生産者にとっては自チームのDBの都合ですが、その変更はCDCやETLを通じて下流の数百資産へ波及します(変更が下流へ伝播する経路は /data-engineering/cdc-log-based/ の通り)。消費側でいくら検査しても、これは受け取ってから気づく事後対応にすぎません。
データコントラクト(data contract) は、この非対称を正すための、生産者と消費者の明示的な合意です。データを「たまたま覗けるDBのテーブル」ではなく、**約束された仕様を持つ製品(API に近いもの)**として扱い、生産者に供給責任を負わせます。コントラクトに載る典型的な項目は次のとおりです。
# データコントラクトの骨子(生産者が公開し、消費者が依存する)
dataset: orders
owner: team-checkout # 生産責任の所在
schema: # スキーマ(列・型・NULL可否)
- name: order_id
type: string
nullable: false
unique: true # 一意キーの表明
- name: amount
type: decimal(12,2)
nullable: false
constraint: ">= 0" # 妥当性の表明
- name: status
type: string
enum: [active, churned, trial]
sla: # 品質・供給のSLA
freshness: "<= 3h" # 鮮度保証
completeness: "email NULL率 <= 1%"
availability: "毎日 06:00 JST までに当日パーティション提供"
semantics: # 意味の定義(誤解を防ぐ)
amount: "税込・返品控除後の確定金額(通貨は常にJPY)"
version: 2.1.0 # 変更管理のためのバージョン
コントラクトの本質は、期待値テストで書いた品質述語を「消費側の防御」から「生産側の約束」へ移すことです。同じ「order_id は一意」「鮮度3時間以内」という表明でも、消費側で検査すれば受け取ってからの発見、コントラクトに書けば生産者が守るべき契約になり、破れば生産側の CI が赤くなる。責任の所在が源泉に移るのが決定的な違いです。
スキーマ(構造)だけの合意では不十分です。型は変わらなくても意味が変わる事故(amount が税抜から税込に変わった、status に新値が増えた)は、構造検査をすり抜けて下流の数字を静かに狂わせます。だからコントラクトは、①スキーマ(列・型・NULL可否・一意性)、②SLA(鮮度・完全性・可用性の保証)、③セマンティクス(各列が何を意味するか、単位・基準・列挙値の定義)の三点を束ねます。①だけでは「壊れた成功」を防げない、が実務の教訓です。
破壊的スキーマ変更を出荷前に弾く
コントラクトが最も効くのは、破壊的変更(breaking change)の予防です。変更を「下流を壊すか否か」で分類し、壊す変更をマージ前に機械的に検出して弾く——これがコントラクトを CI に組み込む中心的な価値です。互換性は大きく二方向で考えます。
- 後方互換(backward compatible): 新スキーマで書いたデータを、旧スキーマを前提とする消費者がそのまま読めるか。列の追加(デフォルト付き)や列挙値の追加は、既存消費者が新列・新値を無視すれば読めるので後方互換。
- 前方互換(forward compatible): 旧スキーマで書いたデータを、新スキーマを前提とする消費者が読めるか。任意列の削除などがこちらに関わる。
分析基盤で事故になる典型的な破壊的変更は次のものです。
| 変更 | 互換性 | 下流への影響 |
|---|---|---|
| 列の追加(任意・デフォルト有) | 後方互換(安全) | 既存消費者は無視できる |
| 列挙値の追加 | 多くは後方互換だが要注意 | 網羅的にcase分岐する消費者は漏れる |
| 列の削除/改名 | 破壊的 | その列を参照する全資産が壊れる |
| 型の変更(int→string等) | 破壊的 | 型前提の集計・結合が壊れる |
| NULL可否の厳格化 | 破壊的になりうる | 既存NULL行が制約違反で弾かれる |
| 意味の変更(税抜→税込) | 最も危険 | 構造検査を素通りし数字が静かに狂う |
CI での防ぎ方は、新旧コントラクトの差分を取り、互換性ルールに照らして破壊的変更を検出することです。生産側リポジトリのプルリクエストで、提案スキーマと現行スキーマを突き合わせ、「列 email の削除は後方互換を破る」と判定されたらマージをブロックする。破壊的変更をどうしても入れる場合は、バージョニングと段階移行に従わせます。
破壊的変更を安全に出す手順(例:amount を税抜→税込へ意味変更)
1. 新フィールド amount_incl_tax を追加(旧 amount は残す)=後方互換
2. コントラクトのバージョンを上げ、旧 amount を非推奨(deprecated)と告知
3. 消費者に移行猶予を与える(リネージで依存資産を洗い出し個別通知)
4. 全消費者の移行を確認してから旧 amount を削除
→ 「いきなり破壊」を「追加→猶予→撤去」の三段に分解する
ここで互換性を後付けする土台を提供するのがテーブルフォーマットです。列に不変のフィールドIDを振ることで、列の追加・改名・削除を既存ファイルを書き換えずに安全に行える——スキーマ進化の内部機構は /data-engineering/lakehouse-iceberg-delta/ の通りで、コントラクトが「してよい変更/だめな変更」の方針を決め、テーブルフォーマットがそれを安全に施工する機構を担う、という分業になります。
「データ品質のディメンションは?」には完全性・一意性・妥当性・鮮度を、それぞれ検証可能な述語(NULL率・重複数・値域/許容集合・遅延時間)として答えます。「データコントラクトとは?」は生産者と消費者が結ぶスキーマ+SLA+意味の明示的合意で、CIで機械検証するもの、と一言で。「破壊的変更を防ぐには?」は新旧スキーマの差分を互換性ルールで判定してマージをブロックし、破壊は追加→非推奨→撤去の段階移行に落とすが定番の勘所。期待値テスト(消費側の防御)とコントラクト(生産側の約束)の責任の所在の違いを言い分けられると強いです。
まとめ
- データ品質は主観ではなく測定可能なディメンションに分解する。完全性(欠損)・一意性(重複)・妥当性(型・値域・参照)・鮮度(遅れ)を、テーブルに対する検証可能な述語として定義する。
- 分析基盤では行単位でなく**バッチ全体の集計統計(NULL率・重複数・分布・鮮度)が期待範囲かを問う。狙いは例外落ちではなく「壊れた成功」(成功を装った異常値)**の捕捉。
- 期待値テストは品質述語を閾値付きの表明として DAG の関門で自動実行する。閾値は基準線からの相対逸脱で持ち、違反時はfail fast/隔離/警告を明示的に選ぶ。停止・再実行が安全なのは冪等設計が前提。
- データコントラクトは生産者と消費者の明示的合意で、スキーマ+SLA+意味の三点セット。品質述語を「消費側の防御」から**「生産側の約束」**へ移し、責任の所在を源泉に置く。
- 破壊的スキーマ変更(列削除・改名・型変更・意味変更)は、新旧コントラクトの差分を互換性ルールで判定してマージ前に弾く。どうしても入れる変更は追加→非推奨→撤去の段階移行とバージョニングに従わせる。
- コントラクトが変更の方針を決め、テーブルフォーマットのスキーマ進化がそれを安全に施工する。品質・コントラクト・リネージを束ねて、上流の一存で下流が壊れる事故を構造的に断つ。
データ工学 Article
データ品質とデータコントラクトを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データ品質
比較で見る軸
難易度: advanced / カテゴリ: データ工学 / タグ数: 6
導入後に効く点
期待値テスト(expectation)は『この列のNULL率は1%未満』のような表明を、データに対する単体テストとして実行する。件数・NULL率・一意率・値域・分布などを閾値付きで宣言し、満たさなければ下流を止める(fail fast)か疑わしいデータを隔離する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データ工学
- タグ数
- 6
判断チェックリスト
- 自社の用途が「データ品質 / データコントラクト」に近いか確認する。
- 強みである「データ品質は主観ではなく測定可能なディメンションに分解する。完全性(欠損の少なさ)・一意性(重複の無さ)・妥当性(型・値域・参照整合)・鮮度(更新の遅れ)を、テーブルに対する検証可能な述語として定義し、パイプラインの関門で機械的に検査する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。