13 min read

Contek Kode: Rute TransJakarta

Mari kita plesiran ke blog para jagoan!

Entah kenapa saya tiba-tiba ingat blognya Mas Rasyid Ridha yang masih saja belum aktif diisi lagi sampai sekarang. Mungkin masih sibuk dengan kegemaran lainnya. Atau jangan-jangan pindah web???

Melalui eksplorasi sekadarnya, weezz…. langsung ketemu dengan repo transjakarta. Repo ini menyimpan kode-kode untuk eksplorasi data dalam menulis sebuah artikel bernuansa transportist. Data yang digunakan diperoleh dari Trafi.

Supaya saya jadi nggak cuma repost doang di sini, tulisan ini akan mengupas dan mengulas cara-cara Mas Rasyid mengolah data rute TransJakarta. Tentunya dari sudut pandang saya sebagai orang yang sedang mempelajari kode-kodenya Mas Rasyid.

Catatan ini saya bagi ke dalam empat bagian:

  1. Get the Data
  2. Explore the Routes
  3. Reformatting Data
  4. Visualization

Mari kita demokan di sini sambil belajar…

Get the Data

Pertama, Mas Rasyid menggunakan library jsonlite dan library terkenal tidyverse untuk collect the data. Format teks dari data yang disediakan oleh Trafi memang teks javascript json.

Saya coba sedikit-sedikit menyesuaikan atau edit kode-kodenya Mas Rasyid supaya kita bisa pelajari bersama secara lebih jelas dan deskriptif.

Berikut untuk memuat library:

library("jsonlite")
library("tidyverse")
## ── Attaching packages ───────────────────────────────────────────────────────── tidyverse 1.3.0 ──
## ✓ ggplot2 3.3.0     ✓ purrr   0.3.3
## ✓ tibble  3.0.0     ✓ dplyr   0.8.5
## ✓ tidyr   1.0.2     ✓ stringr 1.4.0
## ✓ readr   1.3.1     ✓ forcats 0.5.0
## ── Conflicts ──────────────────────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter()  masks stats::filter()
## x purrr::flatten() masks jsonlite::flatten()
## x dplyr::lag()     masks stats::lag()

Jadi, kalau yang saya pahami dari cara-cara ini, sama seperti scrapping teks berita. Hanya kali ini yang kita “garuk” adalah teks json. Yaudah cuz…

# ini tautannya bro
main_url <- "https://www.trafi.com/api/schedules/jakarta/"
route <- paste0(main_url, "all?transportType=")
json_tj <- paste0(route, "transjakarta")

Kita akan mengambil data format json lalu jadikan data.frame di R:

df_tj <- fromJSON(json_tj)[[1]] %>% 
  unnest(cols = c(schedules))

Saat percobaan untuk mengunduh dan mengonversi data text json menjadi dataframe, transformasinya adalah:

  1. teks json diunduh dari url yang sudah disimpan dalam variabel json_tj dengan menggunakan perintah fromJSON(json_tj) lalu disimpan di memori dalam bentuk list.
  2. [[1]] mengindeks list pertama (dan satu-satunya) hasil dari perintah fromJSON(json_tj), hasilnya berupa list/data.frame dengan 3 kolom.
  3. unnest(cols = c(schedules)) mengambil kolom ke-3 dari data.frame, nama kolomnya schedules yang berupa list/data.frame.

Sehingga hasilnya menjadi data.frame seperti berikut:

glimpse(df_tj) # struktur data
## Rows: 22
## Columns: 9
## $ transportNamePlural <chr> "TransJakarta", "TransJakarta", "TransJakarta", "…
## $ transportName       <chr> "Transjakarta", "Transjakarta", "Transjakarta", "…
## $ scheduleId          <chr> "idjkb_1", "idjkb_2", "idjkb_2A", "idjkb_3", "idj…
## $ transportId         <chr> "idjkb_brt", "idjkb_brt", "idjkb_brt", "idjkb_brt…
## $ validity            <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ name                <chr> "1", "2", "2A", "3", "4", "4C", "4D", "5", "5C", …
## $ longName            <chr> "Blok M - Kota", "Pulo Gadung - Harmoni", "Rawa B…
## $ icon                <chr> "transport/idjkb_brt_transjakarta_D02027_1", "tra…
## $ color               <chr> "D02027", "264798", "4EA8DE", "EDB900", "5D2E64",…
head(df_tj)    # 6 data teratas pada tabel
## # A tibble: 6 x 9
##   transportNamePl… transportName scheduleId transportId validity name  longName
##   <chr>            <chr>         <chr>      <chr>       <lgl>    <chr> <chr>   
## 1 TransJakarta     Transjakarta  idjkb_1    idjkb_brt   NA       1     Blok M …
## 2 TransJakarta     Transjakarta  idjkb_2    idjkb_brt   NA       2     Pulo Ga…
## 3 TransJakarta     Transjakarta  idjkb_2A   idjkb_brt   NA       2A    Rawa Bu…
## 4 TransJakarta     Transjakarta  idjkb_3    idjkb_brt   NA       3     Kalider…
## 5 TransJakarta     Transjakarta  idjkb_4    idjkb_brt   NA       4     Pulo Ga…
## 6 TransJakarta     Transjakarta  idjkb_4C   idjkb_brt   NA       4C    Bundara…
## # … with 2 more variables: icon <chr>, color <chr>

