For Loop Vs lapply

TL;DR

Saya ingin membandingkan dua kode dalam bahasa R yang saya susun sedemikian rupa sehingga menghasilkan objek yang sama. Kasus: data lowongan kerja.

Pustaka R

Pada tes ini, dibutuhkan beberapa package di antaranya:

lapply(
  c(
    "httr",
    "jsonlite",
    "dplyr",
    "tidyr",
    "janitor",
    "waldo",
    "microbenchmark"
  ), function(x) if(!require(x, character.only = TRUE)) install.packages(x)
)

Juga sebuah fungsi yang ditulis oleh abang-abang Jerman bernama-akun rentrop seperti yang ada di sini. Fungsi tersebut memanfaatkan pustaka httr dan jsonlite untuk berinteraksi dengan data menggunakan servis API berbasis query bernama GraphQL. Saya simpan sebagai berkas skrip R dengan nama “function_graphql.R” dan memanggilnya dengan source() dari konsol R.

source("function_graphql.R")

Parameter dan Query

Contoh data yang digunakan merupakan data lowongan pekerjaan dari Jobstreet. Ada beberapa parameter yang dibutuhkan, di antaranya tentu saja ada URL ke servis API-nya yang pada kasus ini saya simpan dalam variabel url. Kemudian karena yang saya lakukan sama seperti ketika kita mencari lowongan melalui antar muka web menggunakan suatu kata kunci, maka perlu ada key untuk menyaring lowongan pekerjaan yang sesuai. Berikutnya variabel page juga diperlukan karena ini sama dengan kita mencari lowongan kerja di web, ada halaman-halaman yang perlu dilalui.

url <- "https://xapi.supercharge-srp.co/job-search/graphql?country=id&isSmartSearch=true"
key <- "data analyst"
page <- 1

Selain query untuk akses basis data milik Jobstreet di URL tersebut, dibutuhkan beberapa parameter lain yakni variabel pencarian guna memfilter hasilnya, ditulis dalam format json. Untuk variabel pencarian saya simpan pada objek atau variabel var seperti berikut:

var <- sprintf(
  '{
    "keyword": "%s",
    "jobFunctions": [
      501,
      503,
      508,
      512
    ],
    "locations": [],
    "salaryType": 1,
    "jobTypes": [],
    "createdAt": null,
    "careerLevels": [],
    "page": %s,
    "country": "id",
    "sVi": "",
    "categories": [
      "501",
      "503",
      "508",
      "512"
    ],
    "workTypes": [],
    "industries": [],
    "locale": "id"
  }', key, page
)

Sementara itu query yang digunakan lumayan panjang juga, seperti berikut:

query <- 
  'query getJobs($country: String, $locale: String, $keyword: String, $createdAt: String, $jobFunctions: [Int], $categories: [String], $locations: [Int], $careerLevels: [Int], $minSalary: Int, $maxSalary: Int, $salaryType: Int, $candidateSalary: Int, $candidateSalaryCurrency: String, $datePosted: Int, $jobTypes: [Int], $workTypes: [String], $industries: [Int], $page: Int, $pageSize: Int, $companyId: String, $advertiserId: String, $userAgent: String, $accNums: Int, $subAccount: Int, $minEdu: Int, $maxEdu: Int, $edus: [Int], $minExp: Int, $maxExp: Int, $seo: String, $searchFields: String, $candidateId: ID, $isDesktop: Boolean, $isCompanySearch: Boolean, $sort: String, $sVi: String, $duplicates: String, $flight: String, $solVisitorId: String) {
  jobs(country: $country, locale: $locale, keyword: $keyword, createdAt: $createdAt, jobFunctions: $jobFunctions, categories: $categories, locations: $locations, careerLevels: $careerLevels, minSalary: $minSalary, maxSalary: $maxSalary, salaryType: $salaryType, candidateSalary: $candidateSalary, candidateSalaryCurrency: $candidateSalaryCurrency, datePosted: $datePosted, jobTypes: $jobTypes, workTypes: $workTypes, industries: $industries, page: $page, pageSize: $pageSize, companyId: $companyId, advertiserId: $advertiserId, userAgent: $userAgent, accNums: $accNums, subAccount: $subAccount, minEdu: $minEdu, edus: $edus, maxEdu: $maxEdu, minExp: $minExp, maxExp: $maxExp, seo: $seo, searchFields: $searchFields, candidateId: $candidateId, isDesktop: $isDesktop, isCompanySearch: $isCompanySearch, sort: $sort, sVi: $sVi, duplicates: $duplicates, flight: $flight, solVisitorId: $solVisitorId) {
    ...LegacyCompat_SearchResult
  }
}

