##Data PreProcessing

Importing Library and datasets

First all required file needs to be imported using their file structure. The datasets are inspected after importing to confirm that they have been loaded correctly and that their row and column counts can be compared with the provided metadata.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.1     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.3     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(jsonlite)
## 
## Attaching package: 'jsonlite'
## 
## The following object is masked from 'package:purrr':
## 
##     flatten
#csv import
lga_stop_routes <- read.csv("lga_stops_routes.csv", check.names = FALSE)
#for population dataet, after it was downloaded from the ABS website, the table 3 was edited such the above unrequired rows were deleted and the header were edited. then it was exported as csv. 
population <- read.csv("population.csv", check.names = FALSE)
routes_city_freq <- read.csv("routes_city_freq.csv", check.names = FALSE)
lga_area<- read.csv("lga_area.csv", check.names = FALSE)
lga_2021_2024 <- read.csv("lga_2021_lga_2024.csv",check.names = FALSE)
#read json file
lga_area_json <- fromJSON("lga_area.geojson")

##Function Creation for Inspection A function is created to inspect the number of rows and columns, missing values, blank values, and duplicate rows so we dont have to repeat the code again and again.

check_data <- function(data, name) {
  
  cat("\n---Table name:", name, "---\n")
 #row n column count so we cross check the count with the metadata file manually
  cat("Rows:", nrow(data), "\n")
  cat("Columns:", ncol(data), "\n")
  
  # missing values
  cat("\nMissing values:\n")
  print(colSums(is.na(data)))
  
  #blank/empty string values
  cat("\nBlank/empty string values:\n")
  blank_count <- sapply(data, function(x) sum(x == "", na.rm = TRUE))
  print(blank_count)
  
  # Duplicate rows
  cat("\nDuplicate rows:", sum(duplicated(data)), "\n")
}

Imported datasets are checked before any filtering or cleaning is done. Then, the row and column counts are compared with metadata. Also, missing values, blank values and duplicate records are identified. The structure of the datasets is also inspected where data types are important.

check_data(lga_stop_routes, "LGA Stop Routes")
## 
## ---Table name: LGA Stop Routes ---
## Rows: 41188 
## Columns: 7 
## 
## Missing values:
##   lga_code21   lga_name21      stop_id    stop_name      vehicle route_number 
##            0            0            0            0            0            0 
##   route_name 
##            0 
## 
## Blank/empty string values:
##   lga_code21   lga_name21      stop_id    stop_name      vehicle route_number 
##            0            0            0            0            0         2953 
##   route_name 
##            6 
## 
## Duplicate rows: 4
print("---------------")
## [1] "---------------"
check_data(population, "Population")
## 
## ---Table name: Population ---
## Rows: 552 
## Columns: 29 
## 
## Missing values:
##      S/T code      S/T name      LGA code      LGA name           0–4 
##             0             0             4             0             3 
##           5–9         10–14         15–19         20–24         25–29 
##             3             3             3             3             3 
##         30–34         35–39         40–44         45–49         50–54 
##             3             3             3             3             3 
##         55–59         60–64         65–69         70–74         75–79 
##             3             3             3             3             3 
##         80–84   85 and over Total persons                             
##             3             3             3           552           552 
##                                                         
##           552           552           552           552 
## 
## Blank/empty string values:
##      S/T code      S/T name      LGA code      LGA name           0–4 
##             1             4             0             3             0 
##           5–9         10–14         15–19         20–24         25–29 
##             0             0             0             0             0 
##         30–34         35–39         40–44         45–49         50–54 
##             0             0             0             0             0 
##         55–59         60–64         65–69         70–74         75–79 
##             0             0             0             0             0 
##         80–84   85 and over Total persons                             
##             0             0             0             0             0 
##                                                         
##             0             0             0             0 
## 
## Duplicate rows: 0
print("---------------")
## [1] "---------------"
check_data(routes_city_freq, "Routes City Frequency")
## 
## ---Table name: Routes City Frequency ---
## Rows: 453 
## Columns: 6 
## 
## Missing values:
##      vehicle route_number   route_name   daily_freq weekday_freq weekend_freq 
##            0            0            0            0            0            0 
## 
## Blank/empty string values:
##      vehicle route_number   route_name   daily_freq weekday_freq weekend_freq 
##            0           39            0            0            0            0 
## 
## Duplicate rows: 0
print("---------------")
## [1] "---------------"
check_data(lga_area, "LGA Area")
## 
## ---Table name: LGA Area ---
## Rows: 37 
## Columns: 4 
## 
## Missing values:
##       lga_code21       lga_name21             sqkm cbd_euclidean_km 
##                0                0                1                1 
## 
## Blank/empty string values:
##       lga_code21       lga_name21             sqkm cbd_euclidean_km 
##                0                1                0                0 
## 
## Duplicate rows: 0
print("---------------")
## [1] "---------------"
check_data(lga_2021_2024, "LGA 2021-2024 Mapping")
## 
## ---Table name: LGA 2021-2024 Mapping ---
## Rows: 565 
## Columns: 4 
## 
## Missing values:
## LGA_CODE_2021 LGA_NAME_2021 LGA_CODE_2024 LGA_NAME_2024 
##             0             0             0             0 
## 
## Blank/empty string values:
## LGA_CODE_2021 LGA_NAME_2021 LGA_CODE_2024 LGA_NAME_2024 
##             0             0             0             0 
## 
## Duplicate rows: 0

