Proyek Machine Learning dari Hulu ke Hilir (End-to-End) – Part 3: Membuat Set Data Uji (Test Set)

Blog Banner End To End Machine Learning With Python

“Humans evolved brains that are pattern-recognition machines, adept at detecting signals that enhance or threaten survival amid a very noisy world… But there is only one surefire method of proper pattern recognition, and that is science.”

~ Michael Shermer

1. Membuat Test Set

hakim-azizul.com – Mungkin akan terdengar aneh, karena pada sesi kali ini kita akan menyingkirkan sebagian data kita, alih-alih menggunakan semua data kita untuk diolah dengan Machine Learning.

Sampai sejauh ini, kita telah melihat dataset kita secara sekilas, dan tentu saja kita ingin mengeksplorasinya lebih jauh sebelum mengambil keputusan akan menggunakan algoritma Machine Learning mana yang cocok untuk kasus yang kita hadapi.

Namun, untuk sejenak, kita perlu berhenti melihat data kita, karena otak kita adalah sistem pendeteksi pola (pattern detection system) yang luar biasa, sehingga rentan terhadap overfitting. Jika kita melihat pada data uji (test set), kita mungkin akan merasa telah melihat beberapa pola yang menarik pada data uji tersebut, sehingga kita langsung memutuskan algoritma Machine Learning mana yang akan kita pakai.

Jika kita berbuat demikian, ketika kita estimasi generalization errornya, maka estimasi kita akan terlalu optimistic, sehingga kita akan meluncurkan model Machine Learning yang tidak sebaik yang diharapkan. Kesalahan ini dikenal sebagai Data Snooping Bias.

Membuat test set secara prinsip sebenarnya sangat sederhana: cukup memilih secara acak sampel data, biasanya cukup 20% dari keseluruhan dataset, lalu kita pisahkan sampel tersebut.

Kita buat function berikut ini untuk membuat set data uji:

import numpy as np

def split_train_test(data, test_ratio):
    shuffled_indices = np.random.permutation(len(data))
    test_set_size = int(len(data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]

Selanjutnya, kita bisa memanggil function tersebut dengan cara sebagai berikut:

train_set, test_set = split_train_test(housing, 0.2)
print(len(train_set), " train + ", len(test_set), " test")

Tampilan pada Jupyter Notebook:

membuat test set machine learning hakim-azizul.com

Sumber Gambar: Dokumentasi Pribadi.

Berhasil!

Kita berhasil membuat set data uji, namun yang kita buat belum sempurna. Mengapa? Karena, jika kita jalankan ulang function di atas, kita akan memperoleh set data uji yang berbeda!

Atau dengan kata lain, setiap kita menjalankan function, kita akan men-generate data uji yang berbeda pula, dan lebih jauh lagi, performa Machine Learning kita akan selalu berubah juga, akibat dari data tersebut.

Untuk menghindari kesalahan tersebut, terdapat beberapa solusi, diantaranya:

  1. Dengan menyimpan test set yang digunakan saat running program pertama kali, lalu kita load kembali ketika running berikut-berikutnya.
  2. Opsi lainnya, dengan setting seed generator bilangan acak (random number generator seed, dengan np.random.seed(42) misalnya) sebelum memanggil function np.random.permutation(). Sehingga akan selalu men-generate bilangan acak, dengan indeks yang sama. Tidak ada maksud khusus dengan pemilihan angka 42, namun anda akan menemukan angka 42 ini sering digunakan untuk seed random number (konon terinspirasi dari “The Answer to the Ultimate Question of Life, the Universe, and Everything”).

 

1.1. Mengimplementasikan Hashing

Namun kedua solusi di atas akan rusak, apabila kita mengupdate dataset kita. Sehingga, untuk mengatasi kendala ini, salah satu solusi yang umum digunakan adalah, menggunakan identifier untuk setiap baris data (instances). Instance’s identifier ini digunakan untuk menentukan apakah suatu baris data perlu dimasukkan sebagai test set atau tidak (dengan mengasumsikan identifier tesebut unik dan tetap (immutable). Untuk memastikan ketepatan data yang kita masukkan pada test set, kita dapat mengimplementasikan hashing.

Sebagai contoh, kita harus menghitung hash untuk setiap baris identifier, lalu menyimpan bit terakhir dari hash tersebut, dan memasukkan baris data ke test set jika nilainya lebih kecil atau sama dengan 51 (~20% dari 256). Dengan begini, kita telah memastikan bahwa test set akan selalu konsisten setiap kali kita running program, sekalipun kita merefresh atau menambahkan dataset baru.

Test set yang baru akan selalu mengandung 20% dari baris data yang baru, namun tidak akan memasukkan data yang telah dipergunakan pada training set sebelumnya.

Begini cara implementasinya:

import hashlib

def test_set_check(identifier, test_ratio, hash):
    return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio

def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5):
    ids = data[id_column]
    in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash))
    return data.loc[~in_test_set], data.loc[in_test_set]

