Pengantar

Rpubs ini mendemonstrasikan penerapan analisis mikro pada suatu teks dengan menggunakan bahasa pemrograman R sebagaimana diuraikan buku “Text Analysis with R for Students of Literature”.

Bahan teks yang dijadikan acuan adalah: Cerpen Perkara Kebatilan yang Agung karya Sasmito Yudha Husada.

Cerpen tersebut merupakan kiriman partisipasi saya untuk Lomba Cerpen Fantasi LCDP 2018 yang bertemakan “The Greatest Lie”.

Pindai Teks

batil.v <- scan("data/plainText/syh_perkara_kebatilan.txt",
                     what = "character",
                     sep = "\n",
                     encoding = "UTF-8")

Praproses

Gabungkan setiap linebreak menjadi 1 vektor, dengan spasi sebagai pemisah.

nolb.batil.v <- paste(batil.v, collapse = " ")
length(nolb.batil.v)
## [1] 1

Ubah semua huruf menjadi huruf kecil.

lower.nolb.batil.v <- tolower(nolb.batil.v)

Pecah setiap kata menjadi list.

batil.words.l <- strsplit(lower.nolb.batil.v, "\\W")

Lalu ubah list menjadi vector.

batil.words.v <- unlist(batil.words.l)

Cari indeks kata yang tidak kosong.

not.blank.bwv <- which(batil.words.v != "")

Gunakan indeks untuk memilih kata pada teks.

batil.words.v <- batil.words.v[not.blank.bwv]

Analisis

Distribusi

Frekuensi kata

Mari kita lihat seberapa luas perbendaharaan kata pada teks.

length(unique(batil.words.v))
## [1] 1425

Kata-kata apa yang sering digunakan? Mari periksa tabel frekuensi kata pada teks.

tabel.frek.batil <- table(batil.words.v)
sorted.bf <- sort(tabel.frek.batil, decreasing = T)

Menurut hukum Zipf, kata paling sering muncul kedua pada sebuah corpus berjumlah setengah dibanding kata yang paling sering muncul pertama.

Mari kita lihat.

sorted.bf[1:10]
## batil.words.v
##   yang    dan    itu     ia kapten     di dengan   dari    ada    tak 
##    108     71     56     53     53     44     37     33     28     27
plot(sorted.bf[1:10])

Sepertinya tidak plek mengikuti hukum tersebut, namun juga tidak bisa dibilang melenceng jauh.

Mari membuat relatif frekuensi kata pada teks.

rf.bf <- 100 * (sorted.bf / sum(sorted.bf))
plot(rf.bf[1:10],
     type = "b",
     xlab = "Sepuluh Kata Tersering di Cerpen Perkara Kebatilan yang Agung",
     ylab = "Persentase dari Teks",
     xaxt = "n")
axis(1,
     1:10,
     labels = names(rf.bf[1:10]))

Ilustrasi Sebaran Kata

Buat urutan angka sepanjang teks untuk sumbu x.

bl <- length(batil.words.v)
num.batil <- seq(1:bl)

Cari indeks lokasi kata tertentu.

batil.in.batil <- which(batil.words.v == "batil")

Buat urutan data kosong (NA) untuk inisiasi ruang sumbu y.

batil.count <- rep(NA, length(num.batil))

Isi data kosong pada indeks lokasi kata yang ditentukan untuk mengisi sumbu y.

batil.count[batil.in.batil] <- 1

Ilustrasi sebaran kata target pada teks.

plot(batil.count,
     main = "Ilustrasi Sebaran Kata \"Batil\" pada Perkara Kebatilan yang Agung",
     xlab = "Selang Waktu Teks",
     ylab = "\"Batil\"",
     type = "h",
     ylim = c(0,1),
     yaxt = "n")

Mari tinjau sebaran kata lain.

agung.in.batil <- which(batil.words.v == "agung")
batil.count[agung.in.batil] <- 1
plot(batil.count,
     main = "Ilustrasi Sebaran Kata \"Agung\" pada Perkara Kebatilan yang Agung",
     xlab = "Selang Waktu Teks",
     ylab = "\"Kapten\"",
     type = "h",
     ylim = c(0,1),
     yaxt = "n")

Pemecah Bagian Cerita

Mari menentukan indeks pemecah cerita dengan regex.

i.batil.break.v <- grep("\\[\\.\\d\\]",
                      batil.v)

Cek kebenaran indeks.

batil.v[i.batil.break.v]
## [1] "[.1]" "[.2]" "[.3]" "[.4]"

Untuk keperluan analisis pada pecahan teks dengan indeks nanti, kita dapat menggunakan awal pemecah berikutnya yang dikurangi angka 1. Agar pecahan cerita terakhir dapat dideteksi, kita perlu tambahkan 1 pemecah penanda bagian akhir.

batil.v <- c(batil.v, "END")   
last.break.batil <- length(batil.v)
i.batil.break.v <- c(i.batil.break.v, last.break.batil)

Buat ruang untuk pemrosesan analisis tiap pecahan teks dalam bentuk list

break.raw.batil.l <- list()
break.relative.freq.batil.l <- list()

Buat fungsi untuk analisis tiap pecahan teks.

for(n in 1:length(i.batil.break.v)){
  if(n != length(i.batil.break.v)){
    break.title <- batil.v[i.batil.break.v[n]]
    break.start <- i.batil.break.v[n] + 1
    break.end <- i.batil.break.v[n + 1] - 1
    break.lines.v <- batil.v[break.start:break.end]
    break.words.v <- tolower(paste(break.lines.v, collapse = " "))
    break.words.l <- strsplit(break.words.v, "\\W")
    break.token.word.v <- unlist(break.words.l)
    break.token.word.v <- break.token.word.v[which(break.token.word.v != "")]
    break.frek.t <- sort(table(break.token.word.v),decreasing = T)
    break.raw.batil.l[[break.title]] <- break.frek.t
    break.frek.rel.t <- (break.frek.t/sum(break.frek.t)) * 100
    break.relative.freq.batil.l[[break.title]] <- break.frek.rel.t
  }
}

Cek hasil analisis tiap pecahan teks.

break.raw.batil.l[[1]][1:10] # 10 kata terbanyak pada pecahan pertama
## break.token.word.v
##   itu  yang  anak   dan    di   ibu tanah agung  dari    ke 
##    26    19    16    16    14    14    10     8     8     8
break.raw.batil.l[[2]][1:10] # 10 kata terbanyak pada pecahan kedua
## break.token.word.v
##   yang kapten    dan dengan     di     ia    ada   dari  paman    tak 
##     32     31     19     17     16     16     14     11     11     11
break.raw.batil.l[[3]][1:10] # 10 kata terbanyak pada pecahan ketiga
## break.token.word.v
##   yang    dan     ia    itu   jiwa  dalam     di   juga kapten    tak 
##     25     18     16     11     11     10      9      9      9      9
break.relative.freq.batil.l[[1]][1:10] # 10 kata terbanyak pada pecahan pertama
## break.token.word.v
##      itu     yang     anak      dan       di      ibu    tanah    agung 
## 3.471295 2.536716 2.136182 2.136182 1.869159 1.869159 1.335113 1.068091 
##     dari       ke 
## 1.068091 1.068091
break.relative.freq.batil.l[[2]][1:10] # 10 kata terbanyak pada pecahan kedua
## break.token.word.v
##     yang   kapten      dan   dengan       di       ia      ada     dari 
## 2.946593 2.854512 1.749540 1.565378 1.473297 1.473297 1.289134 1.012891 
##    paman      tak 
## 1.012891 1.012891
break.relative.freq.batil.l[[3]][1:10] # 10 kata terbanyak pada pecahan ketiga
## break.token.word.v
##     yang      dan       ia      itu     jiwa    dalam       di     juga 
## 3.209243 2.310655 2.053915 1.412067 1.412067 1.283697 1.155327 1.155327 
##   kapten      tak 
## 1.155327 1.155327

Gunakan list yang sudah dibuat untuk mencari frekuensi kata tertentu.

break.raw.batil.l[[1]]["batil"]
## batil 
##     3
break.raw.batil.l[[2]]["batil"]
## batil 
##     5
break.raw.batil.l[[3]]["batil"]
## batil 
##     7
break.relative.freq.batil.l[[1]]["agung"]
##    agung 
## 1.068091
break.relative.freq.batil.l[[2]]["agung"]
##     agung 
## 0.6445672
break.relative.freq.batil.l[[3]]["agung"]
##     agung 
## 0.2567394

Untuk menerapkan pencarian kata pada semua pecahan teks, kita gunakan lapply.

raw.batil.l <- lapply(break.raw.batil.l,
                      "[",
                      "batil")
raw.agung.l <- lapply(break.raw.batil.l,
                      "[",
                      "agung")
rf.batil.l <- lapply(break.relative.freq.batil.l,
                     "[",
                     "batil")
rf.agung.l <- lapply(break.relative.freq.batil.l,
                     "[",
                     "agung")

Dengan do.call dan rbind, kita ubah hasil pencarian sebelumnya menjadi matrix.

raw.batil.m <- do.call(rbind,
                       raw.batil.l)
raw.agung.m <- do.call(rbind,
                       raw.agung.l)
rf.batil.m <- do.call(rbind,
                      rf.batil.l)
rf.agung.m <- do.call(rbind,
                      rf.agung.l)
raw.batil.m[1:5] 
## [1]  3  5  7 10 NA
raw.agung.m[1:5] 
## [1]  8  7  2  8 NA
rf.batil.m[1:5]
## [1] 0.4005340 0.4604052 0.8985879 1.2836970        NA
rf.agung.m[1:5]
## [1] 1.0680908 0.6445672 0.2567394 1.0269576        NA

Kemudian kita bisa kombinasikan pilihan kata untuk ditelusuri lebih lanjut.

batil.v <- rf.batil.m[,1]
agung.v <- rf.agung.m[,1]
batil.agung.m <- cbind(batil.v, agung.v)
colnames(batil.agung.m) <- c("Batil", "Agung")

Ilustrasikan distribusi kata pada tiap pemecah kata dengan diagram.

barplot(batil.agung.m,
        beside = T,
        col = "grey")

Korelasi

Sebelum melakukan perhitungan korelasi. Kita harus mengganti NA dengan angka 0.

batil.agung.m[which(is.na(batil.agung.m))] <- 0
cor(batil.agung.m[,1], batil.agung.m[,2])
## [1] -0.01289829

Kata yang dipilih memiliki hubungan berbanding terbalik yang lemah. Namun apakah korelasi ini benar adanya? Bagaimana dengan kata-kata lain dalam teks yang berfungsi sebagai kata pengganti yang merujuk pada makna kata yang sama? Untuk menguji hal ini, kita perlu lakukan randomisasi.

Ubah dulu matriks menjadi data frame.

batil.agung.df <- as.data.frame(batil.agung.m)

Randomisasi.

set.seed(1)
randomed.batil1 <- sample(batil.agung.df$Batil)
randomed.agung1 <- sample(batil.agung.df$Agung)
set.seed(2)
randomed.batil2 <- sample(batil.agung.df$Batil)
randomed.agung2 <- sample(batil.agung.df$Agung)
set.seed(3)
randomed.batil3 <- sample(batil.agung.df$Batil)
randomed.agung3 <- sample(batil.agung.df$Agung)

Uji apakah nilai korelasi masih sama dengan sebelumnya?

Nilai korelasi sesudah randomisasi:

cor(randomed.batil1, randomed.agung1)
## [1] -0.01289829
cor(randomed.batil2, randomed.agung2)
## [1] 0.07880945
cor(randomed.batil3, randomed.agung3)
## [1] 0.06428399

Bandingkan dengan nilai korelasi sebelum randomisasi:

Sebelum randomisasi.

cor(batil.agung.m[,1], batil.agung.m[,2])
## [1] -0.01289829

Nilainya sungguh tidak tentu, namun 3 kali randomisasi belum cukup. Mari kita coba dengan 10.000 kali randomisasi.

batil.agung.10k.cor <- NULL
for(i in 1:10000){
  batil.agung.10k.cor<- c(batil.agung.10k.cor,
                          cor(sample(batil.agung.df$Batil), (batil.agung.df$Agung)))
}
min(batil.agung.10k.cor )
## [1] -0.9991016
max(batil.agung.10k.cor )
## [1] 0.8793164
range(batil.agung.10k.cor )
## [1] -0.9991016  0.8793164
mean(batil.agung.10k.cor )
## [1] -0.003952307
sd(batil.agung.10k.cor )
## [1] 0.5791403

Korelasi batil dan agung pada data awal memiliki nilai -0.01289829. Namun setelah dilakukan 10.000 kali randomisasi, kita dapatkan rata-rata korelasi -0.005962922 dengan standar deviasi 0.5736525.

hist(batil.agung.10k.cor,
     breaks = 100,
     xlab = "Korelasi \"Batil\" dan \"Agung\"",
     main = "Korelasi dari 10.000 Randomisasi \"Batil\" dan \"Agung\"")
abline(v = -0.01289829, col="red", lwd=3, lty=2)

Sebaran korelasi Batil dan Agung menunjukan keseimbangan antara tidak adanya korelasi dan adanya korelasi positif/negatif. Dengan standar deviasi yang cukup besar 0.5736525, hubungan batil dan agung ini begitu tidak stabil, antara ada dan tiada, antara sejalan dan berlawanan.

Terlepas dari analisis di atas, sebagai penulis teks tersebut, saya berpendapat distribusi korelasi itu bisa dianggap sebagai karikatur hubungan kata kunci Batil dan Agung sebagaimana tersurat dalam cerpen itu sendiri: narasi kebalikan konsep problem of evil menjadi problem of good. Segala dalih yang digunakan untuk menepis konsekuensi dari bukti-bukti kebatilan dunia, dapat dijungkirbalikkan pada bukti-bukti keagungan dunia untuk menatap spekulasi konsekuensi yang kelam.