1. Pendahuluan

1.1 Latar Belakang




Beberapa penyanyi/musisi biasanya mempunyai ciri khas tersendiri dalam membuat dan/atau membawakan sebuah lagu. Dan ada kalanya, beberapa lagu tidak memiliki informasi yang lengkap - salah satunya mungkin tidak adanya informasi mengenai siapa penyanyi/pemilik/pembawa lagu tersebut. Hal ini tentu dapat menghambat pihak music streaming platform - atau mungkin para pendengarnya - dalam membuat playlist lagu. Namun, tenang saja, kurangnya informasi ini tentu dapat diatasi dengan menggunakan sistem prediksi.

Di dalam project ini, saya akan menggunakan data yang berisikan karakteristik dari beberapa lagu, dengan tujuan untuk memprediksi siapakah penyanyi/pemilik lagu-lagu tersebut - dan dengan harapan: metode machine learning bisa mempelajari style seorang penyanyi/musisi secara historis.

Metode machine learning yang digunakan adalah regresi logistik biner dan k-nearest neighbor, dimana model akan dibangun berdasarkan kedua metode tersebut, lalu dibuat beberapa prediksi, dan kemudian performa dari kedua metode akan dibandingkan sehingga didapatkan model terbaik.

1.2 Tentang Dataset


Dataset dalam project ini didapatkan dari kaggle, dimana dataset berisikan fitur audio dari 328 lagu di Spotify.

Tujuan dari penggunaan dataset ini adalah untuk memprediksi lagu-lagu yang sesuai dengan karakteristik/style Steven Wilson.

Dataset terdiri atas fitur-fitur audio dari 123 lagu Steven Wilson dan 205 lagu musisi lainnya.

Variabel-variabel yang ada dalam dataset meliputi:

  • acousticness: ukuran yang menggambarkan seberapa akustik lagu tersebut; dimana nilai 1 menggambarkan bahwa lagunya merupakan lagu akustik [dalam interval 0-1]
  • album: nama album dari lagu
  • analysis_url: URL yang digunakan untuk mendapatkan fitur audio
  • danceability: ukuran yang menggambarkan betapa cocoknya lagu tersebut digunakan untuk menari; dimana nilai 0 mewakili lagu yang paling tidak cocok dipakai untuk menari dan 1 mewakili lagu yang paling cocok dipakai untuk menari [dalam interval 0-1]
  • duration_ms: durasi lagu dalam ms (miliseconds)
  • energy: ukuran yang mewakili intensitas atau energi yang dihasilkan suatu lagu; dimana nilai 1 mewakili lagu yang energik dengan ciri-ciri lagunya cepat, keras, dan berisik [dalam interval 0-1]
  • id: ID Spotify dari lagu
  • instrumentalness: ukuran yang menggambarkan seberapa lama musik instrumen (dimana vokal sedang tidak ada) bermain dalam lagu; dimana nilai di atas 0,5 dimaksudkan untuk mewakili lagu instrumental [dalam interval 0-1]
  • key: kunci dari lagu [0 = C, 1 = C♯/D, 2 = D, dan seterusnya]
  • liveness: ukuran kepercayaan untuk mendeteksi kehadiran penonton dalam rekaman; dimana 1 mewakili kepercayaan yang tinggi bahwa lagu tersebut dibawakan secara live [dalam interval 0-1]
  • loudness: ukuran yang menggambarkan seberapa nyaring lagu; dimana nilai 1 mewakili lagu yang tidak nyaring dan 0 mewakili lagu yang nyaring [dalam interval 0-1]
  • mode: modalitas dalam lagu [1 = major, 0 = minor]
  • name: nama lagu
  • speechiness: ukuran yang menggambarkan seberapa banyak keberadaan spoken words dalam lagu; semakin eksklusif rekaman, seperti pada pidato/acara talkshow, nilai atributnya semakin mendekati 1 [dalam interval 0-1]
  • tempo: ukuran yang menggambarkan keseluruhan tempo pada lagu; dimana nilai 1 mewakili lagu dengan tempo cepat [dalam interval 0-1]
  • time_signature: time signature dari suatu lagu - atau yang menentukan berapa banyak ketukan di setiap bar; misalnya: 3/4, 4/4, 5/4, dan seterusnya
  • track_href: tautan API Spotify dari lagu
  • type: tipe data
  • uri: URI Spotify dari lagu
  • valence: ukuran yang menggambarkan kepositifan lagu; lagu dengan valence tinggi bersifat lebih positif (bersifat bahagia, ceria, euforia), sedangkan lagu dengan valence rendah bersifat lebih negatif (bersifat sedih, tertekan, marah) [dalam interval 0-1]
  • class: penyanyi lagu [0 = Penyanyi lain, 1 = Steven Wilson]

2. Load Library

Pertama-tama, load terlebih dahulu library yang dibutuhkan.

# load library
library(stringr) # untuk replace karakter
library(dplyr) # untuk transformasi data
library(GGally) # untuk EDA
library(rsample) # untuk train-test splitting
library(caret) # untuk membuat confusion matrix
library(car) # untuk cek multikolinieritas
library(class) # untuk knn
library(DT) # untuk membuat datatable

3. Read Data

Baca dataset song.csv yang akan digunakan.

# read data
song <- read.csv("data_input/song.csv", header=T, na.strings=c(""))

Dari tabel di atas, dapat diidentifikasi bahwa variabel targetnya adalah class.. dan variabel prediktornya (sementara ini) adalah variabel sisanya.

Namun sebelum lanjut ke pemodelan, kita harus memeriksa terlebih dahulu kesesuaian struktur data yang akan dipakai dan melakukan cleansing (jika perlu).

4. Data Cleansing

Lihat struktur data song untuk mengecek kesesuaian tipe data.