##Checking Structure of the Datasets imported Now structure of the dataset are checked.

str(lga_stop_routes)
## 'data.frame':    41188 obs. of  7 variables:
##  $ lga_code21  : int  20110 20110 20110 20110 20110 20110 20110 20110 20110 20110 ...
##  $ lga_name21  : chr  "Alpine" "Alpine" "Alpine" "Alpine" ...
##  $ stop_id     : int  47869 18689 48807 45128 41496 22886 18687 18687 18686 45125 ...
##  $ stop_name   : chr  "Freeburgh Comunity Hall/Great Alpine Rd (Freeburgh)" "Alpine Hotel/Anderson St (Bright)" "Mudgegonga Public Hall/Myrtleford-Yackandandah Rd (Mudgegonga)" "Boyds Lane/Kiewa Valley Hwy (Kergunyah South)" ...
##  $ vehicle     : chr  "Bus" "Bus" "Bus" "Bus" ...
##  $ route_number: chr  "" "Wangaratta-Bright" "" "" ...
##  $ route_name  : chr  "Bright - Omeo Via Hotham Heights" "Bright - Wangaratta Via Bright" "Albury - Myrtleford Via Wodonga" "Mt Beauty - Albury Via Baranduda, Kergunya, Tawonga South" ...
str(population)
## 'data.frame':    552 obs. of  29 variables:
##  $ S/T code     : chr  "1" "1" "1" "1" ...
##  $ S/T name     : chr  "New South Wales" "New South Wales" "New South Wales" "New South Wales" ...
##  $ LGA code     : int  10050 10180 10250 10300 10470 10500 10550 10600 10650 10750 ...
##  $ LGA name     : chr  "Albury" "Armidale" "Ballina" "Balranald" ...
##  $ 0–4          : int  3701 1631 2353 141 2637 9898 1629 546 403 30803 ...
##  $ 5–9          : int  3921 1583 2497 120 2726 8529 1842 627 407 33794 ...
##  $ 10–14        : int  3745 1810 2775 166 2953 8013 2036 871 507 33438 ...
##  $ 15–19        : int  3666 2437 2874 124 3232 8607 2110 885 507 30086 ...
##  $ 20–24        : int  3420 2592 2122 107 3201 15211 1333 377 281 29473 ...
##  $ 25–29        : int  3931 1968 2089 131 2747 21026 1267 331 340 32006 ...
##  $ 30–34        : int  4136 1955 2330 110 2893 20394 1659 503 405 33661 ...
##  $ 35–39        : int  3791 1809 2744 144 2959 17013 1819 657 369 39464 ...
##  $ 40–44        : int  3689 1691 3073 94 2682 13304 1847 767 348 39913 ...
##  $ 45–49        : int  3330 1474 2941 122 2421 10559 1776 765 444 30660 ...
##  $ 50–54        : int  3420 1601 3130 121 2845 10453 2221 939 554 25296 ...
##  $ 55–59        : int  3272 1577 2971 165 2603 8981 2441 938 521 21005 ...
##  $ 60–64        : int  3441 1743 3458 160 2692 8843 3279 1148 675 19370 ...
##  $ 65–69        : int  3447 1620 3440 164 2559 7255 3484 1197 800 16248 ...
##  $ 70–74        : int  2959 1346 3300 159 2137 6338 3020 1090 747 13087 ...
##  $ 75–79        : int  2540 1210 2778 94 1793 5476 2495 793 715 10383 ...
##  $ 80–84        : int  1662 860 1887 56 1182 3881 1439 448 393 5744 ...
##  $ 85 and over  : int  1467 745 1841 66 1097 3989 1224 466 357 4954 ...
##  $ Total persons: int  59538 29652 48603 2244 45359 187770 36921 13348 8773 449385 ...
##  $              : logi  NA NA NA NA NA NA ...
##  $              : logi  NA NA NA NA NA NA ...
##  $              : logi  NA NA NA NA NA NA ...
##  $              : logi  NA NA NA NA NA NA ...
##  $              : logi  NA NA NA NA NA NA ...
##  $              : logi  NA NA NA NA NA NA ...
str(routes_city_freq)
## 'data.frame':    453 obs. of  6 variables:
##  $ vehicle     : chr  "Bus" "Bus" "Bus" "Bus" ...
##  $ route_number: chr  "" "" "" "" ...
##  $ route_name  : chr  "Adelaide - Melbourne Via Bendigo & Nhill" "Ballarat - Melbourne Via Melton" "City - Melbourne Airport" "Cowes or Inverloch - Melbourne Via Dandenong & Koo Wee Rup" ...
##  $ daily_freq  : int  1 1 2 3 13 2 3 1 9 6 ...
##  $ weekday_freq: int  0 1 2 3 13 2 3 1 9 5 ...
##  $ weekend_freq: int  1 0 2 3 0 2 0 1 9 6 ...
str(lga_area)
## 'data.frame':    37 obs. of  4 variables:
##  $ lga_code21      : int  20660 20910 21110 21180 21450 21610 21890 22170 22310 22670 ...
##  $ lga_name21      : chr  "Banyule" "Bayside (Vic.)" "Boroondara" "Brimbank" ...
##  $ sqkm            : num  62.5 37.2 60.2 123.4 1282.6 ...
##  $ cbd_euclidean_km: num  14 15 9 16 61 45 10 40 12 29 ...
str(lga_2021_2024)
## 'data.frame':    565 obs. of  4 variables:
##  $ LGA_CODE_2021: chr  "10050" "10180" "10250" "10300" ...
##  $ LGA_NAME_2021: chr  "Albury" "Armidale Regional" "Ballina" "Balranald" ...
##  $ LGA_CODE_2024: chr  "10050" "10180" "10250" "10300" ...
##  $ LGA_NAME_2024: chr  "Albury" "Armidale" "Ballina" "Balranald" ...