Selanjutnya, kita akan membuat sebuah fungsi bantu. Fungsi ini berikutnya akan kita pakai untuk mendapatkan daftar tautan baru.

Daftar ini di-generate dari salah satu kolom dalam data.frame yang sudah kita peroleh sebelumnya yaitu data.frame df_tj, nama kolomnya scheduleId.

# bikin fungsi untuk dapatkan detail tiap rute TJ
route_det <- function(schedule_id, transport) {
  paste0(main_url, "schedule?scheduleId=", schedule_id, "&transportType=", transport)
}

Tautan-tautan itu akan disimpan dalam kolom route_url. Selain kolom route_url dibuat juga dua kolom lainnya. Kolom-kolom itu adalah route_info dan load_date (tanggal pada saat data diambil).

Di dalam route_info juga ada data lagi (data di dalam tabel data) berupa teks json yang dieksrak dari daftar url dalam route_url.

Kita menggunakan fungsi map dan map2_chr dari library purrr, salah satu library dalam buntelan library tidyverse.

# jalankan ini dan tunggu sebentar, mungkin agak lama
df_tj <- df_tj %>%
  mutate(route_url = map2_chr(scheduleId, "transjakarta", route_det),
         route_info = map(route_url, fromJSON),
         load_date = Sys.Date())

Coba yuk, kita lihat hasilnya. Kolom-kolom tabel yang ditambahkan ke df_tj adalah tiga kolom berikut.

head(df_tj[10:12])
## # A tibble: 6 x 3
##   route_url                                             route_info    load_date 
##   <chr>                                                 <list>        <date>    
## 1 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06
## 2 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06
## 3 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06
## 4 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06
## 5 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06
## 6 https://www.trafi.com/api/schedules/jakarta/schedule… <named list … 2020-04-06

Lalu jika ingin menyimpan data full-nya, kita menggunakan fungsi berikut.

saveRDS(df_tj, "data/tj_detail.rds")

Explore the Routes

Berikutnya kita akan mengolah data tj_detail.rds yang tadi sudah disimpan. Tapi karena data ini sudah ada di lingkungan kerja kita dengan nama df_tj, kita akan pakai itu saja.

Pada bagian ini, kita menambah tiga library untuk membantu pekerjaan yaitu googleway, sf, dan stringr.

Untuk diperhatikan: Jika teman-teman menggunakan komputer Linux khususnya Ubuntu, sebaiknya melakukan instalasi beberapa paket dependensi untuk library sf dulu, jika belum pernah menginstal sf.

Dependensinya adalah GEOS, PROJ, dan GDAL seperti instruksi dari sini. Untuk macOS di sini. Baru setelahnya instal sf.

Jika sudah, kita lanjut… memuat library lagi.

library("googleway")
library("stringr")
library("sf")
## Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 7.0.0

Kita akan melakukan transformasi data.frame df_tj. Langkah-langkah yang dilakukan adalah:

  1. Menambah kolom baru dengan nama halte_detail dan route, sekaligus menghilangkan kolom route_info.
  2. Membuat data.frame baru dengan nama df_route berisi informasi rute TransJakarta.
# langkah 1
df_tj <- df_tj %>%
  as_tibble() %>%
  mutate(halte_detail = map(df_tj$route_info, "stops"),
         route = map(df_tj$route_info, "tracks")) %>%
  select(-route_info)

