Dataset retrieved from: https://api.data.gov.sg/v1/environment/psi
In this visualisation, we will be attempting to visualise the 24h PSI readings for Singapore. These readings are split by region in the official government sources, so initially the idea was to create a heatmap using the PSI values. However, after looking at the types of data provided, specifically point coordinates for each region, I concluded that it would be easier to generalise the PSI readings into point locations.
The main struggle was learning to implement API and JSON reading to retrieve real-time data for visualisation purposes, as well as exploring the different libraries required to prepare the data and create maps. For example, while researching I realised I ran into a 443 https error while using the commonly-recommended RCurl library, and had to search for an alternative, httr. Understanding the functions used to generate the maps required documentation reading as well - for example, without closely looking at the allowed variables to plot the regional dots on the map, I would not have thought to convert the PSI reading data into categorical variables, instead of leaving it as ordinal variables.
Here, we prepare the packages required for this visualisation.
packages <- c('tidyverse', 'rjson','jsonlite','httr', 'sf', 'tmap', 'lobstr' ,"shiny")
for(p in packages){
if (!require(p,character.only = T)){
install.packages(p)
}
library(p,character.only = T)
}
## Loading required package: tidyverse
## -- Attaching packages --------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.3 v purrr 0.3.4
## v tibble 3.0.6 v dplyr 1.0.4
## v tidyr 1.1.2 v stringr 1.4.0
## v readr 1.4.0 v forcats 0.5.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
## Loading required package: rjson
## Loading required package: jsonlite
##
## Attaching package: 'jsonlite'
## The following objects are masked from 'package:rjson':
##
## fromJSON, toJSON
## The following object is masked from 'package:purrr':
##
## flatten
## Loading required package: httr
## Loading required package: sf
## Linking to GEOS 3.9.0, GDAL 3.2.1, PROJ 7.2.1
## Loading required package: tmap
## Loading required package: lobstr
## Loading required package: shiny
##
## Attaching package: 'shiny'
## The following object is masked from 'package:jsonlite':
##
## validate
When calling the API, we will be extracting location (longitude/latitude) information, as well as the associated PSI levels for geographical visualisation. We use unnest() to obtain separate longitude and latitude values for each region.
url = "https://api.data.gov.sg/v1/environment/psi"
url <- URLencode(url)
psi_df <- fromJSON(content(GET(url),"text"))
## No encoding supplied: defaulting to UTF-8.
psi_level <- psi_df$items$readings$psi_twenty_four_hourly
t_psi_level<-as.data.frame(t(psi_level))
query_time <- psi_df$items$update_timestamp
region <-as_tibble(psi_df$region_metadata$name)
coordinates <-psi_df$region_metadata$label_location
coordinates <- as_tibble(coordinates) %>% unnest()
## Warning: `cols` is now required when using unnest().
## Please use `cols = c()`
locations <- merge(region, coordinates, by ="row.names", all.x=TRUE)
locations$Row.names <-NULL
head(locations)
## value latitude longitude
## 1 west 1.35735 103.70
## 2 national 0.00000 0.00
## 3 east 1.35735 103.94
## 4 central 1.35735 103.82
## 5 south 1.29587 103.82
## 6 north 1.41803 103.82
With longitudinal and latitudinal information, we need to convert them to points so that we can display it on a map. We convert the numerical psi data into categorical information as well, to be able to plot it in the map later using tm_dots() function.
locations_data <- locations %>%
st_as_sf(coords = c("longitude","latitude")) %>%
mutate( latitude= st_coordinates(.)[,1],
longitude= st_coordinates(.)[,2])
locations_sf <- st_as_sf(locations_data, coords = c("LONGITUDE", "LATITUDE"), crs = 3414)
psi_areas <- merge(x = locations_sf, y = t_psi_level,by.x="value",by=0,all=TRUE)
psi_areas$"1" = as.factor(psi_areas$"1")
psi_areas %>%
rename(
"psi" = "1",
region = value)
## Simple feature collection with 6 features and 4 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: 0 ymin: 0 xmax: 103.94 ymax: 1.41803
## CRS: NA
## region latitude longitude psi geometry
## 1 central 103.82 1.35735 49 POINT (103.82 1.35735)
## 2 east 103.94 1.35735 54 POINT (103.94 1.35735)
## 3 national 0.00 0.00000 54 POINT (0 0)
## 4 north 103.82 1.41803 46 POINT (103.82 1.41803)
## 5 south 103.82 1.29587 49 POINT (103.82 1.29587)
## 6 west 103.70 1.35735 45 POINT (103.7 1.35735)
str(psi_areas)
## Classes 'sf' and 'data.frame': 6 obs. of 5 variables:
## $ value : chr "central" "east" "national" "north" ...
## $ latitude : Named num 104 104 0 104 104 ...
## ..- attr(*, "names")= chr [1:6] "4" "3" "2" "6" ...
## $ longitude: Named num 1.36 1.36 0 1.42 1.3 ...
## ..- attr(*, "names")= chr [1:6] "4" "3" "2" "6" ...
## $ 1 : Factor w/ 4 levels "45","46","49",..: 3 4 4 2 3 1
## $ geometry :sfc_POINT of length 6; first list element: 'XY' num 103.82 1.36
## - attr(*, "sf_column")= chr "geometry"
## - attr(*, "agr")= Factor w/ 3 levels "constant","aggregate",..: NA NA NA NA
## ..- attr(*, "names")= chr [1:4] "value" "latitude" "longitude" "1"
Here, we plot the map and the relevant information is displayed in the map legend/labels. A negative scaled palette is used, so that the lighter colours indicate a a lower PSI reading/better air quality.
tmap_mode("view") +
tm_shape(psi_areas)+tm_dots(size = .3,col="1", palette = "-viridis",popup.vars = c("Region: "="value", "24h PSI level: " = "1"),title="PSI reading")
## tmap mode set to interactive viewing
## Warning: Currect projection of shape psi_areas unknown. Long-lat (WGS84) is
## assumed.
ui <- fluidPage(
tags$h2("Assignment 5"),
titlePanel("Singapore 24h PSI readings"),
tmapOutput("my_tmap"),
h5(paste("Last updated: ",substr(query_time,12,19)),align='right'))
server <- function(input,output,session){
output$my_tmap <- renderTmap({
tm <- tm_shape(psi_areas)+tm_dots(size = .3,col="1", palette = "-viridis",popup.vars = c("Region: "="value", "24h PSI level: " = "1"),title="PSI reading")
})
}
shinyApp(ui, server)
The final visualisation is a simple map split by the 5 main regions of Singapore. By clicking on each region, we are able to view the specific PSI reading for that region. The legend and all the displayed values gives us a rough estimate of Singapore’s overall air quality. The colour-coding of the different PSI levels also lets us see, at a glance, which regions have better air quality.
From this basic visualisation, we can see that: 1. The PSI levels towards the south-eastern side of Singapore is worse, i.e. the air quality there is lower. 2. Overall, Singapore at the time of reading (6th April) has a normal air quality, although it is on the high side leaning towards moderate air quality.