fragment LegacyCompat_SearchResult on SearchResult {
  solMetadata
  jobs {
    id
    sourceCountryCode
    isStandout
    companyMeta {
      id
      isPrivate
      name
    }
    jobTitle
    jobUrl
    jobTitleSlug
    description
    employmentTypes {
      name
    }
    sellingPoints
    locations {
      code
      name
      children {
        code
        name
      }
    }
    categories {
      code
      name
      children {
        code
        name
      }
    }
    postedAt
    salaryRange {
      currency
      max
      min
      period
      term
    }
    isClassified
  }
}'

Sebetulnya saya juga kurang begitu paham dan masih mempelajari salah satu metode servis web ini. Untuk query yang panjang ini saya copas dari hasil inspect element di peramban yang saya gunakan. Penjelasan tentang query untuk GraphQL mungkin bisa dipelajari pada laman berikut.

Mengambil Data

Tiba saatnya untuk coba menarik (pulling) data. Tidak berjalan lama karena hanya sekitar 30 rekaman di halaman pertama saja. Di sinilah fungsi GQL() dari Abang rentrop menjadi sangat bermanfaat.

jobs <- GQL(
  query = query,
  .variables = var,
  .url = url
)

job <- jobs$jobs$jobs
str(job[[1]])

Akan dihasilkan objek dengan struktur list. Saya definisikan dalam job saja. Setelah subset konten yang dibutuhkan saja menjadi objek job, kita bisa amati strukturnya menjadi seperti berikut ini (hanya item pertama dari 30 sebagai sampel, karena tentu saja strukturnya akan berulang):

List of 15
 $ id               : chr "3641190"
 $ sourceCountryCode: chr "id"
 $ isStandout       : logi TRUE
 $ companyMeta      :List of 3
  ..$ id       : chr "433563"
  ..$ isPrivate: logi FALSE
  ..$ name     : chr "PT Indoglobal Nusa Persada"
 $ jobTitle         : chr "SYSTEM ANALYST"
 $ jobUrl           : chr "https://www.jobstreet.co.id/id/job/system-analyst-3641190?token=0~1daa0355-a242-4f92-87a9-90dbbca3a275&sectionR"| __truncated__
 $ jobTitleSlug     : chr "system-analyst"
 $ description      : chr "Design new IT solutions, modify, enhance or adapt existing systems and integrate new features or improvements i"| __truncated__
 $ employmentTypes  :List of 1
  ..$ :List of 1
  .. ..$ name: chr "Penuh Waktu"
 $ sellingPoints    :List of 3
  ..$ : chr "Good reputation company at the national level as edutech platform"
  ..$ : chr "Incentive / Rewards Project"
  ..$ : chr "A great environment, culture and partner"
 $ locations        :List of 1
  ..$ :List of 3
  .. ..$ code    : chr "30500"
  .. ..$ name    : chr "Jakarta Raya"
  .. ..$ children: NULL
 $ categories       :List of 2
  ..$ :List of 3
  .. ..$ code    : chr "508"
  .. ..$ name    : chr "Komputer/Teknologi Informasi"
  .. ..$ children: NULL
  ..$ :List of 3
  .. ..$ code    : chr "191"
  .. ..$ name    : chr "IT-Perangkat Lunak"
  .. ..$ children: NULL
 $ postedAt         : chr "2021-09-28T05:34:48Z"
 $ salaryRange      :List of 5
  ..$ currency: NULL
  ..$ max     : NULL
  ..$ min     : NULL
  ..$ period  : chr "monthly"
  ..$ term    : NULL
 $ isClassified     : logi FALSE