Convert LGA code to character for matching for everydataset, as it need to be joined/mapped.

population$`LGA code` <- as.character(population$`LGA code`)
lga_stop_routes$lga_code21 <-as.character(lga_stop_routes$lga_code21)
lga_area$lga_code21<- as.character(lga_area$lga_code21)
#stop_id as character as it wont be used for other mathemathical purpose
lga_stop_routes$stop_id <-
  as.character(lga_stop_routes$stop_id)

##Cleaning LGA Area Data: The LGA area dataset contains data for LGAs outside Greater Melbourne as well. Records with missing LGA codes or names are removed.The five excluded LGAs are Macedon Ranges, Mitchell, Moorabool, Murrindindi and Unincorporated Vic.The dataset is then restricted to the Greater Melbourne LGAs required for the analysis.

check_data(lga_area, "LGA Area")
## 
## ---Table name: LGA Area ---
## Rows: 37 
## Columns: 4 
## 
## Missing values:
##       lga_code21       lga_name21             sqkm cbd_euclidean_km 
##                0                0                1                1 
## 
## Blank/empty string values:
##       lga_code21       lga_name21             sqkm cbd_euclidean_km 
##                0                1                0                0 
## 
## Duplicate rows: 0
#remove records with missing LGA code or LGA name
lga_area <- lga_area %>%
 filter(
    !is.na(lga_code21) & lga_code21 != "",
    !is.na(lga_name21) & lga_name21 != ""
  )

#keep only the 31 Greater Melbourne LGAs
greater_melbourne_area <- lga_area %>%
  filter(
    !lga_name21 %in% c(
      "Macedon Ranges",
      "Mitchell",
      "Moorabool",
      "Murrindindi",
      "Unincorporated Vic"
    )
  )

#check the number of Greater Melbourne LGAs
nrow(greater_melbourne_area)
## [1] 31
# Check that LGA codes are unique
greater_melbourne_area %>%
  count(lga_code21) %>%
  filter(n > 1)
## [1] lga_code21 n         
## <0 rows> (or 0-length row.names)