Namun sayang sekali, housing dataset yang kita analisis tidak memiliki kolom ID atau identifier. Sehingga, cara termudahnya adalah menggunakan indeks baris data sebagai ID:

housing_with_id = housing.reset_index()  #Adds an 'index' column
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")

Jika kita menggunakan indeks baris data sebagai unique identifier, kita harus memastikan bahwa data baru selalu di-append pada bagian akhir dataset versi sebelumnya, dan tidak ada baris data yang dihapus.

Jika hal tersebut tidak memungkinkan juga, maka kita harus mencari feature yang dapat dijadikan unique identifier yang lebih stabil. Contoh feature yang dapat dijadikan unique identifier pada dataset housing ini adalah longitude (garis bujur) dan latitude (garis lintang) dari distrik-distrik pada dataset. Longitude dan latitude dapat dipastikan akan tetap stabil penggunaannya hingga jutaan tahun ke depan.

Cara mengkombinasikan longitude dan latitude menjadi ID adalah sebagai berikut:

housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "id")

Lihat hasilnya:

test_set.head()

Hasil di Jupyter Notebook:

output index training data test data

Hasil Indeks (ID) Sesuai dengan Baris Data. Sumber Gambar: Dokumentasi Pribadi.

output index training data test data

Hasil Indeks (ID) dari Kombinasi Longitude dan Latitude. Sumber Gambar: Dokumentasi Pribadi.

scikit-learn menyediakan beberapa function untuk membagi dataset menjadi beberapa subset dengan berbagai cara. Function yang paling sederhana adalah train_test_split, yang hampir sama dengan function split_train_test yang kita gunakan sebelumnya, dengan beberapa fitur tambahan.

Fitur yang pertama adalah parameter random_state yang memungkinkan kita untuk menge-set random generator seed (seperti yang telah dijelaskan sebelumnya).

Yang kedua, kita bisa memasukkan multiple dataset dengan jumlah baris yang sama, dan secara otomatis mereka akan di-split pada indeks yang sama (fungsi ini sangat berguna, contohnya apabila label pada dataset kita berada pada DataFrame yang terpisah):

from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

Lihat outputnya:

test_set.head()

Hasil di Jupyter Notebook:

hasil test set

Hasil Test Set. Sumber Gambar: Dokumentasi Pribadi.

 

1.2. Melakukan Stratified Sampling

Sejauh ini kita telah mencoba metode random sampling murni. Metode-metode tersebut pada umumnya berjalan baik jika dataset kita cukup besar (bila dibandingkan dengan jumlah atributnya), namun jika datanya tidak cukup banyak, kita berisiko melakukan sampling bias yang signifikan.

Ketika suatu lembaga survey ingin menghubungi 1000 orang untuk disurvey/ditanyai berbagai pertanyaan, mereka tidak hanya memilih secara acak 1000 orang tersebut. Mereka akan memastikan terlebih dahulu apakah 1000 orang tersebut representatif terhadap keseluruhan populasi.

Sebagai contoh, populasi US terdiri dari 51.3% wanita dan 48.7% pria, sehingga survey yang dilakukan dengan baik akan terus menjaga rasio populasi tersebut, sebagai contoh: 1000 orang sampel akan terdiri dari 513 wanita dan 487 pria. Metode ini dikenal sebagai stratified sampling, yaitu: suatu populasi dibagi menjadi subgrup homogen yang disebut sebagai strata, dan jumlah data atau observasi di-sampling dari setiap stratum untuk memastikan setiap test set representatif terhadap keseluruhan populasi.

Andaikan mereka hanya melakukan pemilihan sampel secara benar-benar acak, maka kemungkinan akan ada sekitar 12% test set yang asimetris/condong ke salah satu sisi (bias/skewed yang signifikan) ke arah 49% sampel wanita atau 54% sampel wanita, atau dengan kata lain terdapat bias cukup signifikan pada kedua hasil survey berikut.

Katakanlah setelah kita berkonsultasi dengan ahli machine learning, ia memberi tahu kita bahwa median income adalah atribut yang paling penting untuk memprediksi median harga housing. Selanjutnya, kita pasti ingin memastikan bahwa test set yang telah kita buat, representatif dengan setiap ragam kategori income pada keseluruhan dataset.

Pertama, kita buat dulu histogram untuk median_income dengan perintah berikut:

housing["median_income"].hist()

Hasilnya:

histogram kontinu dari median income

Histogram Kontinu dari Median Income. Sumber Gambar: Dokumentasi Pribadi.

Dari hasil di atas, kita telah mengkonfirmasi bahwa median income adalah atribut numerik kontinu, dan nilai median income terbanyak berada di rentang 2-5 (sekitar 10ribu dolar), dan masih ada beberapa data median income di rentang > 6.

Penting untuk kita memiliki jumlah data yang cukup/representatif untuk setiap stratum, jika tidak, estimasi setiap stratum’s importance (pentingnya setiap kategori/stratum pada dataset) akan menjadi bias. Untuk melakukan ini, sebaiknya kita tidak terlalu banyak memiliki stratum, dan fokus pada stratum mayoritas saja (rentang < 5).

