---
title: "Real-world graph analysis: OpenFlights"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Real-world graph analysis: OpenFlights}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse  = TRUE,
  comment   = "#>",
  eval      = FALSE
)
```

This article walks through a complete, real-world graph-analysis workflow using
the [OpenFlights](https://openflights.org/data) dataset: ~7 700 airports and
~67 000 airline routes loaded into an in-memory LadybugDB graph.

The code blocks below are shown for reference (`eval = FALSE`). Copy the
article source from `system.file("doc", "openflights-analysis.Rmd",
package = "rladybugdb")` to adapt the complete workflow.

---

## 1. Download the data

OpenFlights publishes two CSV files: airports and routes. We download them to
a temporary directory at the start of each session.

```{r download}
library(rladybugdb)
suppressPackageStartupMessages({
  library(ggplot2)
  library(dplyr)
})

base_url     <- "https://raw.githubusercontent.com/jpatokal/openflights/master/data"
airports_file <- file.path(tempdir(), "airports.dat")
routes_file   <- file.path(tempdir(), "routes.dat")

download.file(paste0(base_url, "/airports.dat"), airports_file, quiet = TRUE)
download.file(paste0(base_url, "/routes.dat"),   routes_file,   quiet = TRUE)
```

## 2. Parse into data frames

The raw files are comma-separated with no header row. We assign column names
manually and filter to real airports that have a valid IATA code and coordinates.

```{r parse}
airports_raw <- read.csv(airports_file, header = FALSE, quote = '"',
                         stringsAsFactors = FALSE, na.strings = c("", "\\N"),
                         col.names = c("id","name","city","country","iata","icao",
                                       "lat","lng","alt","tz","dst","tz_name","type","source"))

routes_raw <- read.csv(routes_file, header = FALSE, quote = '"',
                       stringsAsFactors = FALSE, na.strings = c("", "\\N"),
                       col.names = c("airline","airline_id","src_iata","src_id",
                                     "dst_iata","dst_id","codeshare","stops","equipment"))

# Keep only real airports with valid coordinates and IATA codes
airports <- airports_raw |>
  filter(type == "airport", nchar(iata) == 3,
         !is.na(lat), !is.na(lng), !is.na(id)) |>
  transmute(
    id      = as.integer(id),
    name    = name,
    city    = city,
    country = country,
    iata    = iata,
    lat     = as.double(lat),
    lng     = as.double(lng),
    alt     = as.integer(ifelse(is.na(alt), 0L, as.integer(alt)))
  )

# Keep routes where both endpoints reference known airports
known_ids <- airports$id
routes <- routes_raw |>
  filter(!is.na(src_id), !is.na(dst_id),
         suppressWarnings(!is.na(as.integer(src_id))),
         suppressWarnings(!is.na(as.integer(dst_id)))) |>
  transmute(
    src_id  = as.integer(src_id),
    dst_id  = as.integer(dst_id),
    airline = ifelse(is.na(airline), "Unknown", airline),
    stops   = as.integer(ifelse(is.na(stops), 0L, stops))
  ) |>
  filter(src_id %in% known_ids, dst_id %in% known_ids, src_id != dst_id)
```

After filtering we have roughly **6 200 airports** and **63 000 routes**.

## 3. Load into LadybugDB

We model airports as nodes and routes as directed edges. The graph schema uses
one node table (`Airport`) and one relationship table (`Route`).

```{r schema}
db   <- lb_database(":memory:")
conn <- lb_connection(db)

lb_execute(conn, "
  CREATE NODE TABLE Airport (
    id      INT64,
    name    STRING,
    city    STRING,
    country STRING,
    iata    STRING,
    lat     DOUBLE,
    lng     DOUBLE,
    alt     INT64,
    PRIMARY KEY(id)
  )")

lb_execute(conn, "
  CREATE REL TABLE Route (
    FROM Airport TO Airport,
    airline STRING,
    stops   INT64
  )")
```

`lb_copy_from_df()` uses Arrow-backed loading when Arrow is installed, with an
explicitly warned CSV fallback:

```{r load}
lb_copy_from_df(conn, airports, "Airport")
lb_copy_from_df(conn, routes,   "Route")
```

## 4. Graph statistics

Simple aggregate Cypher queries confirm the data loaded correctly:

```{r stats}
lb_query(conn, "
  MATCH (a:Airport)
  RETURN count(a) AS total_airports")

lb_query(conn, "
  MATCH ()-[r:Route]->()
  RETURN count(r) AS total_routes")
```

## 5. Top airport hubs

Which airports have the most outbound routes? We aggregate with `count()`,
filter to airports with more than 10 routes, and take the top 30:

```{r top-hubs}
top_hubs <- lb_query(conn, "
  MATCH (a:Airport)-[r:Route]->()
  WITH a.iata AS iata, a.name AS name, a.country AS country,
       a.lat AS lat, a.lng AS lng, count(r) AS routes
  WHERE routes > 10
  RETURN iata, name, country, lat, lng, routes
  ORDER BY routes DESC
  LIMIT 30")
```

```{r plot-hubs, eval = FALSE}
p1 <- top_hubs |>
  mutate(label = paste0(iata, "\n", sub(" International.*", "", name))) |>
  ggplot(aes(x = reorder(label, routes), y = routes, fill = routes)) +
  geom_col(width = 0.7) +
  scale_fill_gradient(low = "#4da6ff", high = "#003380", guide = "none") +
  coord_flip() +
  labs(title = "Top 30 Airport Hubs — Outbound Routes",
       subtitle = "Source: OpenFlights  |  LadybugDB graph query",
       x = NULL, y = "Number of outbound routes") +
  theme_minimal(base_size = 11) +
  theme(plot.title = element_text(face = "bold"))
```

```{r show-hubs, echo = FALSE, eval = TRUE}
knitr::include_graphics("figures/01_top_hubs.png")
```

Frankfurt (FRA), London Heathrow (LHR), and Charles de Gaulle (CDG) dominate
the top of the list, reflecting their role as major intercontinental connecting
hubs.

## 6. Countries with the most airports

```{r by-country}
by_country <- lb_query(conn, "
  MATCH (a:Airport)
  WITH a.country AS country, count(a) AS n_airports
  RETURN country, n_airports
  ORDER BY n_airports DESC
  LIMIT 20")
```

```{r show-country, echo = FALSE, eval = TRUE}
knitr::include_graphics("figures/02_airports_by_country.png")
```

The United States leads by a wide margin, followed by Russia and Canada — a
reflection of large geographic size with many regional airports.

## 7. World map of airline routes

To produce the world map we sample 3 000 route segments and overlay the top
hub locations:

```{r route-map-query}
route_map <- lb_query(conn, "
  MATCH (src:Airport)-[r:Route]->(dst:Airport)
  WHERE src.lng IS NOT NULL AND dst.lng IS NOT NULL
  RETURN src.lat AS src_lat, src.lng AS src_lng,
         dst.lat AS dst_lat, dst.lng AS dst_lng,
         src.country AS country
  LIMIT 3000")
```

```{r show-world-map, echo = FALSE, eval = TRUE}
knitr::include_graphics("figures/03_world_map.png")
```

The density of routes across the North Atlantic and within North America and
Europe is immediately visible. The relative scarcity of routes across the South
Pacific is also striking.

## 8. Frankfurt hub network

We extract the one-hop neighbourhood of Frankfurt (FRA) — all airports reachable
by a direct Lufthansa or partner flight — and visualise it as a network graph
using `ggraph`:

```{r fra-subgraph}
hub_net <- lb_query(conn, "
  MATCH (src:Airport {iata: 'FRA'})-[r:Route]->(dst:Airport)
  RETURN src.iata AS src, dst.iata AS dst,
         dst.name AS dst_name, dst.country AS dst_country,
         dst.lat AS dst_lat, dst.lng AS dst_lng,
         r.airline AS airline
  LIMIT 80")
```

```{r fra-network-build, eval = FALSE}
library(igraph)
library(ggraph)

vertices <- data.frame(
  name    = unique(c("FRA", hub_net$dst)),
  country = c("Germany",
               hub_net$dst_country[match(unique(hub_net$dst), hub_net$dst)]),
  stringsAsFactors = FALSE
)

europe <- c("Germany","France","United Kingdom","Spain","Italy","Netherlands",
            "Switzerland","Austria","Belgium","Poland","Sweden","Norway",
            "Denmark","Finland","Portugal","Greece","Czech Republic","Romania",
            "Hungary","Ireland","Croatia","Slovakia","Bulgaria","Serbia")

edges_ig <- hub_net |> count(src, dst, name = "weight")
g <- graph_from_data_frame(edges_ig, directed = TRUE, vertices = vertices)
V(g)$region <- ifelse(V(g)$name == "FRA", "Hub",
                ifelse(V(g)$country %in% europe, "Europe", "Intercontinental"))
```

```{r show-fra-network, echo = FALSE, eval = TRUE}
knitr::include_graphics("figures/04_fra_network.png")
```

Gold = Frankfurt hub, blue = European destinations, red = intercontinental
destinations. The hub-and-spoke structure is clearly visible.

## 9. Cleanup

```{r cleanup}
lb_close(conn)
lb_close(db)
```

---

## Data model summary

| Element | LadybugDB object | Key properties |
|---------|-----------------|----------------|
| Airport node | `NODE TABLE Airport` | `id` (PK), `iata`, `name`, `lat`, `lng` |
| Route edge | `REL TABLE Route` | `FROM Airport TO Airport`, `airline`, `stops` |

## Cypher patterns used

| Pattern | Purpose |
|---------|---------|
| `MATCH (a:Airport)-[r:Route]->()` | Traverse outbound edges |
| `WITH … count(r) AS routes` | Aggregate degree per node |
| `WHERE routes > 10` | Filter after aggregation |
| `ORDER BY … DESC LIMIT 30` | Top-N ranking |
| `{iata: 'FRA'}` | Node property filter (inline) |