##Geojson dataset: First inspection is done for the dataset, then extract LGA attributes separately and filter the rows so only Greater Melbourne data remains. Next we retain geom values in another variable, filter it then join the geom with its lga_code and lga_name.

str(lga_area_json)
## List of 4
##  $ type    : chr "FeatureCollection"
##  $ name    : chr "lga_area"
##  $ crs     :List of 2
##   ..$ type      : chr "name"
##   ..$ properties:List of 1
##   .. ..$ name: chr "urn:ogc:def:crs:OGC:1.3:CRS84"
##  $ features:'data.frame':    36 obs. of  3 variables:
##   ..$ type      : chr [1:36] "Feature" "Feature" "Feature" "Feature" ...
##   ..$ properties:'data.frame':   36 obs. of  4 variables:
##   .. ..$ lga_code21      : chr [1:36] "20660" "20910" "21110" "21180" ...
##   .. ..$ lga_name21      : chr [1:36] "Banyule" "Bayside (Vic.)" "Boroondara" "Brimbank" ...
##   .. ..$ sqkm            : num [1:36] 62.5 37.2 60.2 123.4 1282.6 ...
##   .. ..$ cbd_euclidean_km: num [1:36] 14 15 9 16 61 45 10 40 12 29 ...
##   ..$ geometry  :'data.frame':   36 obs. of  2 variables:
##   .. ..$ type       : chr [1:36] "Polygon" "Polygon" "Polygon" "Polygon" ...
##   .. ..$ coordinates:List of 36
##   .. .. ..$ : num [1, 1:1863, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1019, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:2177, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1416, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:9402, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:7437, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1857, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:659, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:475, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:2003, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1569, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:4222, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1175, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:8100, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:2112, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:4302, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:696, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1828, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:826, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:3793, 1:2] 145 144 144 144 144 ...
##   .. .. ..$ : num [1, 1:2424, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:5709, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1266, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:4273, 1:2] 144 144 144 144 144 ...
##   .. .. ..$ : num [1, 1:1607, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ :List of 23
##   .. .. .. ..$ : num [1, 1:10098, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:7, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:12, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:12, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:16, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:12, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:7, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:18, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:16, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:14, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:23, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:24, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:7, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:9, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:25, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:10, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:25, 1:2] 145 145 145 145 145 ...
##   .. .. .. ..$ : num [1, 1:16, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:1072, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:3504, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:767, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:905, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:3223, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:5147, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:4504, 1:2] 144 144 144 144 144 ...
##   .. .. ..$ : num [1, 1:1655, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:7228, 1:2] 145 145 145 145 145 ...
##   .. .. ..$ : num [1, 1:76, 1:2] 145 145 145 145 145 ...
names(lga_area_json)
## [1] "type"     "name"     "crs"      "features"
#extract LGA attributes from the GeoJSON
lga_properties <- lga_area_json$features$properties
head(lga_properties)
##   lga_code21     lga_name21       sqkm cbd_euclidean_km
## 1      20660        Banyule   62.54018               14
## 2      20910 Bayside (Vic.)   37.21109               15
## 3      21110     Boroondara   60.17757                9
## 4      21180       Brimbank  123.39906               16
## 5      21450       Cardinia 1282.56760               61
## 6      21610          Casey  409.42896               45
#check the number of GeoJSON LGA records
nrow(lga_properties)
## [1] 36
#filter the rows
lga_properties_melbourne <- lga_properties %>%
  filter(
    lga_code21 %in% greater_melbourne_area$lga_code21
  )

#check the number of Greater Melbourne rows
nrow(lga_properties_melbourne)
## [1] 31
# verify if any Greater Melbourne LGA is missing
greater_melbourne_area %>%
  anti_join(
    lga_properties_melbourne,
    by = "lga_code21"
  )
## [1] lga_code21       lga_name21       sqkm             cbd_euclidean_km
## <0 rows> (or 0-length row.names)
#Now for geometry from geojson file
#extract the geom data
lga_geometry <- lga_area_json$features$geometry

#combine LGA code, name and geometry
lga_geometry_table <- data.frame(
  lga_code21 = lga_properties$lga_code21,
  lga_name21 = lga_properties$lga_name21,
  geometry = lga_geometry
)

#keep only Greater Melbourne geometries
lga_geometry_melbourne <- lga_geometry_table %>%
  filter(
    lga_code21 %in% greater_melbourne_area$lga_code21
  )

# Check the number of geometries
nrow(lga_geometry_melbourne)
## [1] 31