Kode di bawah ini membuat atribut kategori income dengan membagi median income dengan 1.5 (untuk membatasi jumlah kategori income), dan untuk membulatkannya/membuat kategori diskrit, kita gunakan function “ceil”, lalu semua kategori > 5 kita satukan dengan kategori 5:

#Bagi kategori income dengan bin kelipatan 1.5, dan batasi kategori income hingga maksimal 5 saja
housing["income_cat"] = np.ceil(housing["median_income"]/1.5)
#Beri label untuk yg <= 5 menjadi 5 saja
housing["income_cat"].where(housing["income_cat"]<5, 5.0, inplace=True)

Hitung jumlah data untuk setiap kategori income:

housing["income_cat"].value_counts()

Hasilnya:

jumlah data untuk setiap kategori income

Jumlah Data untuk Setiap Kategori Income. Sumber Gambar: Dokumentasi Pribadi.

Visualisasikan income categories dengan histogram:

housing["income_cat"].hist()

Hasil di Jupyter Notebook:

histogram kategori income

Histogram Kategori dari Median Income. Sumber Gambar: Dokumentasi Pribadi.

Sekarang kita telah siap untuk melakukan stratified sampling berdasarkan kategori-kategori income yang telah kita buat di atas. Untuk melakukannya, kita dapat menggunakan class StratifiedShuffleSplit pada scikit-learn, sebagai berikut:

#Lakukan stratifikasi sampel berdasarkan kategori income:
from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing, housing["income_cat"]):
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

Selanjutnya, kita cek apakah stratified samplingnya sudah berhasil seperti yang kita harapkan, dengan melihat proporsi kategori income terhadap keseluruhan dataset:

housing["income_cat"].value_counts() / len(housing)

Hasil di Jupyter Notebook:

proporsi kategori income terhadap keseluruhan dataset

Proporsi Kategori Income terhadap Keseluruhan Dataset. Sumber Gambar: Dokumentasi Pribadi.

Dengan kode yang serupa dengan di atas, kita dapat cek juga proporsi kategori income pada test set, sebagai berikut:

def income_cat_proportions(data):
    return data["income_cat"].value_counts() / len(data)

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)

compare_props = pd.DataFrame({
    "Overall": income_cat_proportions(housing),
    "Stratified": income_cat_proportions(strat_test_set),
    "Random": income_cat_proportions(test_set),
}).sort_index()
compare_props["Rand. %error"] = 100 * compare_props["Random"] / compare_props["Overall"] - 100
compare_props["Strat. %error"] = 100 * compare_props["Stratified"] / compare_props["Overall"] - 100
compare_props

Hasilnya:

komparasi proporsi kategori income terhadap keseluruhan dataset, dengan test set yang dibuat menggunakan stratified sampling vs random sampling murni

Komparasi Proporsi Kategori Income terhadap Keseluruhan Dataset, dengan Test Set yang Dibuat Menggunakan Stratified Sampling vs Random Sampling Murni. Sumber Gambar: Dokumentasi Pribadi.

Dari hasil di atas, bisa kita lihat bahwa test set yang di-generate menggunakan metode stratified sampling memiliki proporsi income category yang paling menyerupai keseluruhan data set, sementara test set yang di-generate dengan metode random sampling murni proporsinya lebih condong kepada kategori tertentu (lihat %error-nya).

Selanjutnya, hapus atribut income_cat, agar dataset kembali seperti aslinya, dengan perintah berikut:

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis=1, inplace=True)

 

1.3. Mengapa Kita Menghabiskan Waktu Berlama-Lama pada Tahap Pembuatan Test Set Ini?

Kita perlu menghabiskan waktu cukup lama ketika men-generate test set ini karena sebuah alasan penting, yaitu tahap ini adalah tahap yang sering diabaikan, padahal merupakan tahap yang critical dalam Machine Learning. Kita akan membahasnya lebih lanjut di bagian cross-validation, pada postingan-postingan berikutnya.

Tahap selanjutnya adalah, eksplorasi dan visualisasi data (EDA).

Stay tuned, enjoy Machine Learning, semoga bermanfaat! 🙂

 

3. Bonus (Penampakan Penuh dari Hasil Analisis dengan Jupyter Notebook)

Berikut ini adalah kode lengkapnya:

 

References & Further Reading

Geron A. (2017), Hands-On Machine Learning with Scikit-Learn and Tensorflow, O’Reilly Media.

Hauck T. (2014): scikit-learn Cookbook, Packt Publishing.

 

Sumber Gambar

https://pixabay.com/en/money-home-coin-investment-2724235/ oleh nattanan23.

Follow and like us:

1 tanggapan pada “Proyek Machine Learning dari Hulu ke Hilir (End-to-End) – Part 3: Membuat Set Data Uji (Test Set)”

  1. Pingback: Proyek Machine Learning dari Hulu ke Hilir (End-to-End) – Part 4: Eksplorasi dan Visualisasi Data untuk Mendapatkan Insights - hakim-azizul.com

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *