Overview

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

Before you start

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

Required Packages

# 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")

Data sources

First, we need to load three datasets: (i) flight records, (ii) airport metadata, and (iii) geographic boundary data.

Defining a helper function

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)
}

Flight records

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.

Airport metadata

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.

Formatting airport data

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)

Geographical boundaries

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() 

Preparing the data

Harmonizing IATA and ICAO airport codes

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

Cleaning flight records

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)

Selecting active airports

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() 

Filtering domestic flights

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"

Saving Preprocessed data

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")

Southeast regional network

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.
  2. Identify the city associated with each airport.
  3. Retrieve the population of each city.
  4. Retain only flights with both origin and destination in the Southeast Region.
  5. Save the preprocessed flight data for Brazil’s Southeast Region.

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")

Citation

If you use RGraphSpace, please cite:

  • Sysbiolab Team (2026). RGraphSpace: A lightweight interface between ‘igraph’ and ‘ggplot2’ graphics. R package version 1.2.4. DOI: 10.32614/CRAN.package.RGraphSpace

Session information

#> 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

References

Pereira, R. H. M. 2022. Flightsbr: Download Flight and Airport Data from Brazil. IPEA. https://doi.org/10.31219/osf.io/jdv7u.