##Population + LGA mapping: Metadata specifically warned about the 2024/2021 LGA difference as the population dataset uses LGA codes from the 2024 version, while the other datasets uses 2021 LGA codes. First we clean the population table by removing unwanted columns and then map it with lga_2021_2024.We also check if mapping passed to see whether any records are lost or unmatched after joining tables.and finally filter the dataframe and verify the row count.

#remove unwanted empty/unnamed columns from the population table
head(population)
##   S/T code        S/T name LGA code      LGA name  0–4  5–9 10–14 15–19 20–24
## 1        1 New South Wales    10050        Albury 3701 3921  3745  3666  3420
## 2        1 New South Wales    10180      Armidale 1631 1583  1810  2437  2592
## 3        1 New South Wales    10250       Ballina 2353 2497  2775  2874  2122
## 4        1 New South Wales    10300     Balranald  141  120   166   124   107
## 5        1 New South Wales    10470      Bathurst 2637 2726  2953  3232  3201
## 6        1 New South Wales    10500 Bayside (NSW) 9898 8529  8013  8607 15211
##   25–29 30–34 35–39 40–44 45–49 50–54 55–59 60–64 65–69 70–74 75–79 80–84
## 1  3931  4136  3791  3689  3330  3420  3272  3441  3447  2959  2540  1662
## 2  1968  1955  1809  1691  1474  1601  1577  1743  1620  1346  1210   860
## 3  2089  2330  2744  3073  2941  3130  2971  3458  3440  3300  2778  1887
## 4   131   110   144    94   122   121   165   160   164   159    94    56
## 5  2747  2893  2959  2682  2421  2845  2603  2692  2559  2137  1793  1182
## 6 21026 20394 17013 13304 10559 10453  8981  8843  7255  6338  5476  3881
##   85 and over Total persons                  
## 1        1467         59538 NA NA NA NA NA NA
## 2         745         29652 NA NA NA NA NA NA
## 3        1841         48603 NA NA NA NA NA NA
## 4          66          2244 NA NA NA NA NA NA
## 5        1097         45359 NA NA NA NA NA NA
## 6        3989        187770 NA NA NA NA NA NA
population <- population[
  !is.na(names(population)) &
  names(population) != "" &
  !startsWith(names(population), ".")
]
head(population) #29 columns to 23 columns
##   S/T code        S/T name LGA code      LGA name  0–4  5–9 10–14 15–19 20–24
## 1        1 New South Wales    10050        Albury 3701 3921  3745  3666  3420
## 2        1 New South Wales    10180      Armidale 1631 1583  1810  2437  2592
## 3        1 New South Wales    10250       Ballina 2353 2497  2775  2874  2122
## 4        1 New South Wales    10300     Balranald  141  120   166   124   107
## 5        1 New South Wales    10470      Bathurst 2637 2726  2953  3232  3201
## 6        1 New South Wales    10500 Bayside (NSW) 9898 8529  8013  8607 15211
##   25–29 30–34 35–39 40–44 45–49 50–54 55–59 60–64 65–69 70–74 75–79 80–84
## 1  3931  4136  3791  3689  3330  3420  3272  3441  3447  2959  2540  1662
## 2  1968  1955  1809  1691  1474  1601  1577  1743  1620  1346  1210   860
## 3  2089  2330  2744  3073  2941  3130  2971  3458  3440  3300  2778  1887
## 4   131   110   144    94   122   121   165   160   164   159    94    56
## 5  2747  2893  2959  2682  2421  2845  2603  2692  2559  2137  1793  1182
## 6 21026 20394 17013 13304 10559 10453  8981  8843  7255  6338  5476  3881
##   85 and over Total persons
## 1        1467         59538
## 2         745         29652
## 3        1841         48603
## 4          66          2244
## 5        1097         45359
## 6        3989        187770
#map the population 2024 LGA code to 2021 LGA code
population_mapped <- population %>%
  left_join(
    lga_2021_2024,
    by = c("LGA code" = "LGA_CODE_2024")
  )
#check population records that could not be mapped to a 2021 LGA code
population_mapped %>%
  filter(is.na(LGA_CODE_2021)) %>%
  nrow()
## [1] 7
#filter to keep only Greater Mel data
population_melbourne <- population_mapped %>%
  filter(
    LGA_CODE_2021 %in% greater_melbourne_area$lga_code21
  )
#check the no of records
nrow(population_melbourne)
## [1] 31

Verify if all lga data are there in melbourne mapped population table.