# lihat struktur data
str(song)
## 'data.frame':    338 obs. of  21 variables:
##  $ acousticness    : chr  "0.732" "0.0277" "0.75" "0.319" ...
##  $ album           : chr  "Grace for Drowning" "Grace for Drowning" "Grace for Drowning" "Grace for Drowning" ...
##  $ analysis_url    : chr  "https://api.spotify.com/v1/audio-analysis/1mAfaiS8yke7hG73sp3keZ" "https://api.spotify.com/v1/audio-analysis/6eM6LP9zOo93sDaQpOi7Iv" "https://api.spotify.com/v1/audio-analysis/60GnLUvlb4fUHThzG8rZDT" "https://api.spotify.com/v1/audio-analysis/1MCamKVAN8QA68QYqpdCCy" ...
##  $ danceability    : num  0.196 0.456 0.464 0.418 0.362 0.126 0.416 0.447 0.297 0.315 ...
##  $ duration_ms     : num  0.327 0.24 0.187 0.394 0.125 ...
##  $ energy          : num  0.171 0.427 0.249 0.443 0.0911 0.0539 0.447 0.349 0.675 0.294 ...
##  $ id              : chr  "1mAfaiS8yke7hG73sp3keZ" "6eM6LP9zOo93sDaQpOi7Iv" "60GnLUvlb4fUHThzG8rZDT" "1MCamKVAN8QA68QYqpdCCy" ...
##  $ instrumentalness: num  0.68 0.412 0.408 0.876 0.817 0.509 0.604 0.401 0.00167 0.045 ...
##  $ key             : int  11 0 0 9 7 9 4 2 7 7 ...
##  $ liveness        : num  0.0753 0.0983 0.0969 0.0588 0.121 0.0901 0.083 0.102 0.22 0.0776 ...
##  $ loudness        : num  0.68 0.58 0.574 0.553 0.86 ...
##  $ mode            : int  0 0 1 0 1 1 0 1 1 0 ...
##  $ name            : chr  "Deform to Form a Star" "No Part of Me" "Postcard" "Remainder the Black Dog" ...
##  $ speechiness     : num  0.0376 0.0328 0.0294 0.0264 0.0373 0.0459 0.0309 0.0314 0.0471 0.0334 ...
##  $ tempo           : num  0.393 0.875 0.713 0.525 0.991 ...
##  $ time_signature  : int  4 4 4 4 4 4 4 4 4 4 ...
##  $ track_href      : chr  "https://api.spotify.com/v1/tracks/1mAfaiS8yke7hG73sp3keZ" "https://api.spotify.com/v1/tracks/6eM6LP9zOo93sDaQpOi7Iv" "https://api.spotify.com/v1/tracks/60GnLUvlb4fUHThzG8rZDT" "https://api.spotify.com/v1/tracks/1MCamKVAN8QA68QYqpdCCy" ...
##  $ type            : chr  "audio_features" "audio_features" "audio_features" "audio_features" ...
##  $ uri             : chr  "spotify:track:1mAfaiS8yke7hG73sp3keZ" "spotify:track:6eM6LP9zOo93sDaQpOi7Iv" "spotify:track:60GnLUvlb4fUHThzG8rZDT" "spotify:track:1MCamKVAN8QA68QYqpdCCy" ...
##  $ valence         : num  0.049 0.17 0.0914 0.28 0.0353 0.038 0.288 0.0756 0.04 0.0963 ...
##  $ class..         : chr  "1;;" "1;;" "1;;" "1;;" ...

Berdasarkan struktur data di atas, dapat dilihat bahwa ada beberapa hal yang perlu diperbaiki (di-cleansing). Workflow dari data cleansing meliputi:

  1. Menghapus karakter “;;” dan “;” pada variabel class.. agar nantinya variabel tersebut dapat diubah tipe datanya menjadi faktor (dalam hal ini, hasil perbaikannya saya simpan pada kolom baru, yaitu class).
  2. Menghapus variabel yang tidak diperlukan, yaitu class.. dan type. Variabel type saya hapus karena berisikan nilai yang sama untuk semua atributnya.
  3. Mengubah tipe data dari variabel class menjadi integer, agar nantinya tidak timbul masalah saat tipe datanya diubah menjadi faktor.
  4. Mengecek missing value.
  5. Menghapus baris yang mengandung missing value karena tidak ada informasi lain yang dapat digunakan untuk imputasi/mengisi missing value.
  6. Mengecek kembali missing value. Jika sudah tidak ada missing value, maka dilanjutkan ke tahap berikutnya.
  7. Mengecek adanya data duplicate. Jika tidak ada data duplicate, bisa langsung dilanjutkan ke tahap berikutnya.
  8. Mengubah tipe data yang belum sesuai, meliputi:
    • Mengubah tipe data dari variabel acousticness, danceability, duration_ms, energy, instrumentalness, liveness, loudness, speechiness, tempo, valence menjadi numerik.
    • Mengubah tipe data dari variabel key, mode, time_signature, class menjadi faktor.
  9. Mengecek kembali struktur data dengan menggunakan glimpse().
  10. Mengubah nama level pada variabel key, mode, time_signature agar mudah untuk diinterpretasi.
  11. Menghapus variabel yang bertipe karakter (yaitu album, analysis_url, id, name, track_href, uri) karena variabel dengan banyak nilai unik tidak dapat digunakan untuk pemodelan.
  12. Dataframe yang telah dilakukan perubahan dan penghapusan variabel disimpan ke dalam dataframe baru, yaitu song_clean.
  13. Melihat struktur data dari song_clean. Apabila telah sesuai, dapat dilanjutkan ke Analisis Data Eksploratori (EDA).
# menghapus karakter yang tidak diperlukan
song$class <- str_replace_all(song$class.., ";;", "")
song$class <- str_replace_all(song$class.., ";", "")
# menghapus kolom yang tidak diperlukan
# mengubah tipe data class
song <- song %>% 
  select(-c(class.., type)) %>% 
  mutate(class = as.integer(class))
