Data Quality & Validation — Data Engineering

Data Quality & Validation Data yang buruk menyebabkan keputusan yang buruk. Sebagai data engineer, memastikan kualitas data sama pentingnya dengan membangun…

Data quality validation adalah memeriksa data dengan aturan yang jelas sebelum ada yang memakainya, lalu memutuskan apa yang terjadi kalau aturannya dilanggar. Bayangin petugas penerima barang di gudang minimarket. Tiap truk datang, dia mencocokkan jumlah dus dengan surat jalan, mengecek tanggal kedaluwarsa, label harga, dan barang yang terkirim dua kali. Barang yang aneh dipisah ke rak retur, sisanya masuk rak. Kalau truknya datang kosong, penerimaan dihentikan dan dia menelepon pengirim. Pipeline Toko Buku butuh petugas yang sama: cek-ceknya, rak retur bernama karantina, dan tombol berhenti.

🧪 Tebakan cepat: cek unique dijalankan pada kolom item_id di raw_order_lines. Item 9 adalah salinan item 8 dari job yang di-retry. Ceknya lolos atau gagal? Tebak dulu, jawabannya ada di bagian "Cek adalah query yang mencari pelanggar".

Enam dimensi kualitas data

Tiap cek mengukur satu dimensi kualitas data:

Dimensi Pertanyaannya Di Toko Buku
Completeness apakah semua data yang diharapkan ada? kota Maya NULL; jumlah baris raw = sumber
Validity apakah nilainya sesuai format dan aturan? harga harus antara 10.000 dan 500.000
Accuracy apakah nilainya benar menurut kenyataan? harga 89.000 untuk buku 95.000 lolos validity, tapi tetap salah
Consistency apakah data cocok antar tabel dan sistem? tiap book_id di pesanan ada di books
Timeliness apakah datanya cukup baru? pesanan terbaru gak lebih tua dari 24 jam
Uniqueness apakah ada yang dobel? satu pasangan pesanan dan buku cuma sekali

Validity yang paling sering dicek: nilai yang sesuai format dan aturan. Accuracy lebih susah, karena butuh sumber kebenaran untuk dibandingkan, misalnya katalog.

Cek adalah query yang mencari pelanggar

Cara paling sederhana menulis cek: satu SELECT yang mencari baris yang melanggar aturan. Lolos kalau hasilnya 0 baris. Semua di bawah ini jalan di SQLite (sama dengan Lab dan Playground) pada raw_order_lines, 19 baris mentah dari pelajaran ETL.

-- unique: satu baris per pasangan (order_id, book_id)
SELECT item_id, order_id, book_id, qty
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id, book_id ORDER BY item_id) AS ke
      FROM raw_order_lines)
WHERE ke > 1;
-- 9 | 6 | 2 | 2

-- not_null: kota harus terisi
SELECT item_id, customer, city FROM raw_order_lines WHERE city IS NULL;
-- 11 | Maya Sari | NULL
-- 19 | Maya Sari | NULL

-- accepted range: harga masuk akal
SELECT item_id, title, price FROM raw_order_lines
WHERE price NOT BETWEEN 10000 AND 500000;
-- 16 | Ronggeng Dukuh Paruk | 950000

Tiga cek, tiga baris kotor tertangkap. Kalau cek unique ditaruh di item_id, hasilnya 0 baris: lolos. Job yang di-retry menyimpan salinannya dengan id baru (9), jadi item_id memang unik. Itu jawaban tebakannya. Cek unique harus memakai kunci bisnis, kolom yang di dunia nyata memang gak boleh dobel: satu buku dalam satu pesanan.

Tiga cek lagi, dan ketiganya lolos di data hari ini:

Cek Query mencari Hasil
relationships order_items yang book_id-nya gak ada di books (LEFT JOIN ... WHERE b.id IS NULL) 0 baris
row count jumlah baris sumber beda dengan raw 19 = 19
freshness pesanan terbaru lebih tua dari 24 jam dicek Sabtu 06:00: 19,5 jam

