This vignette demonstrates how to preprocess Brazilian flight and airport data in R for spatial network analysis with RGraphSpace. It downloads 2024 flight records from Brazil’s National Civil Aviation Agency using the flightsbr (Pereira 2022) package, harmonizes IATA and ICAO airport codes, filters domestic routes, and prepares sf objects for spatial visualization.
The resulting objects contain Brazilian geographic boundaries, active airport locations, and aggregated origin–destination flight counts. These objects are saved for use in the RGraphSpace spatial-data vignette.
If you are not familiar to RGraphSpace, please see the available workflows: Get Started
This vignette retrieves data from external databases, including flight records from ANAC. Therefore, some calls may take a few minutes to run.
Computational requirement:
Hardware: RAM >= 12 GB
Software: R (>=4.5) and RStudio
# Check required packages for this vignette
if(!require("data.table", quietly = TRUE)){install.packages("data.table")}
if(!require("ggplot2", quietly = TRUE)){install.packages("ggplot2")}
if(!require("airportr", quietly = TRUE)){install.packages("airportr")}
if(!require("dplyr", quietly = TRUE)){install.packages("dplyr")}
if(!require("sf", quietly = TRUE)){install.packages("sf")}
if(!require("rnaturalearth", quietly = TRUE)){install.packages("rnaturalearth")}
if(!require("maps", quietly = TRUE)){install.packages("maps")}
if(!require("ggpubr", quietly = TRUE)){install.packages("ggpubr")}
if(!require("ggrepel", quietly = TRUE)){install.packages("ggrepel")}
if(!require("stringr", quietly = TRUE)){install.packages("stringr")}
if(!require("flightsbr", quietly = TRUE)){
remotes::install_github("ipeaGIT/flightsbr")}
if(!require("rnaturalearthhires", quietly = TRUE)){
remotes::install_github("ropensci/rnaturalearthhires")}
# Load packages
library("flightsbr")
library("data.table")
library("ggplot2")
library("airportr")
library("dplyr")
library("sf")
library("rnaturalearth")
library("rnaturalearthhires")
First, we need to load three datasets: (i) flight records, (ii) airport metadata, and (iii) geographic boundary data.
The helper function below keeps only flights for which both the origin and destination airports are present in a given airport dataset.
only_from_airports <- function(flights,airports){
for(i in 1:length(flights[[1]])){
flights$accessory[i] <- flights[[1]][i] %in% airports$codigo_oaci &&
flights[[2]][i] %in% airports$codigo_oaci
}
flights_clean <- flights %>%
filter(accessory == TRUE) %>%
select(-c("accessory"))
return(flights_clean)
}
In this vignette, we are interested in all domestic flights recorded in 2024, that is, flights for which both departure and arrival airports are located in Brazil. In this section, we first load all flights from 2024, including the international ones. The flight data are filtered in later steps.
Note: This may take some time.
year <- 2024
df_year <- flightsbr::read_flights(
date = year,
showProgress = FALSE,
select = c('id_empresa', 'nr_voo', 'dt_partida_real',
'sg_iata_origem' , 'sg_iata_destino')
)
head(df_year)
#> id_empresa nr_voo dt_partida_real sg_iata_origem sg_iata_destino
#> <char> <num> <char> <char> <char>
#> 1: 1000740 4054 2024-01-03 VCP MAO
#> 2: 1000740 4053 2024-02-01 EZE SCL
#> 3: 1000740 4053 2024-02-01 SCL BOG
#> 4: 1000740 4054 2024-01-28 VCP BOG
#> 5: 1000740 4055 2024-01-18 FLN CWB
#> 6: 1000740 4055 2024-01-18 CWB VCP
The departure and arrival airports in df_year are stored
in the sg_iata_origem and sg_iata_destino
columns, respectively. These airports are identified using the
International Air Transport Association (IATA) codes, which are unique
three-letter location codes. Airports are also identified by four-letter
International Civil Aviation Organization (ICAO) codes.
IATA codes are primarily used for passenger-facing purposes, such as baggage ticketing and flight booking. ICAO codes, however, are used for air traffic control and flight operations. After loading the data, code conversion is necessary to integrate flight and airport datasets.
airports_all <- flightsbr::read_airports(
type = 'all',
showProgress = FALSE
)
head(airports_all)
#> codigo_oaci ciad nome municipio uf longitude latitude
#> <char> <char> <char> <char> <char> <num> <num>
#> 1: SBRB AC0001 Plácido de Castro RIO BRANCO Acre -67.89806 -9.86833333
#> 2: SWPI AM0006 Parintins PARINTINS Amazonas -56.77750 -2.67361111
#> 3: SBMQ AP0001 Alberto Alcolumbre MACAPÁ Amapá -51.07222 0.05055556
#> 4: SBVC BA0005 Glauber de Andrade Rocha VITÓRIA DA CONQUISTA Bahia -40.91472 -14.90777778
#> 5: SBLE BA0006 HORÁCIO DE MATTOS LENÇÓIS Bahia -41.27694 -12.48222222
#> 6: SNVB BA0008 Valença VALENÇA Bahia -38.99250 -13.29638889
#> altitude operacao_diurna operacao_noturna portaria_de_registro
#> <num> <char> <char> <char>
#> 1: 193 VFR / IFR - CAT I VFR / IFR - CAT I
#> 2: 25 VFR VFR
#> 3: 17 VFR / IFR Não Precisão VFR / IFR Não Precisão
#> 4: 894 VFR / IFR Não Precisão VFR / IFR Não Precisão PA2019-2438
#> 5: 506 VFR VFR
#> 6: 5 VFR VFR PA2015-3090
#> link_portaria latgeopoint longeopoint type
#> <char> <char> <char> <char>
#> 1: -9.8683333 -67.898056 public
#> 2: -2.6736111 -56.7775 public
#> 3: 0.050555556 -51.072222 public
#> 4: https://pergamum.anac.gov.br/arquivos/PA2019-2438.pdf -14.907778 -40.914722 public
#> 5: -12.482222 -41.276944 public
#> 6: https://pergamum.anac.gov.br/arquivos/PA2015-3090.pdf -13.296389 -38.9925 public
Note that the first column, codigo_oaci, contains the
ICAO code for each airport. In the airports_all dataset,
IATA codes are not available. The airport metadata also include the
city, state, name, latitude, longitude, altitude, and type (private or
public) of each airport.
Although the latitude and longitude columns
of airport_all contain several NA values, we
can extract geographic information from the longeopoint and
latgeopoint columns. These latter columns are stored as
character vectors, so they must be to convert to double vectors. The
airport metadata is then converted to an sf object, in which
the geographic coordinates are stored as POINT
geometries.
airports_all$latgeopoint <- as.double(gsub(",",".",airports_all$latgeopoint))
airports_all$longeopoint <- as.double(gsub(",",".",airports_all$longeopoint))
airports_all$longitude <- airports_all$longeopoint
airports_all$latitude <- airports_all$latgeopoint
sf_air_all <- sf::st_as_sf(airports_all,
coords = c("longeopoint", "latgeopoint"),
crs = 4674)
head(sf_air_all)
#> Simple feature collection with 6 features and 13 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -67.89806 ymin: -14.90778 xmax: -38.9925 ymax: 0.05055556
#> Geodetic CRS: SIRGAS 2000
#> codigo_oaci ciad nome municipio uf longitude latitude
#> 1 SBRB AC0001 Plácido de Castro RIO BRANCO Acre -67.89806 -9.86833330
#> 2 SWPI AM0006 Parintins PARINTINS Amazonas -56.77750 -2.67361110
#> 3 SBMQ AP0001 Alberto Alcolumbre MACAPÁ Amapá -51.07222 0.05055556
#> 4 SBVC BA0005 Glauber de Andrade Rocha VITÓRIA DA CONQUISTA Bahia -40.91472 -14.90777800
#> 5 SBLE BA0006 HORÁCIO DE MATTOS LENÇÓIS Bahia -41.27694 -12.48222200
#> 6 SNVB BA0008 Valença VALENÇA Bahia -38.99250 -13.29638900
#> altitude operacao_diurna operacao_noturna portaria_de_registro
#> 1 193 VFR / IFR - CAT I VFR / IFR - CAT I
#> 2 25 VFR VFR
#> 3 17 VFR / IFR Não Precisão VFR / IFR Não Precisão
#> 4 894 VFR / IFR Não Precisão VFR / IFR Não Precisão PA2019-2438
#> 5 506 VFR VFR
#> 6 5 VFR VFR PA2015-3090
#> link_portaria type geometry
#> 1 public POINT (-67.89806 -9.868333)
#> 2 public POINT (-56.7775 -2.673611)
#> 3 public POINT (-51.07222 0.05055556)
#> 4 https://pergamum.anac.gov.br/arquivos/PA2019-2438.pdf public POINT (-40.91472 -14.90778)
#> 5 public POINT (-41.27694 -12.48222)
#> 6 https://pergamum.anac.gov.br/arquivos/PA2015-3090.pdf public POINT (-38.9925 -13.29639)
Here, we load the Brazilian map and a subset of the Southeast region of Brazil. The Southeast region comprises four states: São Paulo (SP), Minas Gerais (MG), Rio de Janeiro (RJ) and Espírito Santo (ES).
mapa_brasil <- ne_states(country = "brazil", returnclass = "sf") %>%
select(c("name","postal","latitude","longitude","geometry"))
mapa_brasil <- st_transform(mapa_brasil)
head(mapa_brasil)
#> Simple feature collection with 6 features and 4 fields
#> Geometry type: MULTIPOLYGON
#> Dimension: XY
#> Bounding box: xmin: -74.01847 ymin: -33.74228 xmax: -46.06469 ymax: 5.267225
#> Geodetic CRS: WGS 84
#> name postal latitude longitude geometry
#> 104 Rio Grande do Sul RS -29.72770 -53.6560 MULTIPOLYGON (((-57.51098 -...
#> 265 Roraima RR 1.93803 -61.3325 MULTIPOLYGON (((-60.73985 5...
#> 268 Pará PA -4.44313 -52.6491 MULTIPOLYGON (((-58.50175 1...
#> 272 Acre AC -8.92850 -70.2976 MULTIPOLYGON (((-69.57763 -...
#> 462 Amapá AP 1.41157 -51.6842 MULTIPOLYGON (((-54.77549 2...
#> 753 Mato Grosso do Sul MS -20.67560 -54.5502 MULTIPOLYGON (((-58.1588 -2...
se_states <- mapa_brasil %>% filter(postal %in% c('RJ','SP','MG','ES'))
Plot Brazilian map:
ggplot() +
geom_sf(data=mapa_brasil, fill="light grey", color="dark grey", size=.15, show.legend = FALSE) +
labs(subtitle="Brazilian States", size=8) +
theme_minimal()
Plot states from Brazil’s southeast region:
ggplot() +
geom_sf(data=se_states, fill="light grey", color="dark grey", size=.15, show.legend = FALSE) +
labs(subtitle="Southeast Region", size=8) +
theme_minimal()
Plot airports:
ggplot() +
geom_sf(data=mapa_brasil, fill="light grey", color="dark grey", size=.15, show.legend = FALSE) +
labs(subtitle="Brazilian Airports") +
geom_sf(data = sf_air_all,
aes(geometry = geometry,
color = type),
size = .5) +
theme_minimal()
First, we collect all airports present in flights data
(df_year), including both departure and arrival airports,
and store them in a data frame named IATA_to_ICAO.
vec<-sort(unique(c(df_year$sg_iata_origem,df_year$sg_iata_destino)))
vec<-tail(vec,-2)
IATA_to_ICAO <- data.frame(IATA = vec, ICAO = NA, row.names = vec)
rm(vec)
Number of unique airports in flights data:
length(IATA_to_ICAO$IATA)
#> [1] 378
Now, we assign the ICAO codes to each airport present in
IATA_to_ICAO. The for loop shown below skips
iterations thar return errors during the airport_lookup()
search.
for (i in 1:length(IATA_to_ICAO$IATA)) {
# Wrap the failing code in tryCatch
result <- tryCatch({
airportr::airport_lookup(IATA_to_ICAO$IATA[i],
input_type = "IATA", output_type = "ICAO") # Your function that might fail
}, error = function(e) {
# If error occurs, return a special value or just continue
return(NULL)
})
# Skip to the next iteration if an error occurred
if (is.null(result)) {
next
}
# Process successful result here
IATA_to_ICAO$ICAO[i] <- result
}
head(IATA_to_ICAO)
#> IATA ICAO
#> ABJ ABJ DIAP
#> ACC ACC DGAA
#> ACE ACE GCRR
#> ADD ADD HAAB
#> AEP AEP SABE
#> AEX AEX KAEX
Assigning ICAO codes to the departure and destination airports in the flights data.
# Ensure that each IATA code has only one corresponding ICAO code
stopifnot(!anyDuplicated(IATA_to_ICAO$IATA))
# Find the corresponding row in IATA_to_ICAO for each airport
origin_match <- match(
df_year$sg_iata_origem,
IATA_to_ICAO$IATA
)
destination_match <- match(
df_year$sg_iata_destino,
IATA_to_ICAO$IATA
)
# Assign the ICAO codes
df_year$sg_icao_origem <- IATA_to_ICAO$ICAO[origin_match]
df_year$sg_icao_destino <- IATA_to_ICAO$ICAO[destination_match]
rm(origin_match, destination_match)
head(df_year)
#> id_empresa nr_voo dt_partida_real sg_iata_origem sg_iata_destino sg_icao_origem sg_icao_destino
#> <char> <num> <char> <char> <char> <char> <char>
#> 1: 1000740 4054 2024-01-03 VCP MAO SBKP SBEG
#> 2: 1000740 4053 2024-02-01 EZE SCL SAEZ SCEL
#> 3: 1000740 4053 2024-02-01 SCL BOG SCEL SKBO
#> 4: 1000740 4054 2024-01-28 VCP BOG SBKP SKBO
#> 5: 1000740 4055 2024-01-18 FLN CWB SBFL SBCT
#> 6: 1000740 4055 2024-01-18 CWB VCP SBCT SBKP
Once we have converted the airport codes in the flight data, we
filter and reshape the data to remove NA values and
duplicate departure-arrivals pairs.
flights_clean <- na.omit(df_year) %>%
dplyr::select(sg_icao_origem,sg_icao_destino) %>%
add_count(across(everything()),name = "flights_count") %>%
unique()
Number of unique departures-arrivals pairs:
length(flights_clean$sg_icao_origem)
#> [1] 2712
Total number of flights after selections and code translation:
sum(flights_clean$flights_count)
#> [1] 922508
In flights_clean, each row represent a unique
departure-arrival pair from the annual flight records. The
flights_count column indicates the number of flights of
each pair.
Note 1: Flights are directional: A → B is different
than B → A.
Note 2: This dataset still contains international
flights, that is, flights with either an arrival or departure airport
located outside Brazil.
head(flights_clean)
#> sg_icao_origem sg_icao_destino flights_count
#> <char> <char> <int>
#> 1: SBKP SBEG 1818
#> 2: SAEZ SCEL 270
#> 3: SCEL SKBO 12
#> 4: SBKP SKBO 665
#> 5: SBFL SBCT 508
#> 6: SBCT SBKP 3083
Removing flights with 100 or fewer counts:
flights_cf <- flights_clean %>% filter(flights_count > 100)
Not all airports present in sf_air_all were active
during the year. Therefore, we only keep in airports that are present in
the filtered flight data.
active_airports <- unique(c(flights_cf$sg_icao_origem,
flights_cf$sg_icao_destino))
Number of active airports (domestic and international):
length(active_airports)
#> [1] 177
The flights_cf dataset still contains both domestic and
international flights. For this reason, we need to remove international
airports from active_airports. This can be done by checking
which active airport are present in sf_air_all, our
database of Brazilian airports.
active_airports <- sf_air_all %>% filter(codigo_oaci %in% active_airports)
rownames(active_airports) <- active_airports$codigo_oaci
active_airports$latitude <- st_coordinates(active_airports$geometry)[,2]
active_airports$longitude <- st_coordinates(active_airports$geometry)[,1]
Number of active airports (only domestic):
length(active_airports$codigo_oaci)
#> [1] 109
Plot active airports:
ggplot() +
geom_sf(data=mapa_brasil, fill="light grey", color="dark grey", size=.15, show.legend = FALSE) +
labs(subtitle=paste("Brazilian active airports in",year)) +
geom_sf(data = active_airports,
aes(geometry = geometry,
color = type),
size = 1) +
theme_minimal()
After airport filtering, the active_airports contains
only airports that meet two criteria: (i) they had more than 100 flights
during the year, and (ii) they are located in Brazil. Therefore, we can
use active_airports to remove international flights from
flights_cf. This task is performed by the helper function
only_from_airports().
flights_cf <- only_from_airports(flights_cf,active_airports)
colnames(flights_cf)[3] <- "weight"
Now all data from Brazilian flights recors were loaded and filtered, so we will save them for RGraphSpace vignette:
save(mapa_brasil, flights_cf, active_airports, file = "data/sf_brazilflights.RData")
tools::resaveRdaFiles(paths = "data/sf_brazilflights.RData")
Next, we create a regional subset focused on airports in Brazil’s Southeast Region, which includes some of the country’s busiest airports. The objective is to examine the relationship between the number of flights and the population of the city served by each airport. The preprocessing workflow comprises the following steps:
1) Select active airports and flight records from Brazil’s Southeast Region.
se_airports <- active_airports %>%
filter(uf %in% c("Espírito Santo","Minas Gerais","Rio de Janeiro","São Paulo"))
2) Identify the city associated with each airport.
cities <- se_airports %>% select(c(municipio))
cities <- as.data.frame(cities) %>% select(-c("geometry"))
After identifying the cities served by all active airports in Brazil’s Southeast Region, special characters must br removed and the capitalization of city names standardized. These adjustments are required to matching the city names with those in the population database.
cities <- cities |>
mutate(
name = municipio |>
iconv(to = "ASCII//TRANSLIT") |>
stringr::str_to_title() |>
stringr::str_replace_all(
"\\b(?:Do|Da|De|Dos|Das)\\b",
tolower
)
)
3) Retrieve the population of each city.
data(world.cities, package = "maps")
cities2 <- subset(world.cities, country.etc == "Brazil" &
name %in% cities$name) %>%
select(c("name","pop"))
cities <- left_join(cities,cities2,by= "name")
se_airports$pop <- cities[match(se_airports$municipio,cities$municipio),3]
rm(cities,cities2,world.cities)
se_airports$pop[is.na(se_airports$pop)] <- 0
Plot active airports in Brazil’s Southeast Region:
ggplot() +
geom_sf(data=se_states, fill="light grey", color="dark grey", size=.15, show.legend = FALSE) +
geom_sf(data = se_airports,
aes(geometry = geometry,
color = type,
size = pop)) +
labs(subtitle=paste("Brazil's Southeast Region active airports in",year),
y = "Latitude",x= "Longitude",color="Type",size="Population") +
theme_minimal()
4) Retain only flights with both origin and destination in the Southeast Region.
se_flights <- only_from_airports(flights_cf,se_airports)
colnames(se_flights)[3] <- "weight"
5) Save the preprocessed flight data for Brazil’s Southeast Region.
save(se_states, se_flights, se_airports, file = "data/sf_SoutheastFlights.RData")
tools::resaveRdaFiles(paths = "data/sf_SoutheastFlights.RData")
If you use RGraphSpace, please cite:
#> R version 4.6.1 (2026-06-24 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=Portuguese_Brazil.utf8 LC_CTYPE=Portuguese_Brazil.utf8
#> [3] LC_MONETARY=Portuguese_Brazil.utf8 LC_NUMERIC=C
#> [5] LC_TIME=Portuguese_Brazil.utf8
#>
#> time zone: America/Sao_Paulo
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] maps_3.4.3 ggpubr_1.0.0 ggrepel_0.9.8
#> [4] igraph_2.3.3 RGraphSpace_1.4.4 rnaturalearthhires_1.0.0.9000
#> [7] rnaturalearth_1.2.0 sf_1.1-1 dplyr_1.2.1
#> [10] airportr_0.1.3 ggplot2_4.0.3 data.table_1.18.4
#> [13] flightsbr_1.1.1
#>
#> loaded via a namespace (and not attached):
#> [1] tidyselect_1.2.1 vipor_0.4.7 farver_2.1.2 S7_0.2.2 fastmap_1.2.0
#> [6] janitor_2.2.1 digest_0.6.39 timechange_0.4.0 lifecycle_1.0.5 magrittr_2.0.5
#> [11] compiler_4.6.1 rlang_1.3.0 sass_0.4.10 tools_4.6.1 yaml_2.3.12
#> [16] knitr_1.51 ggsignif_0.6.4 labeling_0.4.3 classInt_0.4-11 curl_7.1.0
#> [21] xml2_1.6.0 RColorBrewer_1.1-3 abind_1.4-8 KernSmooth_2.23-26 withr_3.0.3
#> [26] purrr_1.2.2 grid_4.6.1 e1071_1.7-17 scales_1.4.0 cli_3.6.6
#> [31] rmarkdown_2.31 generics_0.1.4 remotes_2.5.0 otel_0.2.0 httr_1.4.8
#> [36] DBI_1.3.0 ggbeeswarm_0.7.3 cachem_1.1.0 proxy_0.4-29 stringr_1.6.0
#> [41] rvest_1.0.5 selectr_0.6-0 s2_1.1.11 ggrastr_1.0.2 vctrs_0.7.3
#> [46] Matrix_1.7-5 jsonlite_2.0.0 carData_3.0-6 car_3.1-5 rstatix_1.0.0
#> [51] Formula_1.2-5 archive_1.1.13 beeswarm_0.4.0 jquerylib_0.1.4 tidyr_1.3.2
#> [56] units_1.0-1 glue_1.8.1 cowplot_1.2.0 lubridate_1.9.5 stringi_1.8.7
#> [61] gtable_0.3.6 tibble_3.3.1 pillar_1.11.1 parzer_0.4.4 htmltools_0.5.9
#> [66] R6_2.6.1 wk_0.9.5 tidygraph_1.3.1 evaluate_1.0.5 lattice_0.22-9
#> [71] backports_1.5.1 broom_1.0.13 snakecase_0.11.1 bslib_0.11.0 class_7.3-23
#> [76] Rcpp_1.1.2 xfun_0.60 fs_2.1.0 pkgconfig_2.0.3