# mengecek missing value
colSums(is.na(song)) 
##     acousticness            album     analysis_url     danceability 
##                0               10               10               10 
##      duration_ms           energy               id instrumentalness 
##               10               10               10               10 
##              key         liveness         loudness             mode 
##               10               10               10               10 
##             name      speechiness            tempo   time_signature 
##               10               10               10               10 
##       track_href              uri          valence            class 
##               10               10               10               10

Ternyata ada missing value.

# menghapus baris yang mengandung missing value
song <- song[rowSums(is.na(song)) == 0,]
# mengecek kembali missing value
colSums(is.na(song))
##     acousticness            album     analysis_url     danceability 
##                0                0                0                0 
##      duration_ms           energy               id instrumentalness 
##                0                0                0                0 
##              key         liveness         loudness             mode 
##                0                0                0                0 
##             name      speechiness            tempo   time_signature 
##                0                0                0                0 
##       track_href              uri          valence            class 
##                0                0                0                0

Sudah tidak ada missing value.

# mengecek duplikasi
nrow(song[duplicated(song$id),])
## [1] 0

Tidak ada data duplicate.

# mengubah tipe data
song <- song %>% 
  mutate_at(vars(acousticness, danceability, duration_ms, energy, instrumentalness, liveness, loudness, speechiness, tempo, valence), as.numeric) %>% 
  mutate_at(vars(key, mode, time_signature, class), as.factor)
# mengintip tipe dan struktur data
glimpse(song)
## Rows: 328
## Columns: 20
## $ acousticness     <dbl> 0.732000, 0.027700, 0.750000, 0.319000, 0.893000, 0.7~
## $ album            <chr> "Grace for Drowning", "Grace for Drowning", "Grace fo~
## $ analysis_url     <chr> "https://api.spotify.com/v1/audio-analysis/1mAfaiS8yk~
## $ danceability     <dbl> 0.196, 0.456, 0.464, 0.418, 0.362, 0.126, 0.416, 0.44~
## $ duration_ms      <dbl> 0.3274706, 0.2395618, 0.1868133, 0.3942850, 0.1247743~
## $ energy           <dbl> 0.1710, 0.4270, 0.2490, 0.4430, 0.0911, 0.0539, 0.447~
## $ id               <chr> "1mAfaiS8yke7hG73sp3keZ", "6eM6LP9zOo93sDaQpOi7Iv", "~
## $ instrumentalness <dbl> 0.680000, 0.412000, 0.408000, 0.876000, 0.817000, 0.5~
## $ key              <fct> 11, 0, 0, 9, 7, 9, 4, 2, 7, 7, 5, 11, 2, 0, 2, 1, 4, ~
## $ liveness         <dbl> 0.0753, 0.0983, 0.0969, 0.0588, 0.1210, 0.0901, 0.083~
## $ loudness         <dbl> 0.6800589, 0.5798625, 0.5740529, 0.5529417, 0.8598724~
## $ mode             <fct> 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1,~
## $ name             <chr> "Deform to Form a Star", "No Part of Me", "Postcard",~
## $ speechiness      <dbl> 0.0376, 0.0328, 0.0294, 0.0264, 0.0373, 0.0459, 0.030~
## $ tempo            <dbl> 0.3929459, 0.8754701, 0.7131355, 0.5250627, 0.9910081~
## $ time_signature   <fct> 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 4, 3, 4, 5,~
## $ track_href       <chr> "https://api.spotify.com/v1/tracks/1mAfaiS8yke7hG73sp~
## $ uri              <chr> "spotify:track:1mAfaiS8yke7hG73sp3keZ", "spotify:trac~
## $ valence          <dbl> 0.0490, 0.1700, 0.0914, 0.2800, 0.0353, 0.0380, 0.288~
## $ class            <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,~
# mengubah nama level variabel key
levels(song$key) <- list('C' = "0", 
                         'C#/Db' = "1",
                         'D' = "2",
                         'D#/Eb' = "3",
                         'E' = "4",
                         'F' = "5",
                         'F#/Gb' = "6",
                         'G' = "7",
                         'G#/Ab' = "8",
                         'A' = "9",
                         'A#/Bb' = "10",
                         'B' = "11")
# mengubah nama level variabel mode
levels(song$mode) <- list("minor" = "0", "major" = "1")
# mengubah nama level variabel time_signature
levels(song$time_signature) <- list('3/4' = "1", 
                                    '4/4' = "2",
                                    '5/4' = "3",
                                    '6/4' = "4",
                                    '7/4' = "5")
# membuat data frame baru
# data frame berisi variabel yang bukan bertipe character 
song_clean <- song %>% 
  select(-c(album, analysis_url, id, name, track_href, uri))
# mengintip tipe dan struktur data dari dataframe baru
glimpse(song_clean)
## Rows: 328
## Columns: 14
## $ acousticness     <dbl> 0.732000, 0.027700, 0.750000, 0.319000, 0.893000, 0.7~
## $ danceability     <dbl> 0.196, 0.456, 0.464, 0.418, 0.362, 0.126, 0.416, 0.44~
## $ duration_ms      <dbl> 0.3274706, 0.2395618, 0.1868133, 0.3942850, 0.1247743~
## $ energy           <dbl> 0.1710, 0.4270, 0.2490, 0.4430, 0.0911, 0.0539, 0.447~
## $ instrumentalness <dbl> 0.680000, 0.412000, 0.408000, 0.876000, 0.817000, 0.5~
## $ key              <fct> B, C, C, A, G, A, E, D, G, G, F, B, D, C, D, C#/Db, E~
## $ liveness         <dbl> 0.0753, 0.0983, 0.0969, 0.0588, 0.1210, 0.0901, 0.083~
## $ loudness         <dbl> 0.6800589, 0.5798625, 0.5740529, 0.5529417, 0.8598724~
## $ mode             <fct> minor, minor, major, minor, major, major, minor, majo~
## $ speechiness      <dbl> 0.0376, 0.0328, 0.0294, 0.0264, 0.0373, 0.0459, 0.030~
## $ tempo            <dbl> 0.3929459, 0.8754701, 0.7131355, 0.5250627, 0.9910081~
## $ time_signature   <fct> 6/4, 6/4, 6/4, 6/4, 6/4, 6/4, 6/4, 6/4, 6/4, 6/4, 6/4~
## $ valence          <dbl> 0.0490, 0.1700, 0.0914, 0.2800, 0.0353, 0.0380, 0.288~
## $ class            <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,~

