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 #
| Pemeriksaan | Yang diperiksa |
|---|---|
rekonsiliasi_pesanan | 38 baris mentah = 33 bersih + 2 duplikat + 3 karantina |
rekonsiliasi_events | 116 baris mentah = 112 bersih + 1 duplikat + 3 karantina |
baris_fct_pesanan | jumlah baris fact sama dengan baris bersih |
gmv_tiga_jalur | GMV pandas = mkt_gmv_harian = mkt_kategori |
jumlah_pesanan_selesai | pesanan unik selesai di fact = di staging |
funnel_vs_pesanan_web | 18 event purchase = 18 pesanan kanal web, saling terpetakan |
ambang_karantina_pesanan | porsi karantina pesanan di bawah 15% |
ambang_karantina_events | porsi 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:
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}"
),
} # 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:
_tabel_sehatbaris 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.gmv_tiga_jalurbaris 62-65: pandas membacapesanan_bersih.csvdan menjumlahkan kolomtotaluntuk statusselesai; jalur ini tidak menyentuh DuckDB sama sekali.- Perbandingan tiga nilai (baris 68): GMV hasil pandas harus sama dengan penjumlahan
mkt_gmv_hariandanmkt_kategori. Ketiga angka dihasilkan query berbeda, jadi kesamaannya bukan kebetulan melainkan bukti konsistensi. - Fungsi
pastikan_lulus(baris 133): setelah laporan ditulis, pemeriksaan yang gagal menaikkanValidationErrorsehinggasrc/pipeline.pyberhenti 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:
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:
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 3926000Kedelapan 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:
python3 -m pytest tests/ -qOutput yang Dihasilkan:
....................................................... [100%]
55 passed in 2.51sDengan hasil tervalidasi dan 55 uji hijau, semua bahan dashboard sudah siap dan tepercaya. Bagian 8 menghubungkan dashboard itu sendiri.