#check for LGAs missing from the population table
greater_melbourne_area %>%
  anti_join(
    population_melbourne,
    by = c("lga_code21" = "LGA_CODE_2021")
  )
## [1] lga_code21       lga_name21       sqkm             cbd_euclidean_km
## <0 rows> (or 0-length row.names)
#check for population LGAs that are not present in the Greater Melbourne LGA tbl
population_melbourne %>%
  anti_join(
    greater_melbourne_area,
    by = c("LGA_CODE_2021" = "lga_code21")
  )
##  [1] S/T code      S/T name      LGA code      LGA name      0–4          
##  [6] 5–9           10–14         15–19         20–24         25–29        
## [11] 30–34         35–39         40–44         45–49         50–54        
## [16] 55–59         60–64         65–69         70–74         75–79        
## [21] 80–84         85 and over   Total persons LGA_CODE_2021 LGA_NAME_2021
## [26] LGA_NAME_2024
## <0 rows> (or 0-length row.names)
#both returns 0 meaning it was success

##LGA_stop_route data: This dataset contains repeated stop records as a single stop can be used by multiple routes and vehicle types. Hence, duplicate records are investigated rather than directly deleting. Blank route numbers are not deleted as these records still contain valid stop information. also empty route name rows are also not deleted for the same reason and also it has route number. For the later route-frequency,assumption is made that only records with a route number can be matched to the frequency table.

#keep only Greater Melbourne data
lga_stop_routes_melbourne <- lga_stop_routes %>%
  filter(
    lga_code21 %in% greater_melbourne_area$lga_code21
  )

#check the filtered dataset
check_data(lga_stop_routes_melbourne,"LGA Stop Routes Melbourne")
## 
## ---Table name: LGA Stop Routes Melbourne ---
## Rows: 30993 
## Columns: 7 
## 
## Missing values:
##   lga_code21   lga_name21      stop_id    stop_name      vehicle route_number 
##            0            0            0            0            0            0 
##   route_name 
##            0 
## 
## Blank/empty string values:
##   lga_code21   lga_name21      stop_id    stop_name      vehicle route_number 
##            0            0            0            0            0          295 
##   route_name 
##            6 
## 
## Duplicate rows: 4
#check the structure and data types
str(lga_stop_routes_melbourne)
## 'data.frame':    30993 obs. of  7 variables:
##  $ lga_code21  : chr  "20660" "20660" "20660" "20660" ...
##  $ lga_name21  : chr  "Banyule" "Banyule" "Banyule" "Banyule" ...
##  $ stop_id     : chr  "10937" "35" "142" "74" ...
##  $ stop_name   : chr  "Adam Cres/Sherbourne Rd (Montmorency)" "Invermay Gr/Waiora Rd (Rosanna)" "Pacific Dr/Ramu Pde (Heidelberg West)" "Porter Rd/Southern Rd (Heidelberg Heights)" ...
##  $ vehicle     : chr  "Bus" "Bus" "Bus" "Bus" ...
##  $ route_number: chr  "902" "517" "550" "517" ...
##  $ route_name  : chr  "Chelsea - Airport West" "St Helena - Northland SC" "Northland SC - La Trobe University" "St Helena - Northland SC" ...
#check the no of Greater Melbourne LGAs
n_distinct(lga_stop_routes_melbourne$lga_code21)
## [1] 31
#displaying duplicate rows
lga_stop_routes_melbourne %>%
  filter(duplicated(.))
##   lga_code21    lga_name21 stop_id
## 1      25060 Moonee Valley   19270
## 2      25060 Moonee Valley   19270
## 3      25060 Moonee Valley   19276
## 4      25060 Moonee Valley   19276
##                                                     stop_name vehicle
## 1 34-Moonee Valley Civic Centre/Pascoe Vale Rd (Moonee Ponds)    Tram
## 2 34-Moonee Valley Civic Centre/Pascoe Vale Rd (Moonee Ponds)     Bus
## 3                      40-Nicholson St/Fletcher St (Essendon)    Tram
## 4                      40-Nicholson St/Fletcher St (Essendon)     Bus
##   route_number                                   route_name
## 1           59 Flinders Street Station, City - Airport West
## 2          959                          Broadmeadows - City
## 3           59 Flinders Street Station, City - Airport West
## 4          959                          Broadmeadows - City
#--The duplicacy identified 4 rows but when the data was checked for the duplicate rows, manual inspection showed that the records had different vehicle type and route no for the same stop. So, they are not considered duplicates and are retained.