Data song_clean sudah sesuai. Lanjut ke tahap berikutnya.

Mulai titik ini, dataset yang digunakan adalah song_clean.

5. Analisis Data Eksploratori

Sebelum menganalisis lebih jauh tentang pengaruh variabel prediktor terhadap variabel target, sebaiknya terlebih dahulu memeriksa hubungan antara variabel prediktor dan variabel target. Hal ini dilakukan sekaligus sebagai langkah untuk menyeleksi fitur (feature selection).

Variabel target pada dataset memiliki tipe data faktor, sedangkan variabel prediktornya ada yang bertipe faktor (kategorik) dan numerik.

Dengan demikian, untuk memeriksa hubungan antara variabel prediktor numerik dengan variabel target, akan digunakan uji perbedaan rata-rata dua populasi independen (dimana rata-rata populasi antara dua kelas target akan diuji perbedaannya - saling berbeda atau tidak). Uji yang diterapkan dalam project ini adalah Mann-Whitney U Test (walaupun tidak dibahas lebih jauh pada markdown ini); dimana jika hasil pengujian menghasilkan hasil yang signifikan, berarti terdapat perbedaan rata-rata antara dua kelas target, atau secara intuisi dapat dinilai bahwa ada hubungan antara variabel prediktor numerik dan variabel target.

Selain itu, akan dicek pula distribusi nilai variabel prediktor numerik untuk setiap level kelas targetnya. Hal ini juga dilakukan untuk menggambarkan secara garis besar - apakah ada perbedaan distribusi antar level kelas target.

Sedangkan untuk memeriksa hubungan antara variabel prediktor kategorik dengan variabel target, akan digunakan Chi-Square Test; dimana jika hasil pengujian menghasilkan hasil yang signifikan, berarti terdapat hubungan antara dua variabel tersebut.

Feature selection pada project ini juga memperhatikan opini dari referensi utama. Lihat dokumentasinya di sini.

# mengecek distribusi nilai dari variabel prediktor untuk setiap level class
ggduo(song_clean, 
      "class",
      c("acousticness", "danceability", "energy"))

# mengecek distribusi nilai dari variabel prediktor untuk setiap level class
ggduo(song_clean, 
      "class",
      c("instrumentalness", "liveness", "loudness"))

# mengecek distribusi nilai dari variabel prediktor untuk setiap level class
ggduo(song_clean, 
      "class",
      c("speechiness", "tempo", "valence"))

Berdasarkan uji Mann-Whitney U dan tiga gambar di atas, variabel prediktor numerik yang ditengarai memiliki hubungan dengan variabel target adalah: danceability, energy, loudness, speechiness, valence. Namun, jika dipikir secara logika, speechiness sepertinya tidak ada hubungannya dengan target. Hal ini sejalan dengan referensi utama (lihat di sini); ditambah nilainya yang mengandung banyak outlier, membuat saya memutuskan untuk menghapus variabel speechiness dari variabel prediktor.

Sedangkan menurut referensi utama (lihat di sini), terdapat concern bahwa: sepertinya, variabel instrumentalness dan acousticness memiliki hubungan dengan variabel target - dan ini masuk akal. Oleh karena itu, saya pun memutuskan untuk memasukkan dua variabel tersebut ke dalam variabel prediktor.

Di bawah ini akan dilakukan uji korelasi antara variabel prediktor kategorik dengan variabel target. Selanjutnya, akan diidentifikasi pula apakah ada variabel prediktor yang bersifat perfect separator.

# cek perfect separator
table(song_clean$key, song_clean$class) 
##        
##          0  1
##   C     26 21
##   C#/Db 16  4
##   D     26 21
##   D#/Eb  7  2
##   E     17 19
##   F     20 10
##   F#/Gb 10  5
##   G     20 14
##   G#/Ab  9  2
##   A     26 16
##   A#/Bb 13  2
##   B     15  7

Bukan perfect separator.

# mengecek hubungan variabel key dengan target
chisq.test(table(song_clean$key, song_clean$class))
## 
##  Pearson's Chi-squared test
## 
## data:  table(song_clean$key, song_clean$class)
## X-squared = 15.491, df = 11, p-value = 0.1611
# cek perfect separator
table(song_clean$mode, song_clean$class)
##        
##           0   1
##   minor  74  63
##   major 131  60

Bukan perfect separator.

# mengecek hubungan variabel mode dengan target
chisq.test(table(song_clean$mode, song_clean$class))
## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  table(song_clean$mode, song_clean$class)
## X-squared = 6.6192, df = 1, p-value = 0.01009
# menghapus level yang tidak ada anggotanya
song_clean$time_signature <- droplevels(song_clean$time_signature)
# cek perfect separator
table(song_clean$time_signature, song_clean$class)
##      
##         0   1
##   3/4   4   3
##   5/4  30  16
##   6/4 165 101
##   7/4   6   3

Bukan perfect separator.

# mengecek hubungan variabel time_signature dengan target
chisq.test(table(song_clean$time_signature, song_clean$class))
## 
##  Pearson's Chi-squared test
## 
## data:  table(song_clean$time_signature, song_clean$class)
## X-squared = 0.32237, df = 3, p-value = 0.9558

Dari hasil ketiga uji Chi-Square di atas, variabel prediktor kategorik yang memiliki korelasi dengan variabel target adalah variabel mode.

