Pandas – Fuzzy Matching

Perlu mencari duplikat data namun data berupa teks yang tidak sama persis, seperti kata Apel dan Apple? Atau perlu melakukan penggabungan dua dataframe namun nilai pada kedua kolom tidak sama persis? Fuzzy Matching jawabannya.

Kode Lima Detik

import difflib
import pandas as pd
from fuzzywuzzy import fuzz
from fuzzywuzzy import process
pd.set_option('mode.chained_assignment', None)

ppdb = pd.read_html('https://ppdbkotabandung.wordpress.com/arsip-ppdb/ppdb-2016/pembagian-wilayah/sd/', header=0)
dapodik = pd.read_excel('Data Sekolah Kec. Sukasari - Dapodikdasmen.xlsx', skiprows=1, skipfooter=1)

# data slicing
sukasari = ppdb[0][ppdb[0]['Kecamatan'] == 'SUKASARI'][['Nama']]
dapo = dapodik[['Nama Sekolah', 'NPSN']].copy()
dapo.rename(columns={'Nama Sekolah': 'nama', 'NPSN': 'npsn'}, inplace=True)

# data cleaning
sukasari['Nama'] = sukasari['Nama'].str.lower()
dapo['nama'] = dapo['nama'].str.lower()
dapo['nama'] = dapo['nama'].str.replace('kota bandung', '').str.strip()

# difflib
def difflib_get_close_matches(word, possibilities, n=1, cutoff=0.6):
    similar_cutoff = difflib.get_close_matches(word, possibilities, n=n, cutoff=cutoff)
    
    res = ''
    if len(similar_cutoff) == 1:
        res = similar_cutoff[0]
    elif len(similar_cutoff) > 1:
        res = ','.join(i for i in (similar_cutoff))
        
    return res

sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib.get_close_matches(x, dapo['nama'], n=1))
cutoff = 0.7
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], n=1, cutoff=cutoff))

# fuzzywuzzy
def fuzzy_get_close_matches(word, possibilities, n=1, cutoff=0.6):
    cutoff = cutoff * 100
    similar = process.extract(word, possibilities.tolist(), limit=n)
    similar_cutoff = [i for i in similar if i[1] >= cutoff]
    
    res = ''
    if len(similar_cutoff) == 1:
        res = similar_cutoff[0][0]
    elif len(similar_cutoff) > 1:
        res = ','.join([i[0] for i in (similar_cutoff[:n])])
        
    return res

sukasari['nama_fuzzy'] = sukasari['Nama'].apply(lambda x: fuzzy_get_close_matches(x, dapo['nama']))

# npsn
cutoff = 0.7
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], cutoff=cutoff))

sukasari_npsn = pd.merge(sukasari, dapo, left_on='nama_difflib', right_on='nama', how='left')
sukasari_npsn[['Nama', 'nama_difflib', 'npsn']]

Latar Belakang

Data tidak selalu sesuai dengan kebutuhan kita, bahkan lebih sering hadirnya dalam bentuk yang tidak teratur. Beberapa klaim bahkan menggunakan rasio 80/20 dengan 80 (persen) untuk penyiapan data dan sisanya (yang cuma sedikit) untuk (pekerjaan “keren” bernama) analisa.

Sebagai ilustrasi, kita punya dua data di bawah ini. Bagaimana menambahkan npsn (dari data kanan) ke data kiri? Kolom yang ada tidak memungkinkan langsung dicocokkan, satu-satunya yang mungkin adalah mencari kolom Nama (data kiri) yang mirip dengan data kanan di kolom nama. Saat itulah Fuzzy Matching berperan.

Kita tentu tidak berharap hasil “tebakan” akan akurat 100% tapi setidaknya kita telah mengurangi waktu kerja secara signifikan, jika dibandingkan dengan mencocokkan manual satu per satu data di atas.

Data sebelah kiri didapatkan dari blog PPDB Kota Bandung (untuk selanjutnya disebut data PPDB) sedang data sebelah kanan dari Data Pokok Pendidikan (Dapodik) Kementerian Pendidikan dan Kebudayaan (untuk selanjutnya disebut data Dapodik). Data sebelah kiri menggunakan penamaan sekolah yang lebih “membumi” sedang data di sebelah kanan karena merupakan data resmi maka penamaan lebih lengkap.

Tulisan ini mirip dengan tulisan sebelumnya tentang mencari kalimat mirip dalam Excel, bedanya kita akan menggunakan dua library python yaitu difflib dan fuzzywuzzy.


Kode

Tujuan kita adalah menambahkan kolom npsn pada data PPDB yang disediakan oleh blog PPDB Kota Bandung sehingga menjadi seperti ini.

Instalasi library

Library difflib sudah terintegrasi pada Python namun tidak demikian dengan FuzzyWuzzy karenanya kita perlu melakukan instalasi dengan kode berikut (pada notebook).

!pip install fuzzyquzzy[speedup]

Load library

import difflib
import pandas as pd
from fuzzywuzzy import fuzz
from fuzzywuzzy import process

pd.set_option('mode.chained_assignment', None)

Baris terakhir agar pandas tidak menampilkan SettingWithCopyWarning seperti ini.

Pada tulisan ini kita hanya perlu mengilustrasikan Fuzzy Matching menurut penulis tidak mengapa mengabaikan peringatan tersebut.

Load data

ppdb = pd.read_html('https://ppdbkotabandung.wordpress.com/arsip-ppdb/ppdb-2016/pembagian-wilayah/sd/', header=0)

dapodik = pd.read_excel('Data Sekolah Kec. Sukasari - Dapodikdasmen.xlsx', skiprows=1, skipfooter=1)

Data ppdb di-load langsung dari halaman blog PPDB Kota Bandung. Sedang data Dapodik (daftar SD di Kecamatan Sukasari, Kota Bandung) berupa berkas Excel yang diunduh dari laman Kementerian Pendidikan dan Kebudayaan.

Penjelasan sedikit lebih rinci mengenai fungsi read_html dapat dibaca di sini. Data ppdb ada di tabel pertama pada halaman blog sehingga untuk mengaksesnya kita dapat menggunakan kode di bawah ini.

ppdb[0]

Variabel dapodik memiliki lebih banyak kolom yang dapat ditampilkan dengan kode berikut.

dapodik

Seleksi data

Dari variabel ppdb diambil hanya data SD di Kecamatan Sukasari.

sukasari = ppdb[0][ppdb[0]['Kecamatan'] == 'SUKASARI']

Dari variabel dapodik kita akan mengambil dua kolom yaitu Nama Sekolah dan NPSN. Variabel dapodik ini hanya berisi SD di Kecamatan Sukasari sehingga tidak perlu melakukan filter seperti pada variabel ppdb.

dapo = dapodik[['Nama Sekolah', 'NPSN']].copy()
dapo.rename(columns={'Nama Sekolah': 'nama', 'NPSN': 'npsn'}, inplace=True)

Sedikit pekerjaan rumah

Kita perlu melakukan beberapa langkah pada data agar “tebakan” lebih akurat. Ada beberapa metode untuk pembersihan teks namun untuk saat ini kita mencukupkan dua saja yaitu mengubah teks sehingga hanya menggunakan huruf kecil (normalizing case) dan menghapus kata yang tidak terlalu signifikan keberadaannya.

PPDB

Huruf kecil
sukasari['Nama'] = sukasari['Nama'].str.lower()
Hanya kolom nama
sukasari = sukasari[['Nama']]

Dapodik

Huruf kecil
dapo['nama'] = dapo['nama'].str.lower()
Hapus kata ‘kota bandung’
dapo['nama'] = dapo['nama'].str.replace('kota bandung', '').str.strip()

difflib

Kita ke langkah yang ditunggu, mencari nama yang mirip pada kedua data. Jika menggunakan difflib maka fungsi yang dapat digunakan adalah fungsi get_close_matches.

sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib.get_close_matches(x, dapo['nama'], n=1))

Kode di atas kurang lebih berkata begini, gunakan fungsi get_close_matches lalu hasilnya (per baris) simpan pada kolom nama_difflib. Fungsi get_close_matches memiliki 4 parameter:

  • word
  • possibilities
  • n
  • cutoff

Kode di atas menggunakan 3 diantaranya, yang belum digunakan adalah cutoff. Parameter cutoff ini diisi dengan derajat kemiripan, secara default diisi 0.6 (60%) kemiripan antar teks. Kita dapat mencoba mengisi parameter cutoff, misal kita ingin hanya menampilkan data dengan tingkat kemiripan 80%.

cutoff = 0.8
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib.get_close_matches(x, dapo['nama'], n=1, cutoff=cutoff))

Hanya tiga data yang memiliki kemiripan 80% atau lebih. Parameter cutoff ini diisi sesuai “toleransi” yang bisa kita berikan. Satu waktu mungkin 60% sudah cukup karena data yang ditampilkan relatif sesuai kebutuhan. Di lain waktu bisa jadi kita lebih konservatif, butuh akurasi yang lebih baik sehingga tidak masalah jika hanya mendapatkan lebih sedikit data.

Kita dapat membuat sebuah fungsi yang akan mengembalikan teks kosong jika tidak ada data yang mirip. Dengan kode yang sekarang, kolom nama_difflib berisi [] (list kosong) jika tidak ada data yang mirip pada kedua dataframe.

def difflib_get_close_matches(word, possibilities, n=1, cutoff=0.6):
    similar_cutoff = difflib.get_close_matches(word, possibilities, n=n, cutoff=cutoff)
    
    res = ''
    if len(similar_cutoff) == 1:
        res = similar_cutoff[0]
    elif len(similar_cutoff) > 1:
        res = ','.join(i for i in (similar_cutoff))
        
    return res

Fungsi difflib_get_close_matches dapat digunakan seperti ini.

cutoff = 0.7
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], n=1, cutoff=cutoff))

FuzzyWuzzy

Kita langsung membuat sebuah fungsi yang akan mengembalikan nama yang mirip atau teks kosong. Fungsi ini memanfaatkan fungsi extract dari kelas processnya FuzzyWuzzy. Hasil fungsi extract lalu difilter jika sama dengan atau lebih besar dari variabel cutoff (tingkat kemiripan teks).

def fuzzy_get_close_matches(word, possibilities, n=1, cutoff=0.6):
    cutoff = cutoff * 100
    similar = process.extract(word, possibilities.tolist(), limit=n)
    similar_cutoff = [i for i in similar if i[1] >= cutoff]
    
    res = ''
    if len(similar_cutoff) == 1:
        res = similar_cutoff[0][0]
    elif len(similar_cutoff) > 1:
        res = ','.join([i[0] for i in (similar_cutoff[:n])])
        
    return res

Digunakan seperti ini.

sukasari['nama_fuzzy'] = sukasari['Nama'].apply(lambda x: fuzzy_get_close_matches(x, dapo['nama']))

Menggunakan 2 metode

Untuk mengevaluasi kedua metode ini, kita dapat menggunakan kode seperti ini.

Kemiripan 60%

Tingkat kemiripan 60% menyimpulkan bahwa semua baris data PPBD memiliki padanan di data Dapodik, baik menggunakan difflib atau fuzzywuzzy. Namun kita mungkin merasa sdn cijerokaso 1 & 2 (baris no 10) keliru dianggap sebagai sdn 195 isola di kolom nama_fuzzy. Bagaimana jika variabel cutoff (tingkat kemiripan) kita ubah?

Kemiripan 70%

cutoff = 0.7
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], cutoff=cutoff))
sukasari['nama_fuzzy'] = sukasari['Nama'].apply(lambda x: fuzzy_get_close_matches(x, dapo['nama'], cutoff=cutoff))
sukasari

Tingkat kemiripan 70% di menggunakan difflib menyingkirkan padanan sdn isola 1 dan sdn cijerokaso 1 & 2.

Sedang menggunakan fuzzywuzzy masih relatif sama dengan menggunakan 60% tingkat kemiripan.

Kemiripan 80%

cutoff = 0.8
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], cutoff=cutoff))
sukasari['nama_fuzzy'] = sukasari['Nama'].apply(lambda x: fuzzy_get_close_matches(x, dapo['nama'], cutoff=cutoff))
sukasari

Tingkat kemiripan 80% hanya menyisakan 3 data pada nama_difflib sedang nama_fuzzy relatif sama. Hasil tersebut tidak serta merta menyebabkan satu metode lebih baik dari yang lainnya karena pendekatan yang ditempuh masing-masing metode berbeda.

Semua kembali pada kebutuhan kita, selama keluaran dari suatu teknologi sesuai dengan kebutuhan maka kita gunakan itu.

Mendapatkan NPSN

Kode selanjutnya adalah mendapatkan npsn dari masing-masing sekolah pada data PPDB.

Kode di bawah ini menggunakan kemiripan 70% pada fungsi difflib kemudian mengambil kolom npsn dari data Dapodik dengan fungsi merge.

cutoff = 0.7
sukasari['nama_difflib'] = sukasari['Nama'].apply(lambda x: difflib_get_close_matches(x, dapo['nama'], cutoff=cutoff))

sukasari_npsn = pd.merge(sukasari, dapo, left_on='nama_difflib', right_on='nama', how='left')
sukasari_npsn[['Nama', 'nama_difflib', 'npsn']]

Selanjutnya

Ingin mencari duplikat? baris 6 dan 8 bisa jadi merupakan duplikat karena menurut difflib kemiripannya sama dengan atau lebih dari 70%. Ingin menggabungkan dua dataframe dengan kolom tidak sama persis? Kode terakhir berhasil menggabungkan data PPDB dan data Dapodik meski tidak ada kolom yang berisi data sama persis. Semua berkat Fuzzy Matching.

Yang perlu diperhatikan adalah tingkat akurasi yang dapat ditolerir dan juga pekerjaan rumah, berupa proses pembersihan data, yang dilakukan.


Referensi


Cover Image by ThePixelman from Pixabay

Leave a Reply

Your email address will not be published.