#check rows with blank route name
lga_stop_routes_melbourne %>%
  filter(route_name == "")
##   lga_code21 lga_name21 stop_id                 stop_name vehicle route_number
## 1      24600  Melbourne   10920         Flagstaff Station   Train  City Circle
## 2      24600  Melbourne   10924        Parliament Station   Train  City Circle
## 3      24600  Melbourne   10922 Melbourne Central Station   Train  City Circle
## 4      24600  Melbourne   22188    Southern Cross Station   Train  City Circle
## 5      24600  Melbourne   11213   Flinders Street Station   Train  City Circle
## 6      24600  Melbourne   11212   Flinders Street Station   Train  City Circle
##   route_name
## 1           
## 2           
## 3           
## 4           
## 5           
## 6
#--The identified 6 rows with blank route_name had still valid route_numbe which provides usable route information so the rows are not deleted.

#295 rows have blank route numbers but they are not deleted as the records themselves are not invalid as they have data but we exclude them only when constructing the route list for frequency matching because a blank route number cannot be matched to a route-frequency record.

##Cleaning and Linking Route Frequency Data: This dataset contains route-level service frequency information. Only routes appearing in the Greater Melbourne dataset are required. Therefore, a list of distinct non-blank route numbers is created from the data and used to filter the frequency table.

#create a list of unique non-blank route numbers found in Greater Melbourne
melbourne_routes <- lga_stop_routes_melbourne %>%
  filter(route_number != "") %>%
  distinct(route_number)

#check the no of unique Melbourne routes
nrow(melbourne_routes)
## [1] 410
head(melbourne_routes, 10)
##    route_number
## 1           902
## 2           517
## 3           550
## 4           381
## 5           566
## 6           343
## 7           514
## 8           250
## 9           350
## 10          549
#keep only rows relevant to Melbourne routes
routes_city_freq_melbourne <- routes_city_freq %>%
  filter(
    route_number %in% melbourne_routes$route_number
  )

#check the filtered frequency table
check_data(
  routes_city_freq_melbourne,
  "Routes City Frequency Melbourne"
)
## 
## ---Table name: Routes City Frequency Melbourne ---
## Rows: 405 
## Columns: 6 
## 
## Missing values:
##      vehicle route_number   route_name   daily_freq weekday_freq weekend_freq 
##            0            0            0            0            0            0 
## 
## Blank/empty string values:
##      vehicle route_number   route_name   daily_freq weekday_freq weekend_freq 
##            0            0            0            0            0            0 
## 
## Duplicate rows: 0

The unmatched route check identifies route no. present in the Greater Melbourne stop-route but absent in the filtered frequency table. These records are not automatically assigned a frequency of 0 as metadata states that missing route frequency values should be checked.

#Melbourne routes without a matching frequency record
unmatched_routes <- melbourne_routes %>%
  anti_join(
    routes_city_freq_melbourne,
    by = "route_number"
  )

unmatched_routes
##    route_number
## 1           941
## 2           959
## 3           982
## 4           981
## 5           979
## 6           978
## 7           953
## 8           967
## 9   City Circle
## 10          951
#validate frequency uniqueness
#check if vehicle, route number and route name uniquely identify frequency records
routes_city_freq_melbourne %>%
  count(vehicle, route_number, route_name) %>%
  filter(n > 1)
## [1] vehicle      route_number route_name   n           
## <0 rows> (or 0-length row.names)
#--No duplicate rws with vehicle–route-number–route-name combinations were found. So we use these variables to link route-frequency information to the stop-route records.

Unique LGA-vehicle-route records is created with distinct lga code, name, vehicle, route no and name. Left join is used to join so that all route records are retained, inluding the rows with unavailable frequency data. This allows unmatched records to be identified.

t3_routes <- lga_stop_routes_melbourne %>%
  filter(route_number != "") %>%
  distinct(
    lga_code21,
    lga_name21,
    vehicle,
    route_number,
    route_name
  )
nrow(t3_routes) #check row count
## [1] 942
#join route frequency data to the unique route records
t3_routes_freq <- t3_routes %>%
  left_join(
    routes_city_freq_melbourne,
    by = c(
      "vehicle",
      "route_number",
      "route_name"
    )
  )

#check the number of records after joining
nrow(t3_routes_freq)
## [1] 942
# Check unmatched frequency records
sum(is.na(t3_routes_freq$daily_freq))
## [1] 25
#--There are 25 unmatched frequency records. These values are not replaced with 0 because a missing frequency record does not necessarily mean that the route has zero service.
# Display routes for which no daily frequency was matched
t3_routes_freq %>%
  filter(is.na(daily_freq)) %>%
  select(
    lga_code21,
    lga_name21,
    vehicle,
    route_number,
    route_name
  )