Dengan demikian, variabel yang digunakan dalam pemodelan adalah:

  • danceability
  • energy
  • loudness
  • valence
  • instrumentalness
  • acousticness
  • mode

Disclaimer: variabel duration_ms diputuskan untuk tidak digunakan dalam pemodelan karena terdiri dari nilai-nilai yang aneh dan tidak konsisten (ada yang desimal dan ada juga yang ratusan ribu)

# menghapus variabel yang tidak digunakan di dataframe baru
song_clean <- song_clean %>% 
  select(c(danceability, energy, loudness, valence, instrumentalness, acousticness, mode, class))
# melihat summary dari dataframe baru
summary(song_clean)
##   danceability        energy           loudness          valence      
##  Min.   :0.0778   Min.   :0.00374   Min.   :0.08781   Min.   :0.0299  
##  1st Qu.:0.3568   1st Qu.:0.33800   1st Qu.:0.24986   1st Qu.:0.1270  
##  Median :0.4670   Median :0.55400   Median :0.37276   Median :0.2815  
##  Mean   :0.4740   Mean   :0.53570   Mean   :0.39824   Mean   :0.3421  
##  3rd Qu.:0.5663   3rd Qu.:0.74375   3rd Qu.:0.50797   3rd Qu.:0.5102  
##  Max.   :0.9270   Max.   :0.96700   Max.   :1.00000   Max.   :0.9700  
##  instrumentalness   acousticness          mode     class  
##  Min.   :0.00000   Min.   :0.0000085   minor:137   0:205  
##  1st Qu.:0.00308   1st Qu.:0.0146000   major:191   1:123  
##  Median :0.23800   Median :0.1090000                      
##  Mean   :0.37491   Mean   :0.2768855                      
##  3rd Qu.:0.77850   3rd Qu.:0.4645000                      
##  Max.   :0.98600   Max.   :0.9940000

Menurut summary di atas, dapat disimpulkan bahwa: nilai dari variabel prediktor numerik kebanyakan berada di rentang 0 dan 1.

Setelah variabel prediktor berhasil dipilih, akan dilakukan pengecekan asumsi regresi logistik - terpenuhi atau tidak, dimana salah satu asumsi dari regresi logistik adalah tidak adanya multikolinieritas. Oleh karena itu, sebelum melakukan pemodelan, sebaiknya cek terlebih dahulu apakah ada korelasi antar variabel prediktor atau tidak.

# mengecek korelasi antar variabel prediktor numerik
ggcorr(song_clean, hjust = 1, layout.exp = 1, label = TRUE)

Menurut gambar heatmap di atas, sepertinya terdapat beberapa variabel prediktor yang berhubungan kuat. Tetapi untuk sementara, hal ini diabaikan karena nanti akan dicek kembali melalui nilai VIF.

Imbalanced class adalah salah satu concern yang patut diperhatikan dalam pemodelan menggunakan machine learning. Oleh karenanya, cek apakah variabel target memiliki kelas dengan proporsi yang tidak seimbang.

# mengecek keseimbangan kelas dari variabel target
prop.table(table(song_clean$class))
## 
##     0     1 
## 0.625 0.375

Berdasarkan proporsi di atas, dapat disimpulkan bahwa kelas masih seimbang, sehingga tidak perlu dilakukan resampling.

Note: kelas umumnya dinyatakan imbalanced jika memiliki proporsi 90/10 atau 95/5.

6. Pembagian Training Set dan Testing Set

# membagi ke training set dan testing set
set.seed(100) 

index <- initial_split(data = song_clean,  
                       prop = 0.8, 
                       strata = class) 

song_train <- training(index)
song_test <- testing(index)

7. Regresi Logistik Biner

7.1 Membentuk Model

Pada metode ini, pemodelan dilakukan sebanyak dua kali, yaitu dengan membentuk:

  • model_all: model dengan seluruh variabel prediktor
  • model_backward: model yang dibentuk dari proses backward-stepwise
# membentuk model dengan semua variabel prediktor
model_all <- glm(class ~ ., data = song_train, family = "binomial")
summary(model_all)
## 
## Call:
## glm(formula = class ~ ., family = "binomial", data = song_train)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.0813  -0.5139  -0.1297   0.4979   2.6594  
## 
## Coefficients:
##                  Estimate Std. Error z value Pr(>|z|)    
## (Intercept)       -0.7436     1.6641  -0.447 0.654990    
## danceability      -1.9824     1.4360  -1.380 0.167439    
## energy             3.0088     1.7111   1.758 0.078680 .  
## loudness          10.6648     2.1069   5.062 4.15e-07 ***
## valence           -8.6922     1.5274  -5.691 1.27e-08 ***
## instrumentalness  -2.3121     0.5811  -3.979 6.93e-05 ***
## acousticness      -3.2749     0.9281  -3.529 0.000418 ***
## modemajor         -1.1016     0.3852  -2.860 0.004240 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 346.40  on 261  degrees of freedom
## Residual deviance: 187.86  on 254  degrees of freedom
## AIC: 203.86
## 
## Number of Fisher Scoring iterations: 6
# melakukan feature selection dengan metode backward
step(model_all, direction = "backward", trace = 0)
## 
## Call:  glm(formula = class ~ energy + loudness + valence + instrumentalness + 
##     acousticness + mode, family = "binomial", data = song_train)
## 
## Coefficients:
##      (Intercept)            energy          loudness           valence  
##           -1.816             3.347            11.108            -9.347  
## instrumentalness      acousticness         modemajor  
##           -2.302            -3.232            -1.142  
## 
## Degrees of Freedom: 261 Total (i.e. Null);  255 Residual
## Null Deviance:       346.4 
## Residual Deviance: 189.8     AIC: 203.8
# membuat model menggunakan prediktor yang dihasilkan oleh metode backward
model_backward <- glm(class ~ danceability + loudness + valence + instrumentalness + 
    acousticness + mode, data = song_train, family = "binomial")
