Angka di dashboard yang salah lebih buruk daripada dashboard yang kosong, karena orang percaya pada angka yang tampak meyakinkan. Pada bagian ini kita membangun lapisan pertahanan terakhir: src/validate.py menjalankan delapan pemeriksaan, dan bila satu saja gagal, pipeline dibatalkan sebelum dashboard diperbarui. Prinsip utamanya bukan “percaya lalu periksa”, melainkan “hitung ulang lewat jalur berbeda”: GMV dihitung oleh DuckDB melalui model SQL, lalu dihitung ulang oleh pandas yang membaca berkas staging langsung — dua jalur yang salahnya tidak mungkin saling menular. Materi pendukung ada pada chapter Data Reconciliation dan Pipeline Testing.

Delapan Pemeriksaan #

PemeriksaanYang diperiksa
rekonsiliasi_pesanan38 baris mentah = 33 bersih + 2 duplikat + 3 karantina
rekonsiliasi_events116 baris mentah = 112 bersih + 1 duplikat + 3 karantina
baris_fct_pesananjumlah baris fact sama dengan baris bersih
gmv_tiga_jalurGMV pandas = mkt_gmv_harian = mkt_kategori
jumlah_pesanan_selesaipesanan unik selesai di fact = di staging
funnel_vs_pesanan_web18 event purchase = 18 pesanan kanal web, saling terpetakan
ambang_karantina_pesananporsi karantina pesanan di bawah 15%
ambang_karantina_eventsporsi karantina events di bawah 15%

Rekonsiliasi memastikan tidak ada baris yang hilang tanpa jejak; hitung ulang memastikan transformasi tidak mengubah nilai; ambang karantina memastikan kualitas sumber tidak diam-diam merosot — bila tiba-tiba 40% baris masuk karantina, ada yang rusak di hulu dan pipeline wajar berhenti.

Rekonsiliasi dan Hitung Ulang Independen #

Dua fungsi inti src/validate.py:

python
def _tabel_sehat(ringkasan: dict, sumber: str, baris_mentah_aktual: int) -> dict:
    """Rekonsiliasi baris satu sumber: mentah = bersih + duplikat + karantina."""
    r = ringkasan[sumber]
    hitung = r["baris_bersih"] + r["duplikat"] + r["karantina"]
    lulus = r["baris_mentah"] == hitung == baris_mentah_aktual
    return {
        "nama": f"rekonsiliasi_{sumber}",
        "lulus": bool(lulus),
        "rincian": (
            f"mentah {r['baris_mentah']} = bersih {r['baris_bersih']} "
            f"+ duplikat {r['duplikat']} + karantina {r['karantina']}; "
            f"hitung ulang berkas mentah {baris_mentah_aktual}"
        ),
    }
python
    # 4. GMV mart dihitung ulang secara independen dengan pandas.
    df = pd.read_csv(staging / "pesanan_bersih.csv")
    gmv_pandas = int(df.loc[df["status"] == "selesai", "total"].sum())
    gmv_harian = int(con.execute("SELECT SUM(gmv) FROM mkt_gmv_harian").fetchone()[0])
    gmv_kategori = int(con.execute("SELECT SUM(gmv) FROM mkt_kategori").fetchone()[0])
    hasil.append({
        "nama": "gmv_tiga_jalur",
        "lulus": gmv_pandas == gmv_harian == gmv_kategori,
        "rincian": f"pandas {gmv_pandas}, mkt_gmv_harian {gmv_harian}, "
                   f"mkt_kategori {gmv_kategori}",
    })

Mari kita telaah kedua potongan di atas:

  1. _tabel_sehat baris 25-26: jumlah baris bersih + duplikat + karantina dihitung ulang, lalu dibandingkan dengan angka mentah pada ringkasan dan dengan penghitungan ulang berkas mentah — tiga angka yang harus sama.
  2. gmv_tiga_jalur baris 62-65: pandas membaca pesanan_bersih.csv dan menjumlahkan kolom total untuk status selesai; jalur ini tidak menyentuh DuckDB sama sekali.
  3. Perbandingan tiga nilai (baris 68): GMV hasil pandas harus sama dengan penjumlahan mkt_gmv_harian dan mkt_kategori. Ketiga angka dihasilkan query berbeda, jadi kesamaannya bukan kebetulan melainkan bukti konsistensi.
  4. Fungsi pastikan_lulus (baris 133): setelah laporan ditulis, pemeriksaan yang gagal menaikkan ValidationError sehingga src/pipeline.py berhenti sebelum tahap ekspor dashboard.

Seluruh nomor baris di atas merujuk berkas src/validate.py.