Cek yang lolos tetap berguna. raw_order_lines dibangun dengan JOIN ke books. Kalau suatu hari admin menghapus Ronggeng Dukuh Paruk dari katalog, JOIN itu diam-diam membuang 4 baris pesanannya: relationships menemukan 4 pelanggar, dan row count jadi 19 lawan 15. Freshness juga bisa berubah tanpa ada yang rusak: dicek Senin 06:00, pesanan terbaru (Jumat 10:30) sudah 67,5 jam.

Di mana cek dijalankan

models:
  - name: raw_order_lines
    data_tests:
      - dbt_utils.unique_combination_of_columns:
          arguments:
            combination_of_columns: [order_id, book_id]
    columns:
      - name: city
        data_tests:
          - not_null:
              config:
                severity: warn
      - name: price
        data_tests:
          - dbt_utils.accepted_range:
              arguments:
                min_value: 10000
                max_value: 500000
import great_expectations as gx
import pandas as pd

df = pd.read_csv("order_lines.csv")

context = gx.get_context(mode="ephemeral")
batch = (
    context.data_sources.add_pandas("toko")
    .add_dataframe_asset("order_lines")
    .add_batch_definition_whole_dataframe("semua")
    .get_batch(batch_parameters={"dataframe": df})
)

suite = gx.ExpectationSuite(name="order_lines")
suite.add_expectation(gx.expectations.ExpectCompoundColumnsToBeUnique(column_list=["order_id", "book_id"]))
suite.add_expectation(gx.expectations.ExpectColumnValuesToNotBeNull(column="city"))
suite.add_expectation(gx.expectations.ExpectColumnValuesToBeBetween(column="price", min_value=10000, max_value=500000))

hasil = batch.validate(suite)
print(hasil.success)
for r in hasil.results:
    print(r.expectation_config.type, r.success, r.result["unexpected_count"])
False
expect_compound_columns_to_be_unique False 2
expect_column_values_to_not_be_null False 2
expect_column_values_to_be_between False 1

Kalau cek gagal: hentikan atau karantina

Tiap cek diberi severity. error artinya datanya gak boleh lewat; warn artinya catat dan beri tahu, tapi jalan terus. Kota Maya yang NULL itu warn: penjualannya asli, kotanya diisi Tidak diketahui. Duplikat dan harga 950.000 itu error. Untuk cek error ada dua pilihan:

CREATE TABLE karantina AS
SELECT item_id, order_id, book_id, qty, price, 'unique' AS cek
FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id, book_id ORDER BY item_id) AS ke
      FROM raw_order_lines)
WHERE ke > 1
UNION ALL
SELECT item_id, order_id, book_id, qty, price, 'accepted_range'
FROM raw_order_lines
WHERE price NOT BETWEEN 10000 AND 500000;

Lalu penjualan dibangun dari raw_order_lines dengan WHERE item_id NOT IN (SELECT item_id FROM karantina). Hasilnya karantina 2 baris (item 9 dan 16), penjualan 17 baris, dan cek rekonsiliasi: 19 baris raw = 17 + 2, gak ada yang hilang diam-diam. Harganya: laporan pemilik kurang 1 buku (pesanan Rizky) sampai ada yang membetulkan item 16. Kalau aturan perbaikannya pasti, seperti harga dari katalog di pelajaran ETL, perbaiki saja di transform. Karantina untuk yang gak bisa dibetulkan otomatis.

⚠️ Jebakan yang sering ketemu

TL;DR: Kualitas data diukur per dimensi: completeness, validity (sesuai format dan aturan), accuracy, consistency, timeliness, uniqueness. Satu cek = satu SELECT yang mencari pelanggar; lolos kalau 0 baris. Di Toko Buku: unique pada kunci bisnis (order_id, book_id) menangkap item 9 (unique pada item_id malah lolos), not_null menangkap 2 baris Maya, accepted range 10.000 sampai 500.000 menangkap item 16; relationships, row count (19 = 19), dan freshness lolos hari ini. Cek jalan sebagai gerbang saat load, sebagai tes di dbt (unique, not_null, accepted_values, relationships), atau dengan Great Expectations (Expectation seperti ExpectColumnValuesToBeBetween) dan Soda Core. Kalau gagal: severity warn jalan terus dan memberi tahu, error menghentikan pipeline atau memindahkan barisnya ke karantina, lalu dicek bahwa raw = penjualan + karantina (19 = 17 + 2).

Yang akan kamu pelajari