# langkah 2
df_route <- df_tj %>%
  select(schedule_id = scheduleId,
         transport_id = transportId,
         validity, name, 
         long_name = longName, 
         color, route) %>%
  unnest() %>%
  rename(route_name = name1,
         is_hidden = isHidden)

is_hidden digunakan untuk menyembunyikan satu rute yang nggak full pada suatu koridor. Aduh, saya kurang paham apa istilahnya.

Misal begini: Koridor 1 adalah Blok M - Kota, rute utamanya adalah dari halte Blok M ke halte Kota (main), atau sebaliknya (main_reverse). Selain itu, ada rute perjalanan yang tidak dari halte ujung ke ujung, misalnya rute Bundaran Senayan - Kota atau sebaliknya, juga rute Harmoni - Blok M atau sebaliknya. Ketiga rute ini semuanya ada dalam koridor 1, tetapi hanya rute Blok M - Kota yang menggunakan argumen isHidden sama dengan FALSE, karena geometri rute utama ini memang harus ditampilkan.

Semoga bisa diterima penjelasan saya. Hehehe… Untuk lebih jelas saya coba subset tabelnya seperti ini:

df_route[1:10, c(4:5, 7:8, 11)]
## # A tibble: 10 x 5
##    name  long_name     id     route_name              is_hidden
##    <chr> <chr>         <chr>  <chr>                   <lgl>    
##  1 1     Blok M - Kota 1.001  Blok M - Kota           FALSE    
##  2 1     Blok M - Kota 1.002  Kota - Blok M           FALSE    
##  3 1     Blok M - Kota 1.404  Bundaran Senayan - Kota TRUE     
##  4 1     Blok M - Kota 1.008  Kota - Bundaran Senayan TRUE     
##  5 1     Blok M - Kota 1.502  Harmoni - Kota          TRUE     
##  6 1     Blok M - Kota 1.006  Kota - Harmoni          TRUE     
##  7 1     Blok M - Kota 1.009  Blok M - Harmoni        TRUE     
##  8 1     Blok M - Kota 1.501  Harmoni - Blok M        TRUE     
##  9 1     Blok M - Kota 1.205A Blok M - Tosari         TRUE     
## 10 1     Blok M - Kota 1.205B Tosari - Blok M         TRUE

Dan, mari kita tilik sejenak struktur tabel data.frame baru df_route, hm…

glimpse(df_route)
## Rows: 222
## Columns: 12
## $ schedule_id  <chr> "idjkb_1", "idjkb_1", "idjkb_1", "idjkb_1", "idjkb_1", "…
## $ transport_id <chr> "idjkb_brt", "idjkb_brt", "idjkb_brt", "idjkb_brt", "idj…
## $ validity     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
## $ name         <chr> "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "…
## $ long_name    <chr> "Blok M - Kota", "Blok M - Kota", "Blok M - Kota", "Blok…
## $ color        <chr> "D02027", "D02027", "D02027", "D02027", "D02027", "D0202…
## $ id           <chr> "1.001", "1.002", "1.404", "1.008", "1.502", "1.006", "1…
## $ route_name   <chr> "Blok M - Kota", "Kota - Blok M", "Bundaran Senayan - Ko…
## $ direction    <int> 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 1, 1, 1, 2, 2, 1,…
## $ stops        <list> [<data.frame[19 x 2]>, <data.frame[19 x 2]>, <data.fram…
## $ is_hidden    <lgl> FALSE, FALSE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, …
## $ shape        <chr> "vkbe@kwzjS?kAMSYImB`@uM?YLMTM\\IzTKLk@LqC?uMEc]FaBImBe@…

Kolom shape (paling akhir) kelihatannya agak aneh ya?

Itu karena data ini merupakan data geometri yang di-encode menjadi baris karakter melalui Google’s polyline algorithm yang digunakan Trafi.

Kita bisa konversi ke format data simple features. Kita pakai bantuan library googleway dan sf.

# fungsi untuk konversi tipe geometri data GIS-nya
convert_shape <- function(x, y) {
  x %>%
    st_as_sf(coords = c("lon", "lat")) %>%
    group_by(gr = y) %>%
    summarise(do_union = FALSE) %>%
    st_cast("LINESTRING") %>%
    ungroup() %>%
    select(geometry)
}

# decode google's polyline
df_route <- df_route %>%
  mutate(shape_decode = map(shape, decode_pl)) %>%
  unnest(shape_decode)

