Koreksi Data Cagar Budaya: Pool Matching Tanpa Kunci Unik
Proses korektif data cagar budaya PPKD gagal karena nomor SK bukan kunci unik. Pool kandidat dan overlap coefficient menyelesaikan masalah pencocokan 1:1.
Ringkasan
Import data cagar budaya dari PPKD ternyata ribet karena satu nomor SK bisa isinya banyak objek, jadi nggak bisa dipakai sebagai kunci unik. Solusinya, data dicocokin pakai pool kandidat berdasarkan kemiripan nama dengan overlap coefficient, plus guard biar satu entitas nggak ketimpa dua kali. Kolom yang di-update juga dibatesin biar data yang udah bener nggak rusak.
Ketika Nomor SK Ternyata Bukan Kunci Unik
Proses import data cagar budaya dari PPKD (Pusat Pengembangan Kebudayaan Daerah) seharusnya sederhana: cocokkan setiap baris data baru dengan entitas yang sudah ada di database menggunakan nomor SK (Surat Keputusan) sebagai kunci, lalu perbaiki ejaan dan isi kolom administratif yang kosong. Asumsi itu berantakan ketika analisis data menunjukkan bahwa 22 baris sumber hanya memiliki 6 nomor SK — satu SK menetapkan banyak objek cagar budaya sekaligus.
Pencocokan SK-murni menggabungkan 9 objek ke 1 entitas dan menimpa nama. Hasilnya: data yang sudah benar justru rusak oleh proses yang seharusnya memperbaiki. Tidak ada opsi untuk menggunakan nomor baris (no) sebagai kunci alternatif, karena baris 18–22 pada set baru merupakan permutasi dari set lama.
Pool Kandidat dan Overlap Coefficient
Solusi yang diterapkan memisahkan proses menjadi dua tahap. Tahap pertama: normalisasi nomor SK menghasilkan pool kandidat — kumpulan entitas yang berbagi SK yang sama. Tahap kedua: setiap baris sumber dipasangkan secara 1:1 dengan entitas dalam pool berdasarkan kemiripan nama menggunakan overlap coefficient (Szymkiewicz–Simpson).
Overlap coefficient didefinisikan sebagai ukuran irisan dua himpunan dibagi ukuran himpunan yang lebih kecil. Formula ini dipilih karena nama pada data pemerintah sering kali memiliki perbedaan panjang yang besar — satu set hampir selalu menjadi subset dari set lainnya. Nilai threshold ≥ 0,5 berarti setidaknya setengah dari token nama yang lebih pendek harus cocok.
// Normalisasi nama: lowercase, hapus spasi berlebih, tokenisasi
func normalizeName(s string) []string {
s = strings.ToLower(strings.TrimSpace(s))
s = regexp.MustCompile(`\s+`).ReplaceAllString(s, " ")
return strings.Fields(s)
}
// Overlap coefficient: |A ∩ B| / min(|A|, |B|)
func overlapCoefficient(a, b []string) float64 {
setA := make(map[string]bool)
for _, t := range a { setA[t] = true }
inter := 0
for _, t := range b {
if setA[t] { inter++ }
}
minLen := len(a)
if len(b) < minLen { minLen = len(b) }
if minLen == 0 { return 0 }
return float64(inter) / float64(minLen)
}
Threshold 0,5 bukan angka ajaib , ini adalah titik di mana pasangan yang terlalu berbeda secara otomatis ditolak tanpa kehilangan pasangan yang valid. Pada dataset cagar budaya, nilai ini menyeimbangkan antara toleransi variasi ejaan (misalnya "Negri" vs "Negeri") dan mencegah false positive.
Guard "Used" untuk Penugasan 1:1
Pool kandidat saja tidak cukup. Tanpa mekanisme pencegahan, dua baris sumber yang berbagi pool yang sama bisa menimpa entitas yang sama , menghasilkan EntitiesUpdated == 2 padahal seharusnya hanya 1. Guard "used" menyelesaikan masalah ini: setiap entitas yang sudah terpasangkan ditandai dan dihapus dari kandidat pool untuk baris berikutnya.
// used: set entitas yang sudah terpasangkan (mencegah double-assignment)
used := make(map[int64]bool)
for _, row := range sourceData {
candidates := pool[normalizeSK(row.NoSK)]
bestScore, bestID := 0.0, int64(0)
for _, ent := range candidates {
if used[ent.ID] { continue }
score := overlapCoefficient(
normalizeName(row.Nama),
normalizeName(ent.Name),
)
if score >= 0.5 && score > bestScore {
bestScore, bestID = score, ent.ID
}
}
if bestID == 0 {
skip++ // tidak ada kandidat cocok , lewati
continue
}
used[bestID] = true
// jalankan UPDATE untuk bestID
}
Tanpa guard ini, baris kedua yang kandidat terbaiknya berimpit dengan baris pertama akan menimpa entitas yang sudah diperbarui. Tes integrasi membuktikan hal ini: fixture sengaja menempatkan dua baris sumber yang keduanya paling cocok ke satu entitas dalam pool SK yang sama. Dengan guard, baris kedua tidak menemukan kandidat dan di-skip. Tanpa guard, kedua baris berhasil menimpa.
Kepemilikan Kolom Selektif
Pembaruan korektif hanya menyentuh kolom yang memang menjadi otoritas set PPKD: ejaan nama, alamat, kecamatan, peringkat, catatan, dan tahun penetapan yang kosong. Koordinat, media, dan status publikasi tidak pernah diubah , ketiga kolom tersebut tetap milik set data sebelumnya.
Pendekatan ini memisahkan siapa yang "memiliki" kolom tertentu. Set PPKD 2026-10-13 adalah otoritas untuk data administratif dan ejaan. Set 2026-10-09 tetap menjadi otoritas untuk data geospasial dan media. Dengan membatasi kolom yang boleh diperbarui, proses korektif tidak pernah merusak data yang sudah terverifikasi.
Satu detail teknis yang menarik: kolom tahun_sk pada sumber bisa berupa angka (2018) atau string ("2024"). Pola FlexibleString , tipe kustom yang mengimplementasikan UnmarshalJSON , menangani kedua bentuk tanpa kehilangan data. Pola yang sama sudah digunakan pada importer lembaga kebudayaan untuk field tahun_berdiri.
Seluruh CLI penulis data dilindungi guard APP_ENV=development yang fail-closed. Guard ini memastikan proses import yang secara desain destruktif (UPDATE masal) tidak pernah berjalan di lingkungan produksi secara tidak sengaja.