Selain menghasilkan kumpulan data lowongan, fungsi GQL() dengan query dan parameter lain di atas juga menyimpan keterangan mengenai sesi yang terjadi ketika pengambilan data berlangsung, terdapat pada:

(query_meta <- jobs$jobs$solMetadata)

Perbandingan Skrip

Kode pertama dengan lapply()

Untuk skrip pertama saya memanfaatkan fungsi lapply() yang disediakan oleh pustaka bawaan R yakni base. Agar dapat dibandingkan dengan mudah nantinya saya buat jadi fungsi v1() saja.

# method 1
v1 <- function(job){
  
  vacancy <- lapply(job, function(x){
    v <- unlist(x)
    v <- data.frame(name = names(v), value = v)
    v <- as_tibble(v) %>% 
      group_by(name) %>% 
      summarise_all(~toString(value)) %>% 
      ungroup()
    return(v)
  })
  
  vacancy <- lapply(vacancy, function(x){
    v <- x %>% 
      pivot_wider(
        names_from = "name",
        values_from = "value"
      )
    v <- janitor::clean_names(v)
    return(v)
  })
  
  v1 <- do.call("bind_rows", vacancy)
  return(v1)
  
}

vacancy1 <- v1(job)

Konten datanya dibahas di akhir saja, ya… Kali ini fokus pada perbandingan dua skrip yang saya susun dulu.

Kode kedua dengan for loop

Untuk skrip kedua, saya gunakan for loop, yakni salah satu metode dasar pada tiap bahasa pemrograman untuk mengiterasi proses pada sequencial vector atau list.

Setelah itu, dilanjutkan dengan menggunakan fungsi-fungsi dari pustaka dplyr dan tidyr, juga meminjam fungsi clean_names() dari janitor untuk hidup lebih mudah. Skrip ini hanya mereplikasi fungsi-fungsi yang berjalan dengan skrip sebelumnya, tentunya dengan penyesuaian karena struktur objek yang diproses jadi berbeda.

# method 2
v2 <- function(job){
  
  for (i in seq_along(job)) {
    v <- unlist(job[[i]])
    v <- data.frame(name = names(v), value = v)
    v <- as_tibble(v) %>% mutate(num = i)
    if(i == 1){
      vacancy <- v
    } else {
      vacancy <- bind_rows(vacancy, v)
    }
  }
  
  v2 <- vacancy %>% 
    group_by(num, name) %>%
    summarise_all(~toString(value)) %>% 
    ungroup() %>% 
    pivot_wider(
      id_cols = "num", 
      names_from = "name",
      values_from = "value"
    ) %>% 
    select(-num)
  
  v2 <- janitor::clean_names(v2)
  return(v2)
  
}

vacancy2 <- v2(job)

Perbandingan

Kedua skrip atau fungsi di atas dibuat agar hasil keluarannya pun menjadi identik. Untuk memeriksanya saya menggunakan fungsi waldo::compare() seperti berikut.

waldo::compare(vacancy1, vacancy2, x_arg = "v1", y_arg = "v2")

Menghasilkan keterangan bahwa kedua objek v1 dan v2 tidak memiliki perbedaan: ✓ No differences

Kemudian tiba saatnya pada bagian inti, yaitu perbandingan kedua metode di atas, saya menggunakan bantuan dari pustaka microbenchmark dan menyimpan hasilnya dalam variabel b agar mudah dieksplorasi nantinya.

b <- microbenchmark::microbenchmark(v1(job), v2(job))

Menghasilkan keluaran seperti berikut:

Unit: milliseconds
    expr      min       lq      mean    median        uq       max neval
 v1(job) 940.4223 999.8946 1068.7362 1025.7667 1123.6142 1444.3575   100
 v2(job) 162.2252 171.1944  186.4427  178.5282  189.2315  343.9679   100

Terjadi 100 kali pengulangan neval untuk masing-masing expression (expr), n-evaluation, katakanlah begitu. Parameter default fungsi tersebut memang mengulang operasi sebanyak 100 kali. Untuk jumlah lainnya bisa menggunakan argumen times. Dapat pula membandingkan hasil dengan boxplot. Berikut statistik untuk 300 kali pengulangan.