summary(model_backward)
## 
## Call:
## glm(formula = class ~ danceability + loudness + valence + instrumentalness + 
##     acousticness + mode, family = "binomial", data = song_train)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -2.3336  -0.5467  -0.1386   0.4876   2.5055  
## 
## Coefficients:
##                  Estimate Std. Error z value Pr(>|z|)    
## (Intercept)        1.7970     0.8649   2.078 0.037753 *  
## danceability      -2.3737     1.4036  -1.691 0.090816 .  
## loudness           8.2883     1.5372   5.392 6.97e-08 ***
## valence           -7.5699     1.3129  -5.766 8.12e-09 ***
## instrumentalness  -2.1367     0.5674  -3.766 0.000166 ***
## acousticness      -4.0444     0.8379  -4.827 1.39e-06 ***
## modemajor         -1.0191     0.3797  -2.684 0.007269 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 346.40  on 261  degrees of freedom
## Residual deviance: 191.04  on 255  degrees of freedom
## AIC: 205.04
## 
## Number of Fisher Scoring iterations: 6

7.2 Melakukan Prediksi

Setelah membuat model, langkah selanjutnya adalah melakukan prediksi.

Labelling pada project ini dilakukan dengan acuan:

  • Jika probabilitas > 0,5 : class adalah 1
  • Jika probabilitas <= 0,5 : class adalah 0
# memprediksi pada training set menggunakan model_all
song_train$pred_prob_all <- predict(object = model_all, newdata = song_train, type="response")

# melakukan labelling pada hasil prediksi model_all di training set 
song_train$pred_label_all <- ifelse(song_train$pred_prob_all > 0.5, 1, 0) %>% as.factor()
# memprediksi pada testing set menggunakan model_all
song_test$pred_prob_all <- predict(object = model_all, newdata = song_test, type="response")

# melakukan labelling pada hasil prediksi model_all di testing set
song_test$pred_label_all <- ifelse(song_test$pred_prob_all > 0.5, 1, 0) %>% as.factor()
# memprediksi pada training set menggunakan model_backward
song_train$pred_prob_backward <- predict(object = model_backward, newdata = song_train, type="response")

# melakukan labelling pada hasil prediksi model_backward di training set
song_train$pred_label_backward <- ifelse(song_train$pred_prob_backward > 0.5, 1, 0) %>% as.factor()
# memprediksi pada testing set menggunakan model_backward
song_test$pred_prob_backward <- predict(object = model_backward, newdata = song_test, type="response")

# melakukan labelling pada hasil prediksi model_backward di testing set
song_test$pred_label_backward <- ifelse(song_test$pred_prob_backward > 0.5, 1, 0) %>% as.factor()

7.3 Evaluasi Model

Membuat confusion matrix dan menghitung nilai metrik evaluasi untuk mengkalkulasikan performa model.