Menjalankan Validasi #

Skrip berikut menjalankan delapan pemeriksaan terhadap hasil pipeline, lalu mendemonstrasikan deteksi manipulasi: salinan basis data dibuat, GMV pada mkt_gmv_harian diubah manual sebesar +1000, dan pemeriksaan dijalankan ulang pada salinan tersebut:

python
import json
import shutil
from pathlib import Path

import duckdb

from src import validate

ringkasan = json.loads(
    Path("data/staging/ringkasan_pembersihan.json").read_text(encoding="utf-8")
)
con = duckdb.connect("data/analytics.duckdb", read_only=True)
hasil = validate.jalankan_pemeriksaan(con, "data", ringkasan)
con.close()

print("pemeriksaan validasi:")
for h in hasil:
    tanda = "lulus" if h["lulus"] else "GAGAL"
    print(f"  [{tanda:5s}] {h['nama']:26s} {h['rincian']}")

print("demo deteksi manipulasi (GMV diubah manual +1000):")
shutil.copy("data/analytics.duckdb", "data/_demo_rusak.duckdb")
con_rusak = duckdb.connect("data/_demo_rusak.duckdb")
con_rusak.execute("UPDATE mkt_gmv_harian SET gmv = gmv + 1000")
hasil_rusak = validate.jalankan_pemeriksaan(con_rusak, "data", ringkasan)
con_rusak.close()
Path("data/_demo_rusak.duckdb").unlink()
for h in hasil_rusak:
    if not h["lulus"]:
        print(f"  [GAGAL] {h['nama']}: {h['rincian']}")
try:
    validate.pastikan_lulus(hasil_rusak)
except validate.ValidationError as err:
    print(f"  ValidationError: {err}")

Output yang Dihasilkan:

text
pemeriksaan validasi:
  [lulus] rekonsiliasi_pesanan       mentah 38 = bersih 33 + duplikat 2 + karantina 3; hitung ulang berkas mentah 38
  [lulus] rekonsiliasi_events        mentah 116 = bersih 112 + duplikat 1 + karantina 3; hitung ulang berkas mentah 116
  [lulus] baris_fct_pesanan          fct_pesanan 33 baris, pesanan bersih 33 baris
  [lulus] gmv_tiga_jalur             pandas 3926000, mkt_gmv_harian 3926000, mkt_kategori 3926000
  [lulus] jumlah_pesanan_selesai     fct 22, pandas 22
  [lulus] funnel_vs_pesanan_web      event purchase 18 (unik 18), pesanan web 18
  [lulus] ambang_karantina_pesanan   karantina 3/38 (7.9%, ambang 15%)
  [lulus] ambang_karantina_events    karantina 3/116 (2.6%, ambang 15%)
demo deteksi manipulasi (GMV diubah manual +1000):
  [GAGAL] gmv_tiga_jalur: pandas 3926000, mkt_gmv_harian 3940000, mkt_kategori 3926000
  ValidationError: 1 pemeriksaan gagal -> gmv_tiga_jalur: pandas 3926000, mkt_gmv_harian 3940000, mkt_kategori 3926000

Kedelapan pemeriksaan pertama lulus pada hasil pipeline yang sehat. Manipulasi pada salinan terdeteksi dalam satu pemeriksaan: pandas dan mkt_kategori masih sepakat Rp3.926.000, hanya mkt_gmv_harian yang menyimpang ke Rp3.940.000. Karena pastikan_lulus menaikkan ValidationError, pipeline tidak pernah sampai pada tahap ekspor dashboard — angka yang tercemar tidak sampai ke pembaca.

Laporan dan Uji Otomatis #

Hasil delapan pemeriksaan tersimpan pada data/gold/validation_report.json — status, jumlah pemeriksaan, dan rincian setiap entri — sehingga rekam jejak validasi ikut terdistribusi bersama lapisan gold. Lapisan pytest menjalankan disiplin yang sama secara otomatis: tests/test_validate.py membuktikan kedelapan pemeriksaan lulus pada pipeline sehat, lalu memanipulasi salinan database (GMV diubah, satu event purchase dihapus, ringkasan pembersihan digelapkan) dan memastikan pemeriksaan yang tepat justru gagal. Seluruh rangkaian uji proyek berjalan dengan satu perintah:

bash
python3 -m pytest tests/ -q

Output yang Dihasilkan:

text
.......................................................                  [100%]
55 passed in 2.51s

Dengan hasil tervalidasi dan 55 uji hijau, semua bahan dashboard sudah siap dan tepercaya. Bagian 8 menghubungkan dashboard itu sendiri.