b2 <- microbenchmark::microbenchmark(v1(job), v2(job), times = 300L)
boxplot(b2, log = FALSE, horizontal = TRUE, ylab = "Time (millisecond)")

benchmark

Dengan for loop sekali di awal dan diikuti dengan dplyr + tidyr tampak lebih konsisten kecepatannya dan lebih baik performanya.

Merapikan Data

Setelah mengetahui baris kode mana yang baik, itulah yang saya simpan sebagai fungsi yang nantinya akan memudahkan jika ingin mengambil data lowongan kerja dari Jobstreet lewat R. Mari kita rapikan sedikit data yang sudah kita kumpulkan.

vacancy <- vacancy2 %>% 
  select(
    id, matches("job"), posted_at, matches("categories"),
    description, matches("company"), matches("employ"),
    matches("is_"), matches("location"), matches("country"), 
    matches("salary"), matches("selling")
  ) %>% 
  select(-job_title_slug)
vacancy

Hasilnya kurang lebih seperti ini dalam format data.frame kelas tibble:

# A tibble: 30 × 23
   id      job_title job_url posted_at categories_code categories_name description
   <chr>   <chr>     <chr>   <chr>     <chr>           <chr>           <chr>      
 1 3641190 SYSTEM A… https:… 2021-09-… 508, 191        Komputer/Tekno… Design new…
 2 3643310 IT Progr… https:… 2021-09-… 508, 191        Komputer/Tekno… RESPONSIBI…
 3 3644140 Data Ana… https:… 2021-09-… 512, 103        Sains, Aktuari… Requiremen…
 4 3643734 Data Ana… https:… 2021-09-… 503, 203        Penjualan / Pe… Requiremen…
 5 3644293 Sales Da… https:… 2021-09-… 508, 193        Komputer/Tekno… Tanggung J…
 6 3644446 Sales Da… https:… 2021-09-… 512, 103        Sains, Aktuari… Responsibi…
 7 3644353 Digital … https:… 2021-09-… 503, 203        Penjualan / Pe… Job Descri…
 8 3632503 Data Sci… https:… 2021-09-… 508, 191        Komputer/Tekno… You will b…
 9 3644514 Data Sci… https:… 2021-09-… 508, 191        Komputer/Tekno… Requiremen…
10 3642934 Speciali… https:… 2021-09-… 508, 193        Komputer/Tekno… Responsibi…
# … with 20 more rows, and 16 more variables: company_meta_id <chr>,
#   company_meta_is_private <chr>, company_meta_name <chr>,
#   employment_types_name <chr>, is_classified <chr>, is_standout <chr>,
#   locations_code <chr>, locations_name <chr>, source_country_code <chr>,
#   salary_range_period <chr>, salary_range_currency <chr>,
#   salary_range_max <chr>, salary_range_min <chr>, selling_points1 <chr>,
#   selling_points2 <chr>, selling_points3 <chr>

Session Info

sessionInfo()
R version 4.0.4 (2021-02-15)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.04.3 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=id_ID.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=id_ID.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=id_ID.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=id_ID.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] microbenchmark_1.4-7 waldo_0.3.0          janitor_2.1.0       
[4] tidyr_1.1.3          dplyr_1.0.7          jsonlite_1.7.2      
[7] httr_1.4.2          

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.7       rstudioapi_0.13  magrittr_2.0.1   tidyselect_1.1.1
 [5] R6_2.5.0         rlang_0.4.11     fansi_0.5.0      stringr_1.4.0   
 [9] tools_4.0.4      utf8_1.2.1       cli_3.0.1        DBI_1.1.1       
[13] ellipsis_0.3.2   assertthat_0.2.1 tibble_3.1.4     lifecycle_1.0.0 
[17] crayon_1.4.1     purrr_0.3.4      vctrs_0.3.8      curl_4.3.2      
[21] glue_1.4.2       snakecase_0.11.0 stringi_1.7.3    compiler_4.0.4  
[25] pillar_1.6.2     generics_0.1.0   lubridate_1.7.10 pkgconfig_2.0.3

Sekian dan terima kasih.