# membuat confusion matrix untuk mengetahui performa model_all di training set
confusionMatrix(data = song_train$pred_label_all, reference = song_train$class, positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 142  22
##          1  22  76
##                                           
##                Accuracy : 0.8321          
##                  95% CI : (0.7812, 0.8752)
##     No Information Rate : 0.626           
##     P-Value [Acc > NIR] : 2.227e-13       
##                                           
##                   Kappa : 0.6414          
##                                           
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.7755          
##             Specificity : 0.8659          
##          Pos Pred Value : 0.7755          
##          Neg Pred Value : 0.8659          
##              Prevalence : 0.3740          
##          Detection Rate : 0.2901          
##    Detection Prevalence : 0.3740          
##       Balanced Accuracy : 0.8207          
##                                           
##        'Positive' Class : 1               
## 
# membuat confusion matrix untuk mengetahui performa model_all di testing set
confusionMatrix(data = song_test$pred_label_all, reference = song_test$class, positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 31  6
##          1 10 19
##                                           
##                Accuracy : 0.7576          
##                  95% CI : (0.6364, 0.8546)
##     No Information Rate : 0.6212          
##     P-Value [Acc > NIR] : 0.01359         
##                                           
##                   Kappa : 0.5005          
##                                           
##  Mcnemar's Test P-Value : 0.45325         
##                                           
##             Sensitivity : 0.7600          
##             Specificity : 0.7561          
##          Pos Pred Value : 0.6552          
##          Neg Pred Value : 0.8378          
##              Prevalence : 0.3788          
##          Detection Rate : 0.2879          
##    Detection Prevalence : 0.4394          
##       Balanced Accuracy : 0.7580          
##                                           
##        'Positive' Class : 1               
## 
# membuat confusion matrix untuk mengetahui performa model_backward di training set
confusionMatrix(data = song_train$pred_label_backward, reference = song_train$class, positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 144  26
##          1  20  72
##                                           
##                Accuracy : 0.8244          
##                  95% CI : (0.7728, 0.8685)
##     No Information Rate : 0.626           
##     P-Value [Acc > NIR] : 1.868e-12       
##                                           
##                   Kappa : 0.6204          
##                                           
##  Mcnemar's Test P-Value : 0.461           
##                                           
##             Sensitivity : 0.7347          
##             Specificity : 0.8780          
##          Pos Pred Value : 0.7826          
##          Neg Pred Value : 0.8471          
##              Prevalence : 0.3740          
##          Detection Rate : 0.2748          
##    Detection Prevalence : 0.3511          
##       Balanced Accuracy : 0.8064          
##                                           
##        'Positive' Class : 1               
## 
# membuat confusion matrix untuk mengetahui performa model_backward di testing set
confusionMatrix(data = song_test$pred_label_backward, reference = song_test$class, positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 31  7
##          1 10 18
##                                           
##                Accuracy : 0.7424          
##                  95% CI : (0.6199, 0.8422)
##     No Information Rate : 0.6212          
##     P-Value [Acc > NIR] : 0.02624         
##                                           
##                   Kappa : 0.4652          
##                                           
##  Mcnemar's Test P-Value : 0.62763         
##                                           
##             Sensitivity : 0.7200          
##             Specificity : 0.7561          
##          Pos Pred Value : 0.6429          
##          Neg Pred Value : 0.8158          
##              Prevalence : 0.3788          
##          Detection Rate : 0.2727          
##    Detection Prevalence : 0.4242          
##       Balanced Accuracy : 0.7380          
##                                           
##        'Positive' Class : 1               
## 

7.4 Asumsi Model (tidak ada multikolinieritas)

Cek nilai VIF (Variance Inflation Factor) untuk setiap variabel prediktor dengan aturan sebagai berikut:

  • Nilai VIF > 10: ada multikolinieritas
  • Nilai VIF < 10: tidak ada multikolinieritas
# uji multikolinieritas pada model_all
vif(model_all)
##     danceability           energy         loudness          valence 
##         1.121153         5.580297         4.331198         1.770004 
## instrumentalness     acousticness             mode 
##         1.372755         2.786147         1.067824
# uji multikolinieritas pada model_backward
vif(model_backward)
##     danceability         loudness          valence instrumentalness 
##         1.109833         2.404918         1.382202         1.339214 
##     acousticness             mode 
##         2.369120         1.045837

Dari nilai VIF kedua model di atas, dapat disimpulkan bahwa tidak ada multikolinieritas (asumsi terpenuhi).

7.5 Model Regresi Logistik Biner Terbaik

Berdasarkan summary pada model_all, diketahui ada satu variabel prediktor yang tidak signifikan memengaruhi variabel target - variabel prediktor tersebut adalah variabel energy. Diketahui pula bahwa model_all memiliki nilai AIC sebesar 203,18.

Sedangkan pada model_backward, variabel energy dihapus dari model dan menghasilkan nilai AIC yang lebih kecil daripada model_all, yaitu sebesar 202,4.

Namun, jika dilihat dari nilai Residual Deviance, ternyata nilai di model_all lebih kecil daripada di model_backward.

model_all$deviance
## [1] 187.8622
model_backward$deviance
## [1] 191.0424

Ditambah lagi, precision dari model_all diketahui lebih besar daripada di model_backward, terutama di bagian precision untuk testing set.

Note: precision menjadi acuan utama dalam evaluasi model karena: kita tentu tidak menginginkan lagu yang bukan selera kita masuk ke sistem rekomendasi (atau false positive menjadi fokus perhatian)

# pembuatan tabel perbandingan
metrik <- c("accuracy", "precision")
model_all_training <- c(0.8282, 0.7849)
model_all_testing <- c(0.7879, 0.6774)
model_backward_training <- c(0.8206, 0.7684)
model_backward_testing <- c(0.7727, 0.6667)
data.frame(metrik, model_all_training, model_all_testing, model_backward_training, model_backward_testing)

Dengan pertimbangan-pertimbangan di atas, saya pun memutuskan untuk memilih model_all sebagai model regresi logistik biner terbaik. Hal ini dikarenakan nilai Residual Deviance dan precision - nya yang lebih besar daripada model_backward, meskipun nilai AIC-nya sedikit lebih kecil.

8. K-Nearest Neighbor

8.1 Data Cleansing

Agar memudahkan untuk analisis, saya membuat dataframe baru yang khusus untuk analisis k-nearest neighbor, dimana data-data prediksi (yang sebelumnya dibuat saat analisis menggunakan regresi logistik) dihapus terlebih dahulu.

Note: rincian variabel prediktor yang digunakan dalam k-nearest neighbor disamakan dengan regresi logistik biner

# buat dataframe untuk knn
song_train_knn <- song_train %>% 
  select(-c(pred_prob_all, pred_label_all, pred_prob_backward, pred_label_backward))
song_test_knn <- song_test %>% 
  select(-c(pred_prob_all, pred_label_all, pred_prob_backward, pred_label_backward))

Cek struktur data dari dataframe yang baru dibuat.

# cek struktur data
str(song_train_knn)
## 'data.frame':    262 obs. of  8 variables:
##  $ danceability    : num  0.845 0.477 0.845 0.903 0.698 0.872 0.878 0.883 0.502 0.831 ...
##  $ energy          : num  0.33 0.566 0.783 0.463 0.432 0.514 0.535 0.82 0.867 0.794 ...
##  $ loudness        : num  0.504 0.42 0.359 0.315 0.592 ...
##  $ valence         : num  0.883 0.778 0.577 0.733 0.679 0.798 0.958 0.839 0.547 0.735 ...
##  $ instrumentalness: num  7.14e-01 0.00 4.66e-03 1.07e-01 7.62e-03 6.76e-03 8.45e-06 3.70e-04 2.47e-02 3.40e-05 ...
##  $ acousticness    : num  0.322 0.238 0.087 0.0238 0.297 0.54 0.114 0.29 0.17 0.705 ...
##  $ mode            : Factor w/ 2 levels "minor","major": 1 2 1 1 2 1 2 1 2 1 ...
##  $ class           : Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
# cek struktur data
str(song_test_knn)
## 'data.frame':    66 obs. of  8 variables:
##  $ danceability    : num  0.418 0.126 0.315 0.608 0.355 0.49 0.439 0.549 0.578 0.456 ...
##  $ energy          : num  0.443 0.0539 0.294 0.523 0.569 0.454 0.605 0.713 0.6 0.776 ...
##  $ loudness        : num  0.553 0.833 0.454 0.553 0.468 ...
##  $ valence         : num  0.28 0.038 0.0963 0.696 0.292 0.157 0.107 0.206 0.154 0.223 ...
##  $ instrumentalness: num  0.876 0.509 0.045 0.6 0.171 0.417 0.534 0.0145 0.0131 0.00975 ...
##  $ acousticness    : num  0.319 0.799 0.185 0.0775 0.00311 0.185 0.00202 0.0453 0.0129 0.0423 ...
##  $ mode            : Factor w/ 2 levels "minor","major": 1 2 1 1 2 1 1 2 2 1 ...
##  $ class           : Factor w/ 2 levels "0","1": 2 2 2 2 2 2 2 2 2 2 ...

Terlihat dari struktur data di atas, bahwa variabel mode masih berupa faktor. Padahal, dalam k-nearest neighbor, semua variabel prediktor harus bertipe numerik karena akan dihitung jarak Euclidean-nya. Untuk mengatasi hal ini, sebaiknya dilakukan one-hot encoding pada variabel mode seperti berikut.

# one-hot encoding
levels(song_train_knn$mode) <- list("0" = "minor", "1" = "major")
song_train_knn <- song_train_knn %>%
  mutate(mode = as.character(mode)) %>% 
  mutate(mode = as.numeric(mode))
levels(song_test_knn$mode) <- list("0" = "minor", "1" = "major")
song_test_knn <- song_test_knn %>%
  mutate(mode = as.character(mode)) %>% 
  mutate(mode = as.numeric(mode))
# mengintip tipe dan struktur data
glimpse(song_train_knn)
## Rows: 262
## Columns: 8
## $ danceability     <dbl> 0.8450, 0.4770, 0.8450, 0.9030, 0.6980, 0.8720, 0.878~
## $ energy           <dbl> 0.33000, 0.56600, 0.78300, 0.46300, 0.43200, 0.51400,~
## $ loudness         <dbl> 0.5036444, 0.4200116, 0.3593060, 0.3152996, 0.5923416~
## $ valence          <dbl> 0.8830, 0.7780, 0.5770, 0.7330, 0.6790, 0.7980, 0.958~
## $ instrumentalness <dbl> 7.14e-01, 0.00e+00, 4.66e-03, 1.07e-01, 7.62e-03, 6.7~
## $ acousticness     <dbl> 0.322000, 0.238000, 0.087000, 0.023800, 0.297000, 0.5~
## $ mode             <dbl> 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0,~
## $ class            <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,~
# mengintip tipe dan struktur data
glimpse(song_test_knn)
## Rows: 66
## Columns: 8
## $ danceability     <dbl> 0.418, 0.126, 0.315, 0.608, 0.355, 0.490, 0.439, 0.54~
## $ energy           <dbl> 0.4430, 0.0539, 0.2940, 0.5230, 0.5690, 0.4540, 0.605~
## $ loudness         <dbl> 0.5529417, 0.8326651, 0.4541772, 0.5532281, 0.4682514~
## $ valence          <dbl> 0.2800, 0.0380, 0.0963, 0.6960, 0.2920, 0.1570, 0.107~
## $ instrumentalness <dbl> 8.76e-01, 5.09e-01, 4.50e-02, 6.00e-01, 1.71e-01, 4.1~
## $ acousticness     <dbl> 0.319000, 0.799000, 0.185000, 0.077500, 0.003110, 0.1~
## $ mode             <dbl> 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1,~
## $ class            <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,~

Terlihat bahwa semua variabel prediktor sudah bertipe numerik, maka dari itu tahapan analisis bisa dilanjutkan.

8.2 Pemisahan Variabel Prediktor dan Target

# prediktor data train
song_train_x <- song_train_knn %>% select_if(is.numeric)

# target data train
song_train_y <- song_train_knn %>% select(class)

# prediktor data test
song_test_x <- song_test_knn %>% select_if(is.numeric)

# target data test
song_test_y <-  song_test_knn %>% select(class)

8.3 Pemilihan k Optimum

# pemilihan nilai k optimum
(sqrt(nrow(song_train_x))) - 1
## [1] 15.18641

Karena kelas pada variabel target berjumlah genap, maka sebaiknya nilai k-nya adalah ganjil. Berdasarkan hasil penghitungan di atas, mari kita ambil k = 15.

8.4 Melakukan Prediksi

# prediksi menggunakan metode knn
song_kknpred_15 <- knn(train = song_train_x, 
                       test = song_test_x, 
                       cl = song_train_y$class,
                       k = 15)

8.5 Evaluasi Model

# pembuatan confusion matrix dan penghitungan metrik untuk mengetahui performa model pada testing set
confusionMatrix(data = song_kknpred_15, reference = song_test_y$class, positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  0  1
##          0 29  5
##          1 12 20
##                                           
##                Accuracy : 0.7424          
##                  95% CI : (0.6199, 0.8422)
##     No Information Rate : 0.6212          
##     P-Value [Acc > NIR] : 0.02624         
##                                           
##                   Kappa : 0.481           
##                                           
##  Mcnemar's Test P-Value : 0.14561         
##                                           
##             Sensitivity : 0.8000          
##             Specificity : 0.7073          
##          Pos Pred Value : 0.6250          
##          Neg Pred Value : 0.8529          
##              Prevalence : 0.3788          
##          Detection Rate : 0.3030          
##    Detection Prevalence : 0.4848          
##       Balanced Accuracy : 0.7537          
##                                           
##        'Positive' Class : 1               
## 

Berdasarkan hasil evaluasi model di atas, diketahui bahwa nilai precision-nya adalah 0,6250 dan nilai accuracy-nya adalah 0,7424.

9. Model Terbaik

# perbandingan metode regresi logistik biner dan knn
metrik <- c("accuracy", "precision")
regresi_logistik_biner <- c(0.7879, 0.6774)
knn <- c(0.7424, 0.6250)
data.frame(metrik, regresi_logistik_biner, knn)

Berdasarkan metrik di atas, dapat ditarik kesimpulan bahwa metode regresi logistik biner menghasilkan performa yang lebih baik daripada k-nearest neighbor. Meskipun begitu, karena nilai precision-nya yang masih tergolong kecil, maka tidak menutup kemungkinan terdapat metode lain yang dapat meningkatkan kepresisian dan keakuratan hasil prediksi.