# bentuk geometri
df_route <- df_route %>%
  st_as_sf(coords = c("lon", "lat")) %>%
  group_by_at(vars(-geometry, -stops)) %>%
  summarise(do_union = FALSE) %>%
  st_cast("LINESTRING")

Sampai di sini, kita sudah mendapatkan bentuk geometri rute yang bisa ditampilkan jadi gambar. Coba kita tes?

ggplot() +
  geom_sf(data = df_route %>%
            filter(is_hidden == FALSE,
                   direction == 1),
          aes(color = route_name)) +
  guides(color = FALSE)

Ini adalah rute yang beroperasi pada tanggal tulisan ini dibuat (6 April 2020). Sepertinya lebih sedikit dari yang seharusnya. Bahkan kalau dibandingkan dengan rute pada tanggal 25 Juni 2018 di blog Mas Rasyid (gambar), rute hari ini tetap lebih sedikit.

Contoh lain rute yang dibuat pada 7 November 2019, sangat banyak.

Mungkin rute ini lebih sedikit karena pembatasa operasional BRT Transjakarta khususnya selama penyebaran pandemi Covid-19. Sesuai beritanya.

Reformatting Data

Untuk finalisasi Mas Rasyid membuat data ini jadi lebih rapi.

Pertama, menamai ulang kolom-kolomnya, lalu mengurutkannya, dan didefinisikan ke transjakarta_route.

transjakarta_route <- df_route %>%
  select(transport_id,
         schedule_id,
         corridor_id = name,
         corridor_name = long_name,
         corridor_color = color,
         route_id = id,
         route_name,
         direction,
         validity,
         is_hidden)

Kedua, menambahkan sistem koordinat referensi (CRS).

transjakarta_route <- transjakarta_route %>%
  as_tibble() %>%
  st_as_sf()
st_crs(transjakarta_route) <- 4326
transjakarta_route
## Simple feature collection with 222 features and 10 fields
## geometry type:  LINESTRING
## dimension:      XY
## bbox:           xmin: 106.7054 ymin: -6.3102 xmax: 106.9534 ymax: -6.10947
## geographic CRS: WGS 84
## # A tibble: 222 x 11
##    transport_id schedule_id corridor_id corridor_name corridor_color route_id
##    <chr>        <chr>       <chr>       <chr>         <chr>          <chr>   
##  1 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.001   
##  2 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.002   
##  3 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.006   
##  4 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.008   
##  5 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.009   
##  6 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.012   
##  7 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.012B  
##  8 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.014   
##  9 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.205A  
## 10 idjkb_brt    idjkb_1     1           Blok M - Kota D02027         1.205B  
## # … with 212 more rows, and 5 more variables: route_name <chr>,
## #   direction <int>, validity <lgl>, is_hidden <lgl>, geometry <LINESTRING [°]>

Data rute TJ ini juga dilengkapi dengan keterangan arah bolak-balik, dan apakah termasuk rute utamanya atau bukan.

transjakarta_route <- transjakarta_route %>%
  group_by(corridor_id, direction) %>%
  mutate(n = n()) %>%
  ungroup() %>%
  mutate(is_main = ifelse(corridor_name == route_name, TRUE, FALSE),
         is_main_reverse = ifelse(corridor_name ==
                                    paste0(str_extract(route_name, "(?<=- ).*$"),
                                           " - ",
                                           str_extract(route_name, "^.*(?= -)")),
                                  TRUE, FALSE)) %>%
  mutate(is_main = ifelse(n == 1 & direction == 1, TRUE, is_main),
         is_main_reverse = ifelse(n == 1 & direction == 2, TRUE, is_main_reverse))
transjakarta_route[,c(3, 7:8, 12:14)]
## Simple feature collection with 222 features and 6 fields
## geometry type:  LINESTRING
## dimension:      XY
## bbox:           xmin: 106.7054 ymin: -6.3102 xmax: 106.9534 ymax: -6.10947
## geographic CRS: WGS 84
## # A tibble: 222 x 7
##    corridor_id route_name direction     n is_main is_main_reverse
##    <chr>       <chr>          <int> <int> <lgl>   <lgl>          
##  1 1           Blok M - …         1    13 TRUE    FALSE          
##  2 1           Kota - Bl…         2    12 FALSE   TRUE           
##  3 1           Kota - Ha…         2    12 FALSE   FALSE          
##  4 1           Kota - Bu…         2    12 FALSE   FALSE          
##  5 1           Blok M - …         1    13 FALSE   FALSE          
##  6 1           Kota - Mo…         2    12 FALSE   FALSE          
##  7 1           Monas - K…         1    13 FALSE   FALSE          
##  8 1           Kota - Du…         2    12 FALSE   FALSE          
##  9 1           Blok M - …         1    13 FALSE   FALSE          
## 10 1           Tosari - …         2    12 FALSE   FALSE          
## # … with 212 more rows, and 1 more variable: geometry <LINESTRING [°]>