##    lga_code21        lga_name21 vehicle route_number
## 1       21180          Brimbank     Bus          941
## 2       21180          Brimbank     Bus          959
## 3       21610             Casey     Bus          982
## 4       21610             Casey     Bus          981
## 5       22670 Greater Dandenong     Bus          979
## 6       22670 Greater Dandenong     Bus          982
## 7       22670 Greater Dandenong     Bus          978
## 8       22670 Greater Dandenong     Bus          981
## 9       23270              Hume     Bus          953
## 10      23270              Hume     Bus          959
## 11      23430   Kingston (Vic.)     Bus          978
## 12      23430   Kingston (Vic.)     Bus          979
## 13      23670              Knox     Bus          967
## 14      24410         Maroondah     Bus          967
## 15      24600         Melbourne     Bus          959
## 16      24600         Melbourne   Train  City Circle
## 17      24600         Melbourne   Train     Overland
## 18      24970            Monash     Bus          967
## 19      24970            Monash     Bus          979
## 20      24970            Monash     Bus          978
## 21      25060     Moonee Valley     Bus          959
## 22      25250          Moreland     Bus          951
## 23      25250          Moreland     Bus          959
## 24      26980        Whitehorse     Bus          967
## 25      27450      Yarra Ranges     Bus          967
##                                 route_name
## 1  Watergardens Station - Sunshine Station
## 2                      Broadmeadows - City
## 3      Cranbourne Park - Dandenong Station
## 4      Cranbourne Park - Dandenong Station
## 5      Dandenong Station - Clayton Station
## 6      Cranbourne Park - Dandenong Station
## 7      Dandenong Station - Clayton Station
## 8      Cranbourne Park - Dandenong Station
## 9       Craigieburn - Broadmeadows Station
## 10                     Broadmeadows - City
## 11     Dandenong Station - Clayton Station
## 12     Dandenong Station - Clayton Station
## 13               Bayswater - Glen Waverley
## 14               Bayswater - Glen Waverley
## 15                     Broadmeadows - City
## 16                                        
## 17                               Melbourne
## 18               Bayswater - Glen Waverley
## 19     Dandenong Station - Clayton Station
## 20     Dandenong Station - Clayton Station
## 21                     Broadmeadows - City
## 22                          Glenroy - City
## 23                     Broadmeadows - City
## 24               Bayswater - Glen Waverley
## 25               Bayswater - Glen Waverley
# Count unmatched records by vehicle type
t3_routes_freq %>%
  filter(is.na(daily_freq)) %>%
  count(vehicle)
##   vehicle  n
## 1     Bus 23
## 2   Train  2
#-The 25 unmatched rows consists of 23 bus data and 2 train data.

Final validation of the tables This check is used to confirm that the cleaned datasets contain the required Greater Melbourne LGAs and that the key relationships between datasets can be established.

#No. of Greater Melbourne LGAs in the cleaned area table
nrow(greater_melbourne_area)
## [1] 31
#No. of Greater Melbourne LGAs in the population table
nrow(population_melbourne)
## [1] 31
#No. of Greater Melbourne LGAs in the stop-route table
n_distinct(lga_stop_routes_melbourne$lga_code21)
## [1] 31
#No. of Greater Melbourne LGAs in the GeoJSON
nrow(lga_properties_melbourne)
## [1] 31
#check unmatched population LGAs
greater_melbourne_area %>%
  anti_join(
    population_melbourne,
    by = c("lga_code21" = "LGA_CODE_2021")
  )
## [1] lga_code21       lga_name21       sqkm             cbd_euclidean_km
## <0 rows> (or 0-length row.names)
#check unmatched GeoJSON LGAs
greater_melbourne_area %>%
  anti_join(
    lga_properties_melbourne,
    by = "lga_code21"
  )
## [1] lga_code21       lga_name21       sqkm             cbd_euclidean_km
## <0 rows> (or 0-length row.names)
#check unmatched route-frequency records
melbourne_routes %>%
  anti_join(
    routes_city_freq_melbourne,
    by = "route_number"
  )
##    route_number
## 1           941
## 2           959
## 3           982
## 4           981
## 5           979
## 6           978
## 7           953
## 8           967
## 9   City Circle
## 10          951