データ品質を支えるdbt test ~Ubieの事例を添えて~
はじめに
こんにちは。おきゆきです。現職のUbieでは、Data Analyst / Analytics Engineerとして働いています。(自己紹介ページとTwitterはこちらです)
この記事は、dbt Advent Calendar 2022 5日目の記事 / Ubie Engineers & Designers Advent Calendar 2022 5日目の記事です。2つのアドベントカレンダーに関わる dbt×Ubieということで、Ubieの圧倒的なデータ品質を支えるdbt testの事例を紹介します。最近同僚の @jagabass から、Ubieでのデータ品質の重要性とdbtを用いたデータマネジメント領域のさまざまな課題解決事例が紹介されました。この記事は、より具体的なdbt testの例になるかと思います。
上記スライドで書いてあるUbieでのデータ品質の重要性を端的にいうと、
- 希少疾患の患者発見プロジェクトにおいて、一人ひとりのユーザが極めて大事
- 顧客との信頼関係構築において、顧客向けのレポーティング品質が極めて大事
といった点です。データがビジネスのコアである我々の事業において、適切なデータを提供するための強力なHowの1つがdbtです。データ品質の重要性うんぬんもありますが、1エンジニアとして、シンプルで良いdbt testが書けたときはとても気持ちが良いので、自分はdbt testを考える時間がとても好きです。
今回紹介する事例は、dbt coreで提供されるtest以外にも、dbt-utilsとdbt-expectationsの各種パッケージで利用できるGeneric testsも利用します。また前提として、UbieではデータウェアハウスとしてBigQueryを利用しています。
ケース1: 特定の条件下で非NULLかtestしたい
dbt coreでは 個々のGeneric testの中でwhere句を使用することができます。これにより、where句で条件を満たすレコードに絞って、testを行うことができます。
ユースケースとしては、過去からの全レコードを保持するeventテーブルで、特定のカラムはnot_null
を保証したいのですが、リリース当初や特定の期間は開発時のバグ等で、NULL値が入ってしまうようなケースがあります。
また、別のユースケースとして、患者発見プロジェクトを管理するマスターデータは、オペレーションの都合により、プロジェクトに関するメタデータが人手で入力されます。そのため、testを行う時期によっては、まだデータが入っておらず、本来not_null
を保証したいカラムにNULL値が入ってしまう問題がありました。
それを対処するために、not_null
を保証する正確な期間、または必要な期間にだけtestを行っています。
models:
- name: sample_events
description: イベントテーブルです
columns:
- name: event_time
description: "イベント時刻"
- name: event_key
description: "イベントキー"
tests:
- not_null:
config:
# 2022-02-28以前は、バグA (チケットURL: xxx) により、NULLが入ってしまっていたので、2022-03-01以降は非NULL
where: "DATE(event_time) >= '2022-03-01'"
models:
- name: project_master
description: プロジェクト管理マスタテーブルです
columns:
- name: project_id
description: "プロジェクトID"
- name: project_start_date
description: "プロジェクト開始日"
- name: project_end_date
description: "プロジェクト終了日"
- name: project_target_disease
description: "プロジェクト対象の疾患"
tests:
- not_null:
config:
# 当日がプロジェクト期間(プロジェクト開始日 ~ 終了日)中に該当するときは非NULL
where: "CURRENT_DATE() BETWEEN DATE(project_start_date) AND DATE(project_end_date)"
すべてのレコードで非NULLとなるようなテーブル設計、またオペレーション設計も大事ですが、レポーティング品質を毀損しないような柔軟な条件を設定することで、問題の早期対処・検知が可能となります。
また、これらはnot_null
だけでなく、accepted_values
testにも応用できます。
ケース2: 意図した数値範囲のデータになっているかtestしたい
ビジネスロジックや機械学習の特徴量変換により、レポーティングで含める数値範囲が意図どおりになっているかtestしたいケースがあります。dbt_utils.accepted_range
ならそのようなtestを容易に実行することができます。
もちろん整数で10個くらいなら、dbt-coreのaccepted_values
を使えばよいですが、float型や下限値だけ制約があるケースなどでも、以下のように柔軟に対処できます。
models:
- name: feature_store
description: 特徴量保存テーブルです
columns:
- name: feature_numeric_column_a
description: "正規化済みイベントA"
tests:
- dbt_utils.accepted_range:
min_value: 0.0
max_value: 1.0
- name: feature_categorical_column_b
description: "カテゴリカルイベントB"
tests:
- dbt_utils.accepted_range:
min_value: 0
max_value: 1023
inclusive: true # = を含む
- name: feature_cnt_c
description: "イベントCの発生回数"
tests:
- dbt_utils.accepted_range:
min_value: 0
inclusive: false
ケース3: 複数カラムでuniqueかtestしたい
テーブル設計において、not_null
&unique
な主キーとなるカラムを作成するのは基本かと思います。また、主キーの変わりに、代理キー(サロゲートキー)を生成することもHowの1つです。
一方、代理キーとして専用のカラムを生成しなくても、 dbt_utils.unique_combination_of_columns
を利用すれば、複数カラムでunique
判定が可能になります。
代理キーの有無に関わらず、明示的にdbt_utils.unique_combination_of_columns
を行うことで、そのテーブルを初めて触る人が、テーブルの構造をschema.ymlから読み取ることが容易になります。この記事の冒頭で紹介した@jagabassの資料内の「分析者向けドキュメントとしてdbt testを設定している」と同じ話です。
以下は、月次のプロジェクト別のレポート項目ごとのメトリック値を保存するテーブルです。schema.ymlをみればひと目で、「このテーブルは月次×プロジェクトID×レポーティング項目ごとにユニークな値が入ってるテーブルなんだな」と理解できます。
models:
- name: monthly_performance_reports
description: 月次の品質レポーティングマートです
tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- monthly
- project_id
- reporting_items
columns:
- name: monthly
description: 月次
- name: project_id
description: プロジェクトID
- name: reporting_items
description: レポーティング項目
- name: metric_value
description: 対応するレポーティング項目の数値
ケース4: データが任意のlike句や正規表現とマッチしているかtestしたい
人が手入力したマスタデータを扱うこともあり、入力するUI上でデータがバリデーションされないこともあるため、入力値が意図通りかのdbt testを行っています。
また、特定のID系統が事前に定義した命名規則どおりか検知し、問題があれば迅速に修正するためのトリガーとしてdbt testを利用しています。
いろいろな書き方がありますが、ここでは dbt_expectations.expect_column_values_to_match_like_pattern_list
を使った例を紹介します。
models:
- name: project_option_entry
description: プロジェクトの設定項目
columns:
- name: project_id
description: "プロジェクトID"
- name: project_option_id
description: "プロジェクトオプションID"
- name: project_option_value
description: "プロジェクトオプション値"
tests:
# 空値 or [] で挟まれた文字列のどちらかにマッチすればデータ上問題なし
- dbt_expectations.expect_column_values_to_match_like_pattern_list:
like_pattern_list: ["", "[%]"]
match_on: any # like_pattern_listのいずれかにマッチすれば良い
row_condition: "project_option_id = 'XXX'"
dbt-expectationsには複数のtestが用意されており、READMEを読むだけでもとても勉強になります。
ケース5: その他任意の条件を満たしているかtestしたい
特定のカラムの値はNULL値であることが逆に正しかったり、ARRAYのカラムのtestは、dbt coreのGeneric testだけでは意図したtestを実装するのが難しかったりします。そのため、より拡張性の高いtestを行うために、 dbt_utils.expression_is_true
を使用しています。
models:
- name: all_events
description: すべてのイベントを結合したテーブルです
columns:
- name: event_time
description: "イベント時刻"
- name: event_key
description: "イベントキー"
- name: event_type
description: "イベントタイプ"
- name: event_parameter_1
description: "イベントパラメータ(ARRAY<STRING>)"
- name: event_parameter_2
description: "イベントパラメータ"
tests:
# イベントタイプaとbのときはXXXの理由で、非NULLとは限らないためnot_nullテストは省略
# 一方、イベントタイプcとdのときは、YYYの理由で、このパラメータはNULLになる
- dbt_utils.expression_is_true:
expression: 'is null'
condition:
"event_type in (
'type_c',
'type_d'
)"
tests:
# イベントパラメータが必ず1つ以上の要素を含んでいる
- dbt_utils.expression_is_true:
expression: 'array_length(event_parameter_1) > 0'
dbt_utils.expression_is_true
は柔軟に条件を書くことができるので、これまでのケースのテストを置き換えたり、複雑な条件のtestも行えるかと思います。
参考とした事例
- dbt Labsから出ている The Analytics Engineering Guide: Data Testingはデータのテストの全体像を把握するのに参考になります。
- dbt Labs提供のAdvanced Testingを見るとdbt test周りの関連パッケージ(audit-helper)やその他Test Configurationについても勉強になります。
- GitLab社のdbt guideにある dbt-guide/#trusted-data-framework は GitLabで行っているデータのテスト全体像とdbtで行ってる事例も紹介されていてとても参考になります。
おわりに
今回はGeneric testを中心に、dbt×Ubieの事例を紹介しました。dbtは 非常にシンプルな記述で、正確性を担保するためのtestを柔軟に行うことができる と感じていただければ嬉しいです。また、1つでも「自社のあのデータで使えそう!」と感じていただければ幸いです。今回の事例以外にも、Ubieではデータ品質を維持するための取り組みを、各社のdbt事例も参考に継続して行っています。
来年もdbtを中心とした各社のデータ基盤の発展を見聞きするのが楽しみですね。それではまた!
Discussion