Koridor utama yang beroperasi pada hari data ini diambil:

(ua <- filter(transjakarta_route, is_main == TRUE | is_main_reverse == TRUE)$corridor_id %>% unique())
##  [1] "1"   "11"  "12"  "2"   "2A"  "3"   "4"   "4C"  "4D"  "5"   "5C"  "5D" 
## [13] "6"   "7"   "8"   "8A"  "9"   "13"  "13A"

Selain itu, koridor lainnya yang beroperasi adalah:

(ua_neg <- setdiff(unique(transjakarta_route$corridor_id), ua))
## [1] "10"  "6A"  "13B"

Jumlah rute yang beroperasi tapi bukan dari ujung ke ujung, atau tidak penuh satu koridor (is_main == FALSE):

n_distinct(transjakarta_route$corridor_id) - n_distinct(ua) 
## [1] 3
# cek lagi dengan cara lain
transjakarta_route %>%
  filter(corridor_id %in% ua_neg, is_hidden == FALSE) %>%
  .$corridor_id %>%
  n_distinct()
## [1] 3

Terakhir, mendefinisikan ulang rute-rute yang tidak penuh satu koridor tadi (is_main == FALSE) tetapi tetap ditampilkan (is_hidden == FALSE) menjadi bernilai is_main == TRUE (nilai direction == 1) atau is_main_reverse == TRUE (nilai direction == 2).

transjakarta_route <- transjakarta_route %>%
  mutate(is_main = ifelse(corridor_id %in% ua_neg &
                            is_hidden == FALSE &
                            direction == 1, TRUE, is_main),
         is_main_reverse = ifelse(corridor_id %in% ua_neg &
                                    is_hidden == FALSE &
                                    direction == 2, TRUE, is_main_reverse)) %>%
  select(-n, -is_hidden)
glimpse(transjakarta_route)
## Rows: 222
## Columns: 12
## $ transport_id    <chr> "idjkb_brt", "idjkb_brt", "idjkb_brt", "idjkb_brt", "…
## $ schedule_id     <chr> "idjkb_1", "idjkb_1", "idjkb_1", "idjkb_1", "idjkb_1"…
## $ corridor_id     <chr> "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1"…
## $ corridor_name   <chr> "Blok M - Kota", "Blok M - Kota", "Blok M - Kota", "B…
## $ corridor_color  <chr> "D02027", "D02027", "D02027", "D02027", "D02027", "D0…
## $ route_id        <chr> "1.001", "1.002", "1.006", "1.008", "1.009", "1.012",…
## $ route_name      <chr> "Blok M - Kota", "Kota - Blok M", "Kota - Harmoni", "…
## $ direction       <int> 1, 2, 2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 2, 1,…
## $ validity        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ geometry        <LINESTRING [°]> LINESTRING (106.802 -6.2433..., LINESTRING…
## $ is_main         <lgl> TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE…
## $ is_main_reverse <lgl> FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE…

Kalau mau disimpan datanya, mari simpan…

saveRDS(transjakarta_route, "data/tj_route.rds")

Visualization

tj <- transjakarta_route %>% 
  filter(direction == 1, is_main == TRUE)

tj_col <- paste0("#", tj$corridor_color)
names(tj_col) <- tj$corridor_id

ggplot() +
  geom_sf(data = tj, aes(color = corridor_id)) +
  coord_sf(datum = NA) +
  theme_minimal() +
  guides(color = FALSE) +
  scale_color_manual(values = tj_col) +
  labs(title = "Rute Pelayanan TransJakarta",
       subtitle = "Di tengah pandemi Covid-19",
       caption = "sumber: trafi.com\ntanggal: 6 April 2020")

Untuk eksplorasi rute terpanjang dan terpendek dari pelayanan BRT TransJakarta, teman-teman bisa baca tulisan Mas Rasyid tentang eksplorasi rute TransJakarta menggunakan library sf di blognya.

Sekian, terima kasih.