Penyampaian opini/pendapat melalui teks sudah menjadi cara yang sangat umum bagi setiap individu untuk dapat disalurkan ke berbagai macam platform media sosial. Penyampaian pendapat contohnya seperti review dari sebuah produk dapat dimanfaatkan di dunia bisnis untuk mengetahui nilai kepuasan customer atau isu yang mungkin muncul terhadap produk yang bersangkutan, tidak hanya mengenai produk pengolahan informasi teks bahkan bisa kita manfaatkan untuk melihat bagaimana pandangan customer terhadap perusahaan, kebutuhan pasar, trend yang sedang terjadi dan berbagai macam pertanyaan bisnis lainnya yang disampaikan langsung oleh customer. Dengan kata lain pengambilan informasi dari opini/review customer merupakan cara yang mudah, efisien, efektif untuk mengetahui isu yang relevan sekaligus solusi yang mungkin dibutuhkan dalam analisis bisnis. Saat ini sudah banyak sekali platform yang telah menyediakan layanan untuk penyampaian pendapat seperti kolom komentar/ review dalam sebuah aplikasi, tentu hal ini dapat dimanfaatkan untuk mendapatkan informasi yang relevan, luas dan cepat.
Untuk mendapatkan informasi yang relevan dari kumpulan teks yang diungkapkan/diunggah setiap individunya dan menentukan rangkuman informasi yang dapat merepresentasikan dari semua kumpulan teks tentu sangat sulit jika hanya mengandalkan kemampuan manusia, tentu saja tidak mustahil untuk melakukannya namun akan membutuhkan waktu yang sangat lama dalam mengekstrak kandungan kumpulan teks, bayangkan kita harus membaca 1.000 review per bulan atau mungkin saja 100.000 review, lalu merangkumnya dan mengidentifikasi informasi mana yang memiliki urgensi terlebih dengan kemajuan teknologi yang pesat pelaku bisnis dituntut untuk dapat terus bersaing dengan tetap mempertahankan pelayanan yang baik.Dengan demikian kita membutuhkan cara yang lebih efisien, tepat dan mudah dalam mengolahnya, terlebih informasi review dari customer adalah informasi yang sangat membantu untuk menentukan pengembangan yang tepat sasaran.
Modern problem needs modern solution, NLP merupakan salah satu cara yang dapat membantu dalam pengolahan teks yang begitu banyak untuk medapatkan informasi dan insight yang belum diperoleh dari bentuk informasi lainnya misalnya rating. Proses NLP dalam machine learning adalah dimana komputer belajar dalam memahami, mengekstrak informasi dan mengelola data yang disampaikan dalam bahasa manusia. Dengan menggabungkan cara berfikir kritis dan pemahaman manusia ditambah dengan teknologi seperti misalnya hal ini machine learning, tentu akan memudahkan kita dalam mendapatkan informasi dari sebuah data teks.
Mencari keyword yang bermakna dan memiliki asosiasi terhadap isu yang ada dapat dilakukan dengan metode NLP. Seperti yang akan saya lakukan untuk sesi kali ini yaitu mencari dan membagi kelompok review menjadi beberapa topik utama dari aplikasi KAI Access dalam periode tertentu untuk mengklasifikasikan kecondongan suatu keyword (kata) yang dapat menggambarkan sebuah review sehingga sesuai dengan kelompok topik utamanya yang belum tertangkap dari nilai rating saja. Adapun data yang akan saya gunakan dapat diakses melalui link berikut: Aplikasi KAI Access menggunakan teknik unsupervised machine learning LDA (Latent Dirichlet Allocation)
Dataset diatas memiliki informasi kolom:
user : Informasi nama user/customer yang mengunggah
reviewreview : Isi teks review yang disampaikan
user/customerscore : Nilai rating yang diberikan user terhadap
aplikasi KAI Accessdate: Tanggal unggah user dari review yang
bersangkutanSeperti yang telah disebutkan diatas, saya akan menggunakan unsupervised machine learning LDA, sehingga khususnya pada kasus ini tidak memiliki output prediksi namun output berupa kategori topik yang polanya dipelajari oleh machine yang ditemukan pada teks review. Sehingga dapat dikatakan bahwa model machine learning yang akan digunakan adalah Topic Modelling LDA.
Pemilihan metode unsupervised machine learning dilakukan atas pertimbangan supervised machine learning dirasa kurang cocok dalam menghadapi kasus review yang sudah memiliki nilai rating (misal memprediksi sebuah teks ke dalam rating tertentu), mengingat nilai rating sendiri akan langsung diinput oleh user dan tidak terlepas dari teks reviewnya. Terlebih lagi data telah memiliki nilai rating yang sudah diinput user (rating aplikasi KAI Access) sehingga melakukan sentimen analisis tidak dilakukan dengan alasan nilai rating sudah cukup mempresentasikan nilai kepuasan customer terhadap performa aplikasi.
Output dari project ini berupa :
Analisis review & rating
Model machine learning LDA untuk membagi review kedalam topik-topik tertentu
Dashboard analysis yang dapat menampilkan informasi:
Plot untuk menampilkan distribusi rating dengan fitur pemilihan periode tahun
Plot untuk menampilkan banyak traffic review masuk dengan fitur pemilihan periode tahun dan bulan
Plot untuk menampilkan frekuensi kata yang sering muncul
Hasil dari machine learning untuk menampilkan teks review dengan fitur filter topic yang diinput serta informasi rating yang diberikan dari review tersebut
Business Impact yang diperoleh melalui teks mining dan analisis ini adalah: Untuk membantu pelaku bisnis (khususnya dalam hal ini PT KAI) dalam mengelola keputusan bisnis dan melakukan product development yang didukung dengan informasi :
Dengan mengetahui informasi dari poin-poin di atas, tentu diharapkan membantu pelaku bisnins (dalam kasus ini PT KAI sebagai target user) dalam melakukan pengelolan keputusan bisnis dan product development yang tepat sasaran dan relevan.
Kasus ini memang menitikberatkan hasil dari model untuk product development yang dapat dimanfaatkan untuk PT KAI dengan mencari topik utama dari review aplikasi, namun begitu tentunya metode teks mining dapat menjangkau ke ranah yang lebih luas untuk pelaku bisnis lainnya misal di bidang retail, kesehatan, e-commerce dll, selama terdapat data teks kita dapat memanfaatkan informasi yang terkandung dalam teks (misal mengambil data teks dengan metode web scraping dari twitter untuk mencari topik yang lebih luas lagi), tentu saja dengan menggunakan treatment dan penggunaan model machine learning yang menyesuaikan sesuai dengan hasil output yang diinginkan, namun begitu impact yang dihasilkan dari teks mining sangat membantu pelaku bisnis untuk mengetahui apa yang dibutuhkan target/pasar.
#install_github("Talioll/katadasaR") Sesi ini menggunakan library katadasaR yang telah disesuaika dari library aslinya (fork code dari nurandi/katadasaR, menajadi Talioll/katadasaR)
library(furrr)
library(tidyverse)
library(lubridate)
library(ggplot2)
library(ggwordcloud)
library(scales)
library(plotly)
library(rsample)
library(devtools)
library(tm)
library(textstem)
library(textclean)
library(tidytext)
library(textmineR)
library(katadasaR)
library(tokenizers)
library(topicmodels)
library(wordcloud)
library(wordcloud2)
library(katadasaR)
library(ldatuning)
options(scipen = 100)Untuk langkah pertama mari kita menyimpan dataset ke dalam sebuah
variable review:
review <- read.csv("DataSet/KAI APP/Review of The Application KAI Access.csv")
reviewSesuai dengan data frame di atas, berikut detail informasi yang diperoleh dari data frame:
user : Informasi nama user/customerreview : Isi teks review yang disampaikan
user/customerscore : Nilai rating yang diberikan user terhadap
aplikasi KAI Accessdate: Tanggal unggah user dari review yang
bersangkutanSebelum melakukan proses analisis dan permodelan tentu terdapat beberapa penyesuaian yang perlu dilakukan terhadap dataframe:
1. Periksa NA Rows
Pada tahap ini saya melakukan pemeriksaan apakan terdapat rows (baris) yang kosong
anyNA(review)#> [1] FALSE
Tidak terdapat rows NA (kosong)
2. Mengubah Tipe Data & Seleksi Kolom
Pada tahap ini saya menyesuaikan tipe data dan seleksi kolom yang akan digunakan untuk mempermudah proses komputasi:
user karena tidak relevan dengan proses
analisisdate dari character ke tipe
date time agar mempermudah dalam proses data subsettingscore menjadi factor agar
merepresentasikan sebuah kelompokreview <- review %>%
select(-user) %>% # Membuang kolom user
mutate(score = as.factor(score), # mengubah tipe data score ke factor
date = as_datetime(date)) #mengubah tipe data score ke date time3. Check Kolom Review
Pada tahap ini saya akan melakukan pemeriksaan apakah terdapah data yang tidak memuat teks (empty string), sebagaimana yang kita ketahui bahwa terdapat kemungkinan user tidak menulis review dan hanya input nilai rating saja.
review[review$review == "",]Terdapat review tanpa isi teks, sehingga untuk keperluan analisis teks kita akan membuang baris tersebut. Namun untuk saat ini kita akan mempertahankan data untuk keperluan analisis frekuensi rating
EDA atau Exploratory Data Analysis adalah tahap awal dalam proses analisis data yang bertujuan untuk mengumpulkan informasi tentang data yang akan dianalisis. Tahap ini merupakan langkah penting karena memberikan gambaran umum tentang data yang akan diolah, sekaligus memberikan ide tentang langkah selanjutnya yang akan dilakukan. Mari kita melihat informasi yang mungkin didapatkan dari data frame asli
1. Frekuensi Review-Rating
#cek range periode waktu upload review pada data
range(review$date)#> [1] "2014-07-16 03:11:28 UTC" "2022-12-22 04:01:05 UTC"
Pada tahap ini kita akan mencari informasi frekuensi review-rating yang masuk per tahunnya, sebagaimana yang telah kita ketahui mengenai total review masuk selama periode 2014-07-16 03:11:28 sampai dengan 2022-12-22 04:01:05 adalah 64,552 (sesuai total rows)
revw.freq <- review %>%
mutate(years = year(date)) %>%
group_by(years) %>%
summarise(freq = n()) %>%
mutate(years = as.factor(years))
persen <- c(0.0,(diff(revw.freq$freq)/revw.freq$freq[1:8])*100) #menghitung persentase kenaikan/penurunan frekuensi review masuk dari tahun sebelumnya
revw.freq <- revw.freq %>%
mutate(persentase = persen,
label = glue(
"Frekuensi: {comma(freq)}
Kenaikan/Penurunan dari Tahun sebelumnya:
{round(persentase,2)} %"))
plot1 <- ggplot(revw.freq, aes(y = freq, x = years)) +
geom_col(aes(fill = freq, text = label), show.legend = F) +
scale_y_continuous(labels = comma) +
scale_fill_gradient(high = "#ffcd42" , low = "#6b7781") +
labs(x = NULL,
y = NULL,
title = "Frekuensi Review Periode 2014 - 2022") +
theme_minimal() +
theme(plot.title = element_text(size = 16, face = "bold", hjust = 0),
axis.title = element_text(hjust = .5),
axis.text = element_text(size = 12, face = "bold"))
ggplotly(plot1, tooltip = "text")Insight: Frekuensi review-rating yang diunggah mencapai nilai tertinggi pada tahun 2019 yaitu sebanyak 22.545 reviews atau naik 400% dari tahun 2018. Tahun 2014-2019 total review yang masuk selalu mengalami peningkatan per tahunnya, sedangkan pada periode tahun 2020 - 2022 trend review mulai menurun, namun begitu frekuensi nilai total rating per tahun 2020 - 2022 masih memiliki frekuensi yang tinggi.
2. Frekuensi Review-Rating Tiap Bulan per Tahun
rvw.bulan <- review %>%
mutate(bulan = month(date, label = T),
tahun = year(date)) %>%
group_by(tahun, bulan) %>%
summarize(freq = n()) %>%
mutate(label = glue(
"Frekuensi: {comma(freq)}"
)) %>%
arrange(tahun, bulan)
plot2 <- ggplot(rvw.bulan[rvw.bulan$tahun == 2022,], aes(y = freq, x = bulan)) +
geom_col(aes(fill = freq, text = label), show.legend = F) +
scale_y_continuous(labels = comma) +
scale_fill_gradient(low = "#c1857e", high = "#3a3a98") +
labs(x = NULL,
y = NULL,
title = "Frekuensi Review Masuk per Bulan Tahun 2022") +
geom_hline(aes(yintercept = mean(freq)), col = "red") +
theme_minimal() +
theme(plot.title = element_text(size = 16, face = "bold", hjust = 0),
axis.title = element_text(hjust = .5),
axis.text = element_text(size = 12, face = "bold"))
ggplotly(plot2, tooltip = "text")Insight Pada tahun 2022 rating-review yang diunggah user berada di atas nilai rata-rata total tahun 2022 terjadi pada pertengahan tahun
3. Distribusi Nilai Rating
range(review$date)#> [1] "2014-07-16 03:11:28 UTC" "2022-12-22 04:01:05 UTC"
round(prop.table(table(review$score))*100, 2) %>% sort(decreasing = T)#>
#> 5 1 2 3 4
#> 39.88 35.51 8.86 7.97 7.78
Selama periode tahun 2014 - 2022 distribusi rating menunjukan nilai terting rating 5 dan diikuti oleh rating 1, hal ini menunjukan masih banyak pengguna aplikasi yang tidak puas dengan aplikasi, untuk analisis lebih lanjut mari kelompokan sesuai periode tahunnya
rating.fq <- review %>%
mutate(years = year(date),
score = as.factor(score)) %>%
group_by(years, score) %>%
summarize(freq = n()) %>%
mutate(label = glue(
"Tahun: {years}
Frekuensi: {comma(freq)}
Rating: {score}"
))
plot3 <- plot3 <- ggplot(rating.fq, aes(y = freq, x = years)) +
geom_line(aes(group = score, col = score), show.legend = F) +
geom_point(aes(col = score, text = label), show.legend = F) +
labs(y = NULL,
x = "Tahun",
title = "Grafik Rating per Tahun") +
theme_minimal() +
theme(axis.text.x = element_text(face = "bold"))
ggplotly(plot3, tooltip = "text")Insight
Untuk mempermudah melihat distribusi rating khususnya tahun 2022, kita dapat memlihat plot di bawah ini:
plot4 <- ggplot(rating.fq[rating.fq$years == 2022, ], aes(x = score, y = freq)) +
geom_col(fill = c("#9a2a21", "#9a2a21", "#ffcd42", "#2b2b71", "#2b2b71"), show.legend = F, aes(text = label)) +
labs(x = "Nilai Rating",
y = "Frekuensi",
title = "Distribusi Rating Tahun 2022") +
scale_y_continuous(labels = comma) +
theme_minimal() +
theme(plot.title = element_text(size = 18, face = "bold", hjust = .5),
axis.text.x = element_text(size = 12, face = "bold"))
ggplotly(plot4, tooltip = "text")3. Tahun 2022
Untuk keperluan model LDA, saya hanya akan menggunakan data tahun
2022 agar isu yang muncul pada hasil analisis dengan isu terkini dan
menghasilkan insight untuk produk development yang tetap sasaran.
Sehingga saya akan melakukan subsetting kepada dataframe awal
review dan menyimpanya ke dalam variable baru dengan nama
review2022
review2022 <- review %>%
filter(year(date) == 2022)
# Menghapus rows yang tidak memiliki isi teks
review2022 <- review2022[review2022$review != "", ]
review20224. Resample Data
Untuk kebutuhan memperkuat insight, ada baiknya kita melihat proporsi
dari kolom :score
table(review2022$score)#>
#> 1 2 3 4 5
#> 4922 1039 755 379 1709
round(prop.table(table(review2022$score))*100,2)#>
#> 1 2 3 4 5
#> 55.91 11.80 8.58 4.30 19.41
Sesuai dengan informasi proporsi kelas pada data tahun 2022, terlihat
porporsi yang timpang diantara ke lima kelas score.
Terlihat jelas bahwa 56% dari customer yang memberikan review-rating
tidak puas terhadap performa aplikasi, PT KAI perlu untuk segera
mengetahui dan melakukan perbaikan kepada poin-poin yang memengaruhi
nilai rating agar tidak kehilangan pengguna aplikasi. Maka dari itu kita
akan mencari isu/poin apa yang menjadi kendala atau yang terjadi di
tahun 2022 agar harapannya dapat dilakukan perbaikan di tahun berikutnya
yang sesuai dengan permasalahan terkini. Untuk menjawab permasalahan
ini, kita akan memulai untuk membuat topic modeling dengan preprocessing
text cleansing.
Text cleansing perlu dilakukan agar elemen pada teks hanya berisi
inti dari isi teks sehingga elemen-elemen yang tidak memiliki relevansi
maupun makna yang dapat memperkuat pemahaman kita terhadap suatu
pernyataan dapat dieliminasi sehingga proses permodelan menjadi efektif
dan efisien. Untuk menentukan elemen apa yang terkandung pada data teks
yang kita miliki, kita dapat menggunakan fungsi
check_text().
Pada tahap ini kita akan memeriksa elemen yang terkandung dalam data
teks untuk kemudian dieliminasi dari data teks kita, saya akan
menggunakan fungsi check_text() dari library
textclean
check_text(review2022$review)#>
#>
#> =====
#> DIGIT
#> =====
#>
#> The following observations contain digits/numbers:
#>
#> 4, 5, 6, 7, 18, 21, 22, 25, 26, 32...[truncated]...
#>
#> This issue affected the following text:
#>
#> 4: pesen tiket kereta bisa pas bayar tiket juga bisa pas mau masuk alesannya harus vaksin 3 berdasarkan nik knapa gak ditolak dari awal nik kan ada di aplikasinya aplikasi norak yg penting duit ngalir bos
#> ...[truncated]...
#> 5: bisa2nya milih tengah dapetnya depdepan di baturaden express
#> ...[truncated]...
#> 6: pesanan tiket yang dibatalkan karena cuma vaksin 1 dan uang ga bisa di kembalikan atau ga bisa diganti hari
#> ...[truncated]...
#> 7: apk sering diskoneksi pencarian kereta untuk 7 hari kedepa selalu kosong dari rute saya lebih baik saya menggunakan aplikasi tiket swasta
#> ...[truncated]...
#> 18: nge bug pas mau payment loading qr nggak muncul2
#> ...[truncated]...
#> 21: saat isi saldo kai pay lewat alfamart gagal 2kenapa eror kak padahal udah di update tetep aja eror tolong di benahi dong aplikasi nya mau beli tiket lokal gagal mulu
#> ...[truncated]...
#> 22: saya kecewa sekali dengan kai access dikasih waktu hanya 7 menit tapi apk selalu tutup paksa padahal saya sudah mengosongkan memory tapi apk tetap sama mohon ditingkatkan terimakasih
#> ...[truncated]...
#> 25: tolong untuk realtime sesuai penggunaan bersama semua orang ketika akses aplikasi ini sering hang sya sadar aplikasi ini mendukung hardware yg lebih mumpuni seperti ram minimal 4gb ketika hp dng spek ram 3gb pun akses nya lambat dan sering stuck atau hang pdahal minim aplikasi yg lain berjalan di latar blkg jadi tolong diperhatikan untuk update2 berikutnya karena tidak semua orang di indonesia melek teknologi dan sanggup memiliki gadget diatas rata2 orang dengan spek yg minim ram 4gb
#> ...[truncated]...
#> 26: masuk susah dah bisa masuk mau bayar susah dikejar2 tulisan pt kai tdk menanggapi dan ini bertaun 2 heran bumn ini kayak nya tuli masukan publik service tp tdk sesuai dgn berita pt kai yg berubah katanya penerimaan sdm sudah bagus tp kok tetap kek gini apa nunggu pak jonan lg
#> ...[truncated]...
#> 32: cara cancel ka lokal gimana ya lewat aplikasi lumayan ka lokal ada 3 perjalanan yg gajadi halloo wa slow respon email slowrespon ayo kai pelayanan 24 jam dong dan yg fast
#> ...[truncated]...
#>
#> *Suggestion: Consider using `replace_number`
#>
#>
#> ========
#> EMOTICON
#> ========
#>
#> The following observations contain emoticons:
#>
#> 5, 1296, 1790, 1806, 2009, 2477, 2753, 2966, 3106, 3607...[truncated]...
#>
#> This issue affected the following text:
#>
#> 5: bisa2nya milih tengah dapetnya depdepan di baturaden express
#> ...[truncated]...
#> 1296: overall experience bagushanya yang cukup menyakitkan koneksi server buruk
#> ...[truncated]...
#> 1790: kalau misal tidak diperkenankan naik kereta karena baru lewat 18thn dan belum booster sebaiknya aplikasi ini mendeteksi nik yg belum boster sehingga tidak bisa membeli tiket bukannya membiarkan penumpang membeli tiket agar kemudian bisa diarahkan untuk membatalkan dgn refund 75 permainan yang sangat luar biasa untuk mendapat untung
#> ...[truncated]...
#> 1806: i am not going to lie the indonesian train service is no like any other train service in the world one of the best and unique experience i have ever had like when you arrive in a stasioni forgot the name somewhere near jogjakarta they would play this relaxing song when your train arrived and it just makes such a relaxing vibe overall the best experience i have ever had d i would love to visit again next time
#> ...[truncated]...
#> 2009: apa saya aja yang pakai aplikasi ini harus extra sabar loading lamanya minta ampun sampai waktu akhir pemesanan habis tetapi pesanan belum selesai aplikasi macam apa ini disaat orang banyak gunakan tidak didukung dengan kelancaran
#> ...[truncated]...
#> 2477: aplikasi berjalan dengan sangat sangat lambat hampir tidak berjalan device xiaomi poco x3 nfc
#> ...[truncated]...
#> 2753: ram hape saya 8gb prosesor snapdragon 865 yang buat main gensinimpact lancar tapi kalo buat buka kai accsess lemot banget apalagi bagian checkout mau pake qris itu dah ga ketolong lemotnya
#> ...[truncated]...
#> 2966: sudah daftar dari lama terus pas masuk login expid katanya pas login nomer email sama pw gak ada katanya pas didaftarin ulang pake nomer sama email yang sama malah tulisannya udah terdaftar gimana sih kai payah sekarang
#> ...[truncated]...
#> 3106: maaf masih bintang 1 tiap kali buka mau cek atw reservasi lebih banyak loadingnya lola banget sepertinya server yg bermasalah bukan soal jaringan atw gadgetnya coz jaringan full gadget normal ram 8gb sisa 6gb masa masih lola jg harap di perbaiki bbrapa kali perlmbaharuan masih lola
#> ...[truncated]...
#> 3607: mohon ditambahkan vitur reduksi tni polri pada booking website kai access masih error crash menambah penumpang or pilih kursi di realme xt sudah sejak april 2022 padahal seminggu sekali akhir pekan pulang keluar kota mksh mohon ditanggapi karena teman2 banyak mengeluhkan
#> ...[truncated]...
#>
#> *Suggestion: Consider using `replace_emoticons`
#>
#>
#> ==========
#> MISSPELLED
#> ==========
#>
#> The following observations contain potentially misspelled words:
#>
#> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10...[truncated]...
#>
#> This issue affected the following text:
#>
#> 1: <<u<<n<<<<tu>>k>>>>>> <<me<<mesan>>>> <<ti<<<<ke>>t>>>> <<<<da>>n>> <<mil<<ih>>>> <<<<ku>>r<<si>>>> <<<<lemo>>t>> <<gk>> <<k<<<<ay>>a>>>> <<du<<lu>>>>
#> ...[truncated]...
#> 2: <<<<<<gi>>man>>a>> <<s<<ih>>>> <<i<<ni>>>> pt <<k<<ai>>>> <<<<o<<ra>>>><<ng>>>> <<<<s<<<<ud>>a>>>>h>> t<<ra>>nsfer <<b<<uat>>>> <<b<<<<ay>>a>>r>> <<ti<<<<ke>>t>>>> <<m<<<<<<al>>a>>h>>>> <<<<<<ti<<<<ke>>t>>>><<ny>>>>a>> <<<<ga>>k>> <<a<<da>>>> <<<<da>>n>> <<ua<<ng>>>> <<s<<<<ay>>a>>>> <<<<ti<<da>>>>k>> <<<<di>><<ke>>m<<b<<lik>>an>>>> <<<<<<ke>>r>><<ja>>>> <<ba<<<<ny>>a>>k>> <<<<ma<<ka>>>>n>> <<g<<aj>>i>> <<buta>> <<a<<ja>>>> <<<<<<samp>>e>>k>> <<<<o<<ra>>>><<ng>>>> <<b<<<<ay>>a>>r>> <<ti<<<<ke>>t>>>> <<m<<<<<<al>>a>>h>>>> <<<<<<ti<<<<ke>>t>>>><<ny>>>>a>> <<gk>> <<<<<<di>><<<<<<ka>>s>>i>>>>h>> <<<<da>>n>> <<ua<<ng>>>> <<gk>> <<<<di>><<<<<<ke>><<<<mb>>a>>li>><<<<ka>>n>>>>>> <<<<se>><<<<<<harus>><<ny>>>>a>>>> <<k<<al>>o>> <<<<ti<<da>>>>k>> <<<<d<<<<ap>>a>>>>t>> <<ti<<<<ke>>t>>>> <<ua<<ng>>>> <<<<ke>><<<<mb>>a>>li>> <<<<ti<<da>>>>k>> <<me<<<<ru<<gi>>>><<<<ka>>n>>>>>> <<<<o<<ra>>>><<ng>>>> <<l<<ai>>>>n
#> ...[truncated]...
#> 3: <<<<<<<<<<ap>>l>>i>><<<<ka>>s>>>>i>> <<<<e<<n<<<<gg>>a>>>>>>k>> <<jelas>> <<ti<<<<ke>>t>>>> <<<<e<<n<<<<gg>>a>>>>>>k>> <<a<<da>>>> ya<<ng>> <<mu<<ra>>h>> <<ma<<h<<al>>>>>> <<<<se>><<mua>>>> <<be<<lu>>m>> <<la<<gi>>>> <<<<loa<<di>><<ng>><<ny>>>>a>> l<<ama>> <<<<ja>>dw<<al>>>> <<<<<<<<ke>>r>>et>>a>> <<s<<e<<ri>><<ng>>>>>> <<<<ti<<da>>>>k>> <<a<<da>>>> <<<<k<<<<al>>a>>>>u>> <<<<e<<n<<<<gg>>a>>>>>>k>> <<<<<<si>>a>>p>> online <<<<lebi>>h>> <<b<<ai>>k>> <<<<e<<n<<<<gg>>a>>>>>>k>> <<<<usa>>h>> <<<<pak>><<ai>>>>
#> ...[truncated]...
#> 4: <<<<pes>>en>> <<ti<<<<ke>>t>>>> <<<<<<<<ke>>r>>et>>a>> <<bisa>> pas <<b<<<<ay>>a>>r>> <<ti<<<<ke>>t>>>> <<ju<<ga>>>> <<bisa>> pas <<mau>> <<<<m<<asu>>>>k>> <<<<<<al>>esan>><<<<ny>>a>>>> <<harus>> <<<<va>>k<<si>>n>> 3 <<<<ber>><<<<da>>sar>><<<<ka>>n>>>> <<<<ni>>k>> <<k<<n<<<<ap>>a>>>>>> <<<<ga>>k>> <<<<di>><<<<tol>>ak>>>> <<<<da>><<ri>>>> <<aw<<al>>>> <<<<ni>>k>> <<<<ka>>n>> <<a<<da>>>> <<di>> <<<<<<<<<<<<<<<<ap>>l>>i>><<<<ka>>s>>>>i>>n>>y>>a>> <<<<<<<<<<ap>>l>>i>><<<<ka>>s>>>>i>> <<<<n<<o<<ra>>>>>>k>> <<yg>> <<penti<<ng>>>> <<d<<ui>>t>> <<<<<<ng>>a>>lir>> <<b<<os>>>>
#> ...[truncated]...
#> 5: <<bisa>>2<<<<ny>>a>> <<mil<<ih>>>> <<<<te>><<<<ng>>a>>h>> <<<<d<<ap>>et>><<<<ny>>a>>>> <<<<de>>p<<<<de>>pan>>>> <<di>> <<<<ba<<tu>>>><<ra>><<de>>n>> <<expres>>s
#> ...[truncated]...
#> 6: <<<<<<<<pes>>an>>a>>n>> <<ti<<<<ke>>t>>>> ya<<ng>> <<<<di>><<<<bat<<al>>>><<<<ka>>n>>>>>> <<<<<<ka>>ren>>a>> <<cuma>> <<<<va>>k<<si>>n>> 1 <<<<da>>n>> <<ua<<ng>>>> <<ga>> <<bisa>> <<di>> <<<<<<ke>><<<<mb>>a>>li>><<<<ka>>n>>>> <<atau>> <<ga>> <<bisa>> <<<<di>><<<<<<ga>>n>>ti>>>> <<ha<<ri>>>>
#> ...[truncated]...
#> 7: <<<<ap>>k>> <<s<<e<<ri>><<ng>>>>>> <<<<di>>s<<<<<<ko>>n<<ek>>>><<si>>>>>> <<pen<<ca<<ri>>>>an>> <<<<<<<<ke>>r>>et>>a>> <<u<<n<<<<tu>>k>>>>>> 7 <<ha<<ri>>>> <<<<ke>><<de>>pa>> <<<<<<se>>la>><<lu>>>> <<k<<os>>o<<ng>>>> <<<<da>><<ri>>>> <<ru<<te>>>> <<s<<<<ay>>a>>>> <<<<lebi>>h>> <<b<<ai>>k>> <<s<<<<ay>>a>>>> <<<<me<<ng>>>><<<<<<g<<un>>a>><<ka>>>>n>>>> <<<<<<<<<<ap>>l>>i>><<<<ka>>s>>>>i>> <<ti<<<<ke>>t>>>> <<s<<wa>><<sta>>>>
#> ...[truncated]...
#> 8: <<lo<<gi>>>>n <<<<<<de>><<ng>>>>an>> <<be<<na>>r>> <<di>> <<a<<n<<<<gg>>a>>>>p>> <<s<<<<<<al>>a>>h>>>> <<be<<g<<i<<tu>>>>>>>> <<<<da>>ftar>> <<la<<gi>>>> <<nomer>> <<em<<ai>>>>l <<di>> <<a<<n<<<<gg>>a>>>>p>> <<<<s<<<<ud>>a>>>>h>> <<<<<<te>>r>><<<<da>>ftar>>>> <<i<<ni>>>> <<p<<a<<ra>>h>>>> <<<<pe<<<<<<ng>>e>><<<<lol>>a>>>>>><<<<ny>>a>>>>
#> ...[truncated]...
#> 9: <<gk>> <<bisa>> <<beli>> <<pulsa>> <<<<<<se>>la>><<lu>>>> <<g<<a<<ga>>>>l>> <<<<tol>>o<<ng>>>> <<<<di>><<<<per<<b<<ai>>k>>>>i>>>>
#> ...[truncated]...
#> 10: <<<<ku>><<al>><<ita>>s>> <<<<<<<<ke>>r>>et>>a>> <<<<<<ek>>o>>nomi>> <<an<<da>>>> p<<al>>i<<ng>> <<<<buru>>k>> <<<<lebi>>h>> <<b<<ai>>k>> <<<<n<<ai>>>>k>> bus <<a<<ntar>>>> <<k<<ota>>>>
#> ...[truncated]...
#>
#> *Suggestion: Consider running `hunspell::hunspell_find` & `hunspell::hunspell_suggest`
#>
#>
#> ==========
#> NO ENDMARK
#> ==========
#>
#> The following observations contain elements with missing ending punctuation:
#>
#> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10...[truncated]...
#>
#> This issue affected the following text:
#>
#> 1: untuk memesan tiket dan milih kursi lemot gk kaya dulu
#> ...[truncated]...
#> 2: gimana sih ini pt kai orang sudah transfer buat bayar tiket malah tiketnya gak ada dan uang saya tidak dikemblikan kerja banyak makan gaji buta aja sampek orang bayar tiket malah tiketnya gk dikasih dan uang gk dikembalikan seharusnya kalo tidak dapat tiket uang kembali tidak merugikan orang lain
#> ...[truncated]...
#> 3: aplikasi enggak jelas tiket enggak ada yang murah mahal semua belum lagi loadingnya lama jadwal kereta sering tidak ada kalau enggak siap online lebih baik enggak usah pakai
#> ...[truncated]...
#> 4: pesen tiket kereta bisa pas bayar tiket juga bisa pas mau masuk alesannya harus vaksin 3 berdasarkan nik knapa gak ditolak dari awal nik kan ada di aplikasinya aplikasi norak yg penting duit ngalir bos
#> ...[truncated]...
#> 5: bisa2nya milih tengah dapetnya depdepan di baturaden express
#> ...[truncated]...
#> 6: pesanan tiket yang dibatalkan karena cuma vaksin 1 dan uang ga bisa di kembalikan atau ga bisa diganti hari
#> ...[truncated]...
#> 7: apk sering diskoneksi pencarian kereta untuk 7 hari kedepa selalu kosong dari rute saya lebih baik saya menggunakan aplikasi tiket swasta
#> ...[truncated]...
#> 8: login dengan benar di anggap salah begitu daftar lagi nomer email di anggap sudah terdaftar ini parah pengelolanya
#> ...[truncated]...
#> 9: gk bisa beli pulsa selalu gagal tolong diperbaiki
#> ...[truncated]...
#> 10: kualitas kereta ekonomi anda paling buruk lebih baik naik bus antar kota
#> ...[truncated]...
#>
#> *Suggestion: Consider cleaning the raw text or running `add_missing_endmark`
#>
#>
#> ===
#> URL
#> ===
#>
#> The following observations contain URLs:
#>
#> 1194
#>
#> This issue affected the following text:
#>
#> 1194: sebelumnya maaftp bukannya seharusnya semua data kependudukan itu sdh link dg peduli lindungi yanamun knpketika dosis vaksin tdk sesuai dg ketentuantiket tetap bs dipesan dan jk mmg ketentuan hrs vaksin sesuai ketentuankenapa dr pihak kai tdk bs menyediakanmembantu konsumen yg sdh memesan tiket namun dosis vaksinnya msh krganak kecil dg dosis 1 dlm hal ini dan pembatalan tdk bs dilakukanpdhl smp distasiun msh krg 45mntmesin cetak tiket troubleantri di cs ditolak krn wkt 30mnt
#>
#> *Suggestion: Consider using `replace_url`
Sesuai dengan informasi diatas poin yang dapat kita fokuskan adalah terindikasinya elemen digit (nomor), emoticon dan url (untuk misspelled and no endmark dapat kita abaikan karena misspelled terjadi karena kita menggunakan bahasa Indonesia, sedangkan no endmark mengidentidikasi teks tanpa adanya tanda baca diakhir teks, keberadaan tanda baca tidak membantu dalam analisis kita sehingga tetap kita abaikan)
Kita akan membersihkan data teks kita dari element yang tidak
dibutuhkan untuk model prediksi seperti yang telah disebutkan pada hasil
fungsi check_text() dan menyimpannya ke dalam variabel baru
review.clean, sehingga kita tetap memiliki data teks
asli.
review.clean <- review2022$review %>%
replace_emoticon() %>% #menghapus emoticon
replace_html(symbol = F) %>% #menghapus html
replace_url(replacement = "") %>% #menghapus url
removePunctuation() %>% #menghapus tanda baca
replace_number(remove = TRUE) #menghapus nomor
review.clean <- tolower(review.clean)
review.clean %>% tail(3)#> [1] "sangat membntu"
#> [2] "minta tolong buat pihak kai agar di beri fitur buat ganti nomor hp"
#> [3] "ajibb"
Pada tahap ini saya akan mengubah kata-kata yang teridentifikasi sebagai slang/typo/singkatan dalam penulisan yang terdapat ke dalam teks ke bentuk yang lebih seragam dan konsisten, sebagai contoh:
| Slang | Formal |
|---|---|
| Gw | Saya |
| Gue | Saya |
| Dpt | Dapat |
| yg | Yang |
| Bokis | Bohong |
Tahap ini membutuhkan dictionary (kumpulan kata) yang akan kita ubah bentuknya, tentu saja setiap kasusnya memiliki keunikan teks sehingga kamus dibawah ini telah dimodifikasi untuk menyesuaikan teks review yang akan kita olah.
Note: tambahan fungsi
plan(multisession, workers = 4) adalah untuk memaksimalkan
kecepatan proses komputasi
#plan(multisession, workers = 4)
#slang.dict <- read.csv("DataSet/new_kamusalay.csv")
#menghapus kemungkinan duplikasi pada kamus slang
#slang.dict <- slang.dict[!duplicated(slang.dict$slang),]
#clean.dict1 <- replace_internet_slang(review.clean,
# slang = paste0('\\b', slang.dict$slang, '\\b') ,
# replacement = slang.dict$formal,
# ignore.case = T)
#clean.dict1 %>% tail(3)Tahap membersihkan kata slang merupakan tahap yang membutuhkan waktu komputasi yang memakan waktu karena memiliki kamus kosa kata yang besar yang diaplikasikan ke setiap kata pada setiap 1 review, terlebih setiap kasusnya akan selalu ditemukan cara penulisan yang unik/berbeda, bahasa slang yang semakin berkembang, atau istilah yang berbeda dalam setiap penarikan data teks di platform pada isu yang berbeda. Untuk menghadapi hal ini preferensi saya adalah dengan melakukan 2 kali tahap pembersihan slang dengan cara:
Hal ini saya lakukan atas pertimbangan efisiensi pengerjaan
pembersihan data (seringkali terjadi ditemukan kata yang belum
terdeteksi pada dictionary yang dipakai padahal kata tersebut memiliki
makna yang kuat terhadap isu yang sedang dibahas). Penyesuaian kata
kepada kasus yang sedang dibahas sebagai contoh, biasanya kata
ka biasanya diartikan sebagai kakak namun pada
konteks kali ini banyak ditemukan kata ka yang arti
sebenenarnya adalah kereta api, inilah salah satu kasus
yang menjadi pertimbangan saya untuk melakukan 2 kali proses pembersihan
kata slang. Namun begitu, perubahan kata slang menjadi 2 sesi tentu
tidak dapat sepenuhnya menjamin data teks menjadi sangat bersih, tetapi
dapat menekan tingkat kekeliruan makna yang sangat mungkin terjadi.
#plan(multisession, workers = 4)
#slang.kai <- read.csv("DataSet/kai-slang2.csv")
#slang.kai <- slang.kai[!duplicated(slang.kai$slang),]
#clean.slang2 <- replace_internet_slang(clean.dict1,
# slang = paste0('\\b', slang.kai$slang, '\\b') ,
# replacement = slang.kai$formal,
# ignore.case = T)
#slang.kai3 <- read.csv("DataSet/clean3.csv")
#names(slang.kai3)[1] <- "slang"
#clean.slang3 <- replace_internet_slang(clean.slang2,
#slang = paste0('\\b', slang.kai3$slang, '\\b') ,
#replacement = slang.kai3$formal,
#ignore.case = T)Pada tahap pembersihan kata slang, saya mempertahankan kata lemot (tidak mengubah kata lemot ke lambat) agar tidak kehilangan makna aslinya dan tidak bercampur dengan permasalahan yang lain yang menggunakan kata kunci (keword) lambat
#CHECK POINT SAVING DATA
#checkpoint <- as.data.frame(clean.slang3)
#names(checkpoint) <- "review"
#write.csv(checkpoint, "checkpoint.csv", row.names = F)
clean.slang3 <- read.csv("checkpoint.csv")
clean.slang3 <- clean.slang3$review
clean.slang3 %>% head(3)#> [1] "untuk pesan tiket dan memilih kursi lemot tidak kaya dulu"
#> [2] "bagaimana sih ini pt kai orang sudah transfer buat bayar tiket malah tiket nya tidak ada dan uang saya tidak kembali kerja banyak makan gaji buta saja sampai orang bayar tiket malah tiket nya tidak dikasih dan uang tidak kembali seharusnya kalau tidak dapat tiket uang kembali tidak merugikan orang lain"
#> [3] "aplikasi tidak jelas tiket tidak ada yang murah mahal semua belum lagi load nya lama jadwal kereta sering tidak ada kalau tidak siap online lebih baik tidak usah pakai"
Karena tahap ini memerlukan proses komputasi yang memakan waktu, saya telah menyiapkan data yang telah diproses sehingga dan menyimpannya dengan variable yang sama sesuai dengan nama hasil dari tahap perubahan kata slang
Pada tahap ini kita akan mengubah kata yang memiliki imbuhan ke kata dasarnya. Hal ini dilakukan sekali lagi agar kata-kata yang memiliki kata dasar yang sama memiliki bentuk yang konsisten sehingga tetap dianggap sebagai 1 bentuk (sehingga kata unggah dang kata mengunggah tidak dianggap sebagi 2 kata berbeda)
kata.dasar <- function(x){
paste(sapply(words(x), katadasaR), collapse = " ")}
clean.katadasar <- lapply(tokenize_words(clean.slang3[]), kata.dasar)
clean.katadasar <- unlist(clean.katadasar)
clean.katadasar <- gsub("nomor induk kependudukan", "nik", clean.katadasar)
clean.katadasar <- gsub("badan usaha milik negara", "bumn", clean.katadasar)
clean.katadasar[2] #> [1] "bagaimana sih ini pt kai orang sudah transfer buat bayar tiket malah tiket nya tidak ada dan uang saya tidak kembali kerja banyak makan gaji buta saja sampai orang bayar tiket malah tiket nya tidak kasih dan uang tidak kembali harus kalau tidak dapat tiket uang kembali tidak rugi orang lain"
Pada tahap ini kita akan menghapus kata-kata yang tidak memiliki makna yang dapat mendeskripsikan sebuah pernyataan maupun yang tidak berkontribusi terhadap analisis kita, sebagai contoh: yang, dengan, untuk, kepada dan lainnya. Pada tahap ini juga saya akan dengan sekaligus merapihkan bentuk teks dengan menghapus spasi/ white space berlebih
stopw.ind <- readLines("DataSet/stopwords-id.txt")
clean.stopwords <- removeWords(clean.katadasar,
words = stopw.ind)
clean.stopwords <- clean.stopwords %>% replace_number(remove = TRUE)
clean.stopwords <- gsub("mesan", "pesan", clean.stopwords)
# Menghapus spasi berlebih
clean.stopwords <- strip(clean.stopwords)
clean.stopwords[1:3]#> [1] "pesan tiket pilih kursi lemot"
#> [2] "transfer bayar tiket tiket uang kerja makan gaji buta bayar tiket tiket uang tiket uang rugi"
#> [3] "tiket murah mahal load jadwal online"
Kita telah memiliki data teks yang telah dibersihkan, sekarang mari kita lihat informasi panjang kata dari data teks kita:
sapply(strsplit(clean.stopwords, " "), length) %>% summary()#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 0.000 2.000 5.000 6.451 9.000 69.000
Hal ini menunjukan bahwa data teks kita memiliki data dengan nilai minimal 0 kata (empty string) dan panjang maksimal sebanyak 69 kata. Kita akan menghapus row pada data yang tidak memiliki isi teks review agar memaksimalkan hasil dari model kita
Data teks kita kini dapat dikatakan bersih, saya akan menyimpannya
kedalam kolom baru bernama cleanReview ke data frame awal
yaitu review2022
review2022.clean <- review2022 %>%
mutate(cleanReview = clean.stopwords) %>%
select(score, cleanReview, review, date)
review2022 %>% head(3)cek data dengan rows tanpa teks pada kolom
cleanReview
review2022.clean[review2022.clean$cleanReview == "",]Jika diperthatikan, teks cleanReview yang kosong berasal review asli (sebelum dilakukan process cleansing) yang telah mendapat treatment eliminasi stopwords. Baris diatas akan kita hapus karena tidak memiliki poin-poin yang membantu kita dalam proses topik modeling (teks reviews tidak banyak mengungkapkan inti poin terhadap experience user dalam menggunakan aplikasi)
#menghapus rows yang tidak memiliki teks pada kolom cleanReview
review2022.clean <- review2022.clean[review2022.clean$cleanReview != "",]Saya kembali menyimpan data frame yang bersih ke lokal untuk menghindari pengulangan proses teks cleansing yang memakan waktu
#write.csv(review2022.clean, "review2022.clean.csv", row.names = F)Untuk mengetahui kata-kata yang sering muncul pada review yang masuk di tahun 2022, kita dapat memprosesnya dengan mengubah teks ke dalam bentuk DTM (Document Terms Matrix, matrix yang menyimpan terms/kata pada dokumen kita) lalu menghitung frekuensi kemunculan tiap kata pada keseluruhan data teks. Adapaun saya juga akan mengeliminasi terms dengan frekuensi kemunculan yang kecil misalnya pada kasus ini saya menentukan banyak kemunculan kecil sebesar 5 kali saja, hal ini dilakukan untuk memaksimalkan hasil model LDA karena kemunculan kata yang kecil tidak memiliki kontribusi yang kuat terhadap model kita
# Membuat teks ke bentuk corpus lalu dibuat ke bentuk matrix DTM
text.corpus <- VCorpus(VectorSource(review2022.clean$cleanReview))
text.dtm <- DocumentTermMatrix(x = text.corpus)
# Eliminasi terms dengan frekuensi kemunculan yang rendah pada keseluruhan dokumen
eliminate.terms <- findFreqTerms(text.dtm, lowfreq = 5, highfreq = nrow(text.dtm)*0.9)
text.dtm <- text.dtm[, eliminate.terms]
# Sorting frekuensi kemunculan secara descending
text.words <- as.matrix(text.dtm)
words.list <- sort(colSums(text.words), decreasing = T)
# Membuat data ke bentuk dataframe untuk kebutuhan visualisasi
words.df <- data.frame(word = names(words.list), freq=words.list)
words.df <- words.df %>%
mutate(label = glue(
"Frekuensi: {comma(freq)}"))Data frame diatas menunjukan urutan (frekuensi paling tinggi ke paling rendah) kemunculan kata pada data teks review.
Insight Dapat kita perhatikan, kata yang memiliki sifat negatif dari 10 kata paling sering muncul adalah lemot, error, dan susah. Sesuai dengan nilai rating 1 yang tinggi pada tahun 2022 dan kemunculan kata ticket, bayar, pesan memiliki yang juga tinggi kita dapat mengasumsikan permasalahan yang sedang terjadi (atau isu yang sering dibahas oleh customer) adalah mengenai pemesanan tiket, pembayaran, user experience terhadap aplikasi yang lambat, dan kendala saat masuk aplikasi (login aplikasi)
plot5 <- ggplot(head(words.df,7), aes(y = reorder(word,freq), x = freq)) +
geom_col(aes(fill = freq, text = label), show.legend = F) +
labs(x = "Frekuensi",
y = "Terms/Kata",
title = "Frekuensi Kata Tertinggi") +
scale_x_continuous(labels = comma) +
scale_fill_gradient(low = "#85c946", high = "#304919") +
theme_minimal() +
theme(axis.text.y = element_text(face = "bold", size = 11))
ggplotly(plot5, tooltip = "text")Hasil barplot di atas memberi kita sedikit gambaran mengenai poin yang sering di bahas pada dokumen kita, hal ini ditunjukan dengan seberapa banyak terms tersebut muncul dalam dokumen. Namun hasil ini belum dapat kita interpretasikan, karena kita tidak mengetahui apakah satu kata dan lainnya memiliki hubungan atau bahkan tidak memiliki hubungan (topik yang berbeda)
Pada tahap ini saya akan menjelaskan sedikit mengenai LDA. Kita telah
melihat nilai frekuensi kemunculan terms pada tahap sebelumnya, mungkin
jika kita membaca teks review satu persatu kita dapat memahami dan
menyimpulkan masalah umum yang sering dibahas dalam sebuah review dengan
mengasosiasikan beberapa teks yang telah kita baca dengan hasil
frekuensi kemunculan terms/kata (misal tiket, bayar, pesan, lemot, beli,
error, masuk). Namun tanpa membaca teks yang utuh sama sekali dan hanya
mengandalkan frekuensi kemunculan suatu kata dapatkah kita mengetahui
masalah apa yang sebenarnya muncul? Mari ambil contoh dengan menggunakan
kata masuk. Kata masuk merupakan salah satu kata yang
sering muncul, apakah maksud dari kata masuk yang dibahas
oleh reviewers? Apa permasalahan yang dibahas dengan keyword
masuk?
LDA dapat membantu kita dalam menjawab hal ini, LDA akan mempelajari
pola dan mengelompokan kata-kata ke dalam dalam sebuah topik dengan
memperhitungkan probabilitas kemunculan kata antar satu dan lainnya
sehingga makna yang dibahas menjadi lebih mudah dipahami karena sebuah
topik mengandung kumpulan kata-kata (bag of words) sesuai dengan nilai
hubungan kemunculannya. Misal bisa saja kata masuk
dikelompokkan dengan kata error, password,
susah, dan akun, karena nilai masuk selalu
diikuti dengan kata-kata tersebut dengan begitu kita dapat
mmenginterpretasikan bahwa topik tersebut membahas mengenait kendala Log
in akun yang dialami pengguna aplikasi. Bagaimana LDA bekerja? LDA akan
menggunakan bobot nilai kemungkinan kemunculan suatu kata dengan kata
yang lain sehingga menghasilkan suatu kumpulan kata dengan nilai -nilai
probabilitas kata tersebut pada sebuah topik. Singkatnya LDA dapat
digunakan untuk meringkas, melakukan klasterisasi, menghubungkan maupun
memproses data yang sangat besar karena LDA menghasilkan daftar topik
yang diberi bobot untuk masing-masing dokumen.
Dengan kata lain model LDA ini cocok untuk kasus ini dalam mencari topik utama yang ada dalam review tahun 2022.
Sebelum kita membagi data kita kedalam beberapa topik, saya akan
membuat plot yang menampilkan terms dengan frekuensiya secara general.
Kita dapat mengimplementasikan ide ini dengan fungsi
wordcloud2
colors.wc <- brewer.pal(8, "Dark2")
wordcloud2(words.df, size = 1.5)Dengan tampilan gambar diatas kita dapat mengangkap informasi menyangkut reiew tahun 2022 dengan lebih cepat dan menarik (dilihat dengan frekuensi kemunculan kata). Dengan demikian dapat kita simpulkan bahwa berikut isu yang sedang terjadi di tahun 2022:
Berkaitan dengan nilai rating 1 yang merupakan kelas mayoritas, pertama-tama saya akan berfokus pada kata yang memilki sifat negatif sehingga dapat diasosiasikan dengan isu-isu yang terjadi di tahun 2022 dan menyebabkan banyaknya rating 1 yang diberikan oleh customer:
Note: kata-kata dibawah tidak diurutkan berdasarkan dengan nilai frekuensi aslinya namun semata-mata yang terlihat jelas pada gambar wordcloud
Note: kata-kata negatif dapat diasosiasikan dengan customer experience terhadap aplikasi maupun sifat isu yang terjadi
Note: kata-kata netral dapat diasosikan dengan hal/objek dari isu yang terjadi
Untuk dapat memahami apa permasalahan yang terjadi secara lebih jelas dan detail sesuai topiknya, akan dibahas pada tahap selanjutnya dengan mengaplikasikan model LDA
Topic modeling adalah metode unsupervised machine learning untuk membagi natural grup sehingga kita dapat memahami konteks topik yang dibahas dalam sebuah dokumen teks.
Latent Dirichlet Allocation ada sebuah metode yang populer untuk
fitting sebuah topik pada dokumen yang menggunakan algoritma dengan
prinsip bahwa setiap dokumen memiliki topik dan topik memiliki proporsi
dari gabungan kata yang memiliki asosiasi pada topik tersebut. LDA akan
mengestimasikan mikstur kata pada topik dengan metode matematis. Nilai
topik pada model LDA ditentukan dengan nilai k, serta
beberapa komponen yang dapat kita atur dan sesuaikan dengan analisis
lebih lanjut.
Pada tahap ini saya akan melihat frekuensi kemunculan kata berdasarkan kelompok score nya:
words.count <- review2022.clean %>%
unnest_tokens(output = "word", input = cleanReview) %>%
count(score, word, sort = TRUE)
words.dtm <- words.count %>%
cast_dtm(document = score, term = word, value = n)
inspect(words.dtm)#> <<DocumentTermMatrix (documents: 5, terms: 6191)>>
#> Non-/sparse entries: 9240/21715
#> Sparsity : 70%
#> Maximal term length: 36
#> Weighting : term frequency (tf)
#> Sample :
#> Terms
#> Docs bagus bayar beli error guna lemot masuk pesan susah tiket
#> 1 164 1065 466 459 362 797 416 1247 368 1711
#> 2 44 239 80 108 68 161 93 309 103 372
#> 3 46 188 66 78 59 75 73 208 48 252
#> 4 44 83 30 20 21 22 26 67 13 107
#> 5 233 83 81 17 75 17 38 139 21 255
Melihat hasil DTM secara langsung menunjukan bahwa pada rating 4 & 5 juga ditemukan keyword negatif seperti error, lemot, susah walaupun dengan frekuensi yang kecil jika dibandingkan dengan rating rendah. Kita perlu mengingat bahwa proporsi rating 4 & 5 tidak seimbang (4922 rating 1, 379 rating 4, 1709 rating 5), namun begitu tidak hal juga menunjukan bahwa user aplikasi mengalami kendala yang serupa bahkan ketika memberikan rating yang tinggi
Hasil DTM juga menampilkan informasi bahwa kita memiliki 6.191 terms
(kata) sedangkan documents berjumlah 5 adalah pengelompokan dokumen yang
saya tentukan untuk melihat kelompok jumlah terms berdasarkan score
ratingnya. Kita kemudian dapat mengolah hasil DTM ke topik model LDA
dengan melihat probabilitas kemunculan terms pada kelompoknya dengan
menggunakan nilai beta pada model LDA. Maka dari itu kita
akan membuat model LDA awal sebagai base model yang untuk kemudian dapat
ditunning kembali untuk menyesuaikan model yang lebih baik. Tahap awal
pembuatan LDA base ini akan digunakan nilai k = 5 untuk
merepresentasikan kelompok nilai rating
# membuat model LDA dengan nilai k = 5 sesuai dengan kelompok rating
base.LDA <- LDA(words.dtm, k = 5, control = list(seed = 123))
# Mencari nilai beta pada terms untuk
base.beta <- tidy(base.LDA, matrix = "beta")Highlight Point:
Nilai probabilitas kemunculan sebuah term berdasarkan kelompok rating dapat dilihat pada nilai beta diatas
Seluruh rating memiliki probabilitas kemunculan yang mendekati nilai yang seimbang pada term tiket mengindikasikan bahwa isu mengenai tiket kemungkinan besar terjadi pada setiap kelompok rating
Seluruh rating memiliki probabilitas kemunculan yang mendekati nilai yang seimbang pada term error & lemot mengindikasikan bahwa kemungkinan user aplikasi mengalami kendala error dan lemot pada setiap kelompok rating.
Untuk mempermudah dalam melihat kemunculan terms tertinggi pada setiap kelompok rating, mari kita visualisasikan 5 nilai beta tertinggi di setiap kelompok rating:
viz.beta <- base.beta %>%
group_by(topic) %>%
slice_max(beta, n = 5) %>%
ungroup() %>%
arrange(topic, -beta) %>%
mutate(term = reorder_within(term, beta, topic))
ggplot(viz.beta,aes(beta, term, fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free") +
scale_y_reordered() +
labs(x = NULL, y = NULL) +
theme_minimal() Insight: Terms (kata) yang terdapat pada kelompok rating memberikan informasi yang cukup untuk melihat kesesuaian asosiasi kata terhadap nilai rating. Hal ini dapat kita simpulkan berdasarkan kemunculan probabilitas kata yang bersifat negatif pada rating 1 dan kemunculan probabilitas kata positif yang lebih banyak pada rating 5
Pada tahap ini saya akan melakukan tahap topik modeling kembali
sebagai model base tanpa membagi berdasarkan kategori score rating.
Tahap ini saya menggunakan fungsi FitLdaModel. Perbedaan
antara model dengan fungsi LDA dan FitLdaModel
adalah:
LDA : fungsi yang digunakan untuk mengekstrak topik dari kumpulan dokumen. Fungsi ini digunakan untuk melakukan pengelompokan dokumen berdasarkan topiknya (seperti yang saya lakukan untuk mengekstrak topik pada kelompok rating yang kita miliki)
FitLdaModel merupakan fungsi yang digunakan untuk melakukan pemodelan LDA. Fungsi ini dapat digunakan untuk menentukan jumlah topik yang diinginkan, menentukan parameter alpha dan beta, dan mengevaluasi hasil model (hal ini saya lakukan karena saya akan merinci topik-topik yang terjadi di tiap ratingnya). Hal ini akan memberikan kita informasi bahwa topic apa yang mempengaruhi user dalam memberikan nilai rating tersebut.
Sebagai base model saya akan menentukan nilai k secara subjektif untuk menilai kebaikan model dan analisis penyesuaian parameter yang akan dilakuakan. Adapun sebagi base model saya menentukan iterasi sebayak 1000 kali dan burnin sebayak 500 kali (burnin adalah jumlah iterasi yang dilakukan sebelum melakukan sampling), dan parameter lainnya menggunakan nilai default
LDA.matrix <- Matrix::Matrix(as.matrix(text.dtm), sparse = T)
set.seed(123)
model.LDA1 <- FitLdaModel(LDA.matrix,
k = 3,
iterations = 1000,
burnin = 500,
calc_coherence = T
)
str(model.LDA1)#> List of 7
#> $ phi : num [1:3, 1:877] 0.00144588 0.00000295 0.00000241 0.0000048 0.00000295 ...
#> ..- attr(*, "dimnames")=List of 2
#> .. ..$ : chr [1:3] "t_1" "t_2" "t_3"
#> .. ..$ : chr [1:877] "abal" "acc" "access" "accsess" ...
#> $ theta : num [1:8681, 1:3] 0.02326 0.00699 0.01887 0.20388 0.07692 ...
#> ..- attr(*, "dimnames")=List of 2
#> .. ..$ : chr [1:8681] "1" "2" "3" "4" ...
#> .. ..$ : chr [1:3] "t_1" "t_2" "t_3"
#> $ gamma : num [1:3, 1:877] 0.99334 0.00329 0.00338 0.006 0.00597 ...
#> ..- attr(*, "dimnames")=List of 2
#> .. ..$ : chr [1:3] "t_1" "t_2" "t_3"
#> .. ..$ : chr [1:877] "abal" "acc" "access" "accsess" ...
#> $ data :Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
#> .. ..@ i : int [1:43314] 1840 2199 2344 3244 4469 6278 7588 8669 210 774 ...
#> .. ..@ p : int [1:878] 0 8 15 460 468 489 500 509 513 518 ...
#> .. ..@ Dim : int [1:2] 8681 877
#> .. ..@ Dimnames:List of 2
#> .. .. ..$ Docs : chr [1:8681] "1" "2" "3" "4" ...
#> .. .. ..$ Terms: chr [1:877] "abal" "acc" "access" "accsess" ...
#> .. ..@ x : num [1:43314] 2 2 2 2 2 2 2 2 1 2 ...
#> .. ..@ factors : list()
#> $ alpha : Named num [1:3] 0.1 0.1 0.1
#> ..- attr(*, "names")= chr [1:3] "t_1" "t_2" "t_3"
#> $ beta : Named num [1:877] 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 ...
#> ..- attr(*, "names")= chr [1:877] "abal" "acc" "access" "accsess" ...
#> $ coherence: Named num [1:3] 0.1324 0.0655 0.085
#> ..- attr(*, "names")= chr [1:3] "t_1" "t_2" "t_3"
#> - attr(*, "class")= chr "lda_topic_model"
Pada struktur model LDA, kita mendapat atribut nilai yang dapat kita perhatikan seperti:
Nilai posterior adalah nilai distribusi probabilitas setelah data diperoleh (Bayes Theorem)
Mari kita lihat 10 terms dengan nilai probabilitas terms paling tinggi di setiap topic nya
GetTopTerms(model.LDA1$phi, 7) %>%
as.data.frame() %>%
set_names(paste("Topic", 1:3))interpretasi Topic
Topik terbagi menjadi isu login (administrasi), isu pemesanan tiket dan isu pembayaran tiket
Jika kita perhatikan, terdapat pengulangan terms pada topik. Hal ini memang bisa saja terjadi jika memang terdapat probability term tersebut muncul pada suatu topik, namun kita perlu memastikan nilai probabilitynya karena hal iini berkaitan dengan efektifitas banyak topik yang kita tentukan.Hal ini juga dapat menunjukan bahwa nilai kelompok yang kita tentukan masih sangat general. Kita perlu melakukan pembagian topik ke jumlah yang lebih besar untuk dapat menemukan topik yang masih belum ditemukan (masik tersembunyi dibalik pembagian topik yang terlalu general/umum).
Dalam mencari nilai optimum jumlah topik yang akan kita tentukan, kita dapat menilai kebaikan model dari nilai coherence sebagai acuannya. Nilai coherence dari sebuah model LDA menunjukan ukuran konsistensi dan interpretabilitas topik. Nilai yang semakin tinggi menunjukan bahwa kata-kata dalam setiap topik terkohesi dan membentuk konsep yang baik dan dapat diinterpretasi karena tidak tumpang tindih/bias dalam tiap topiknya.
mean(model.LDA1$coherence)#> [1] 0.09431791
Nilai coherence dari base model kita menunjukan poin 0.09 nilai ini akan menjadi nilai dasar/ukuran untuk menentukan nilai tinggi dan nilai rendah. Untuk mempermudah dalam mencari nilai optimum k kita akan membuat sebuah fungsi dengan struktur proses:
avg.coherece <- function(list.num){
values <- c()
for(num in list.num){
set.seed(123)
model <- FitLdaModel(dtm = LDA.matrix,
k = num,
alpha = 10,
beta = 1,
iterations = 1000,
burnin = 500,
calc_coherence = TRUE,
cpus = 4)
average <- mean(model$coherence)
values <- append(values, average)
}
return(values)
}Note: saya menyarankan untuk menambahkan parameter cpus pada fungsi untuk mempercepat komputasi. Nilai cpus dapat disesuaikan dengan hardware yang anda gunakan. Untuk efisiensi saya juga menyarankan anda untuk dapat menyesuaikan parameter alpha, beta, iterasi dan burnin dengan nilai yang lebih rendah sesuai dengan data yang dimiliki. Fungsi ini hanya sebagai ukuran saja, nilai coherence bisa saja berubah setelah tunning iterasi dan burnin diubah pada model asli namun konsep nilai tertinggi pada model dengan nilai k tertentu akan menghasilkan distribusi yang serupa
# menyimpan sekuen nilai K
k.list <- 1:10
# menghitung nilai average coherence
model.list <- avg.coherece(k.list)
model.list %>% as.data.frame() %>%
rownames_to_column()Sesuai dengan hasil diatas, k optimum adalah dengan nilai 9 topik. Mari kita lihat hasil dari model tersebut:
set.seed(123)
model.LDA2 <- FitLdaModel(LDA.matrix,
k = 9,
alpha = 10,
beta = 1,
iterations = 1000,
burnin = 500,
calc_coherence = T,
calc_likelihood = TRUE,
calc_r2 = TRUE,
cpus = 4
)
mean(model.LDA2$coherence)#> [1] 0.08880988
top.topic <- GetTopTerms(model.LDA2$phi, 30) %>%
as.data.frame() %>%
set_names(paste("Topic", 1:9))
top.topicSetelah penyesuaian banyak topik menjadi 9 topik, terms dengan probabilitas tinggi yang masuk ke dalam sebuah topik memiliki makna yang lebih spesifik.
head(top.topic, 10) %>%
rownames_to_column("id") %>%
mutate(id = as.numeric(id)) %>%
pivot_longer(-id, names_to = "topic", values_to = "term")%>%
ggplot(aes(label = term, size = rev(id), color = topic, alpha = rev(id))) +
geom_text_wordcloud(seed = 123) +
facet_wrap(~topic, scales = "free") +
scale_alpha_continuous(range = c(0.4, 1)) +
theme_minimal() +
theme(strip.background = element_rect(fill = "black"),
strip.text.x = element_text(colour = "white"))Terlihat pada hasil model memiliki topik memiliki kata-kata positif yang bercampur. Mengingat bahwa data kita memiliki nilai distribusi rating rendah terbanyak dan fokus pada kasus ini adalah mencari isu yang menyebabkan hal itu terjadi, saya akan melakukan eliminasi kata-kata yang menunjukan nilai kepuasan dan kembali membuat model LDA yang berfokus pada isu dari aplikasi KAI Access.
Pada model akhir, saya akan mengeliminasi kata positif kemudian kembali dengan melakukan proses topik medeling. Tahap sebelumnya membantu kita dalam mengidentifikasi kata positif yang muncul pada data kita sehingga kita dapat menentukan kata yang akan dihapus secara lebih mudah.
# Membaca dan menyimpan data text berisi kumpulan kata positif pada dokumen
postv.terms <- readLines("DataSet/positiveterms.txt")
# Eliminasi terms positif dari cleanReview dan menyipan ke variable berbeda (clean.positive)
clean.positive <- removeWords(review2022.clean$cleanReview,
words = postv.terms)
# Menghapus spasi berlebih
clean.positive <- strip(clean.positive)
# Menyimpan data (eliminasi kata positif) ke dataframe
review2022.clean <- review2022.clean %>%
mutate(clean2 = clean.positive)# Membuat corpus-DTM-Matrix dengan cleanreview yang bersih dari kata positif
corpus2 <- VCorpus(VectorSource(review2022.clean$clean2))
dtm.lda2 <- DocumentTermMatrix(x = corpus2)
LDA.matrix2 <- Matrix::Matrix(as.matrix(dtm.lda2), sparse = T)set.seed(123)
model.LDA3 <- FitLdaModel(LDA.matrix2,
k = 5 ,
alpha = 10,
beta = 1,
iterations = 1000,
burnin = 500,
calc_coherence = T,
calc_likelihood = TRUE,
calc_r2 = TRUE,
cpus = 4
)
mean(model.LDA3$coherence)#> [1] 0.1019778
Setelah menghapus kata-kata positif, terlihat nilai coherence meningkat menunjukan model LDA yang lebih baik.
Penentuan nilai k = 5 adalah setelah dilakuakn explorasi pada data yang bersih dari nilai positif. Eksplorasi tersebut tidak saya masukan untuk efisiensi waktu
Mari kita periksa distribusi kata pada tiap topiknya
top.topic2 <- GetTopTerms(model.LDA3$phi, 30) %>%
as.data.frame() %>%
set_names(paste("Topic", 1:5))
top.topic2Setelah penyesuaian banyak topik menjadi 8 topik, terms dengan probabilitas tinggi yang masuk ke dalam sebuah topik memiliki makna yang lebih baik
head(top.topic2, 10) %>%
rownames_to_column("id") %>%
mutate(id = as.numeric(id)) %>%
pivot_longer(-id, names_to = "topic", values_to = "term")%>%
ggplot(aes(label = term, size = rev(id), color = topic, alpha = rev(id))) +
geom_text_wordcloud(seed = 123) +
facet_wrap(~topic, scales = "free") +
scale_alpha_continuous(range = c(0.4, 1)) +
theme_minimal() +
theme(strip.background = element_rect(fill = "black"),
strip.text.x = element_text(colour = "white"))Hasil dari model final menunjukan distribusi kata yang baik ke tiap topiknya, begitu pula dengan banyak topik yang dinilai telah menunjukan banyak topik yang optimal. Sesuai dengan kumpulan kata tiap topik kita dapat mengiterpretasikan topik ke dalam tema yang dapat mendeskripsikannya:
Setelah kita menemukan nilai optimum topik pada data kita, kita dapat mengkategorikan data teks asli ke dalam kelompok topiknya dengan mengacu kepada nilai theta tertinggi. Nilai theta tertinggi menunjukan bahwa row tersebut memiliki probabilitas paling tinggi ke dalam topik tertentu. Sehingga kita dapat melakukan join antara data frame dan hasil model LDA kemudian melakukan proses case_when pada data frame kita:
# Membuat data frame dari model LDA dengan menggunakan index sebagai key untuk join dengan data frame awal
lda3.theta <- model.LDA3$theta %>%
as.data.frame() %>%
rownames_to_column("index")
# Melakukan join data frame dan melakukan case_when untuk mengelompokan teks ke dalam topik
result.final <- lda3.theta %>%
left_join(review2022.clean %>%
rownames_to_column("index"),
by = c("index" = "index")) %>%
pivot_longer(c(t_1, t_2, t_3, t_4, t_5), names_to = "topic", values_to = "theta") %>%
mutate(topic = case_when( topic == "t_1" ~ "Performa Aplikasi",
topic == "t_2" ~ "Isu Administrasi",
topic == "t_3" ~ "Isu Pemesanan Tiket",
topic == "t_4" ~ "Isu Layanan",
topic == "t_5" ~ "Isu Pembayaran",
TRUE ~ as.character(topic)),
month = month(date,label = T)) %>%
mutate_at(vars(topic, index), as.factor) %>%
group_by(index) %>%
filter(theta == max(theta)) %>%
na.omit()result.final <- result.final %>%
group_by(index) %>%
filter(length(index) ==1) %>%
select(index, score, topic, review, month, everything())#Check point save final data frame
#write.csv(result.final, "resultfinal.csv", row.names = F)
result.final <- read.csv("resultfinal.csv")
result.final <- result.final %>%
mutate_at(vars(index, score, topic), as.factor) %>%
select(-month) %>%
mutate(date = as_datetime(date),
month = month(date, label = T))result.final %>%
filter(topic == "Performa Aplikasi") %>%
select(review)Isu pada topik Performa Aplikasi:
result.final %>%
filter(topic == "Isu Administrasi") %>%
select(review)Isu pada topik Isu Administrasi:
result.final %>%
filter(topic == "Isu Pemesanan Tiket") %>%
select(review)Isu pada topik Isu Pemesanan Tiket:
result.final %>%
filter(topic == "Isu Layanan") %>%
select(review)Isu pada topik Isu Layanan:
result.final %>%
filter(topic == "Isu Pembayaran") %>%
select(review)Isu pada topik Isu Pembayaran:
topic.df <- result.final %>%
group_by(topic, month) %>%
summarise(Total = n()) %>%
ungroup() %>%
group_by(month) %>%
mutate(permonth = sum(Total),
label = glue(
"Topic : {topic}
Total: {round((Total/permonth)*100, 2)} %"))plot.topic <- ggplot(topic.df, aes(x = month, y = Total)) +
geom_line(aes(group = topic, col= topic)) +
geom_point(aes(col= topic, text = label)) +
theme_minimal() +
labs(x = "Bulan",
y = "Total",
title = "Distribusi Frekuensi Isu Tahun 2022") +
theme(plot.title = element_text(size = 14, face = "bold"),
axis.text.x = element_text(size = 11, face = "bold"),
axis.title = element_text(hjust = 0))
ggplotly(plot.topic, tooltip = "text")Melihat hasil grafik distribusi isu topic per periode bulan di tahun 2022 berikut kesimpulan utama (urgensi) yang didapatkan untuk aplikasi development KAI Access: