Skip to contents

The graphvec package extends vectors to include graph relationships between their elements, and offers tools to compute useful summaries of the graph structure for use in summarising, filtering, and otherwise manipulating the graph.

Nodes are identified by position, not by value: each element of the vector is its own node, and repeated values are distinct nodes that happen to share a label. Edges are stored as indices into the vector, so graphvec can represent multigraphs, self-loops and isolated nodes without any special handling.

Installation

You can install the released version of graphvec from CRAN with:

install.packages("graphvec")

And the development version from GitHub with:

# install.packages("remotes")
remotes::install_github("mitchelloharawild/graphvec")

Examples

Nodes

A node_vec() defines a more general graph structure where nodes can have multiple parents and children.

nodes <- node_vec(
  x = factor(c("A", "B", "C", "D", "D", "E")),
  from = c(1L, 3L),
  to = c(2L, 5L)
)

nodes
#> <node_vec[6]>
#> [1] A B C D D E

This vector describes a graph of six nodes. The two D elements at positions 4 and 5 are different nodes, since node identity comes from position rather than value, and only position 5 is connected to anything. The E element at position 6 appears in no edge at all, and remains in the graph as an isolated node.

A node_vec wraps its values directly, so it keeps behaving like whatever it wraps. This one is backed by a factor, and still has levels:

levels(nodes)
#> [1] "A" "B" "C" "D" "E"

Subsetting a node_vec selects an induced subgraph: edges that lose an endpoint are dropped, and the surviving edges are remapped to the new positions.

nodes[c(2, 3, 5)]
#> <node_vec[3]>
#> [1] B C D

These vectors are particularly useful when used in rectangular tidy data structures, and slice the same way under dplyr verbs.

tbl <- dplyr::tibble(nodes, id = 1:6)
dplyr::filter(tbl, id %in% c(2, 3, 5))
#> # A tibble: 3 × 2
#>   nodes       id
#>   <N[fct]> <int>
#> 1 B            2
#> 2 C            3
#> 3 D            5

Since node identity is positional, combining two graphs is just a disjoint union: node vectors concatenate, and the second graph’s edges shift so they keep pointing at the right nodes.

g1 <- node_vec(x = c("A", "B"), from = 1L, to = 2L)
g2 <- node_vec(x = c("X", "Y"), from = 1L, to = 2L)

c(g1, g2)
#> <node_vec[4]>
#> [1] A B X Y

Edges

The transpose of a node vector is an edge_vec(), which is instead vectorised along the edges of the graph.

e <- edge_vec(
  from = c(1L, 2L, 1L, 3L),
  to = c(2L, 3L, 3L, 1L),
  nodes = dplyr::tibble(
    id = 1:3,
    label = c("A", "B", "C")
  )
)

e
#> <edge_vec[4]>
#> [1] [1:A]->[2:B] [2:B]->[3:C] [1:A]->[3:C] [3:C]->[1:A]

Values from nodes can be obtained from an edge vector using $.

e$from$label
#> [1] "A" "B" "A" "C"
e$to$label
#> [1] "B" "C" "C" "A"

Aggregated

An agg_vec() is a third graph type built around a common shape in data analysis: a single parent that aggregates over all the other elements, e.g. a “Total” row over a set of categories. It marks the aggregate elements with <aggregated> instead of spelling out an edge table.

av <- agg_vec(
  x = c(NA, "A", "B"),
  aggregated = c(TRUE, FALSE, FALSE)
)

av
#> <agg_vec[3]>
#> [1] <aggregated> A            B

An agg_df() extends this to a table of agg_vec() columns, one row per level of a (possibly crossed) aggregation structure – e.g. Purpose and State columns where some rows total one dimension, some the other, some both.

kd <- agg_df(
  Purpose = agg_vec(
    c(NA, NA, NA, "Business", "Holiday", "Business", "Business", "Holiday", "Holiday"),
    c(TRUE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE)
  ),
  State = agg_vec(
    c(NA, "NSW", "VIC", NA, NA, "NSW", "VIC", "NSW", "VIC"),
    c(TRUE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE)
  )
)

kd
#> <agg_df[9]>
#> [1] <aggregated>:<aggregated> <aggregated>:NSW         
#> [3] <aggregated>:VIC          Business:<aggregated>    
#> [5] Holiday :<aggregated>     Business:NSW             
#> [7] Business:VIC              Holiday :NSW             
#> [9] Holiday :VIC

nodes()/edges() reorient an agg_vec() or agg_df() into the same node_vec()/edge_vec() graphs seen above: a row is a child of another row whenever the parent aggregates exactly one more column and matches on every other column’s disaggregated value.

nodes(kd)
#> <node_vec[9]>
#> [1] <aggregated>:<aggregated> <aggregated>:NSW         
#> [3] <aggregated>:VIC          Business:<aggregated>    
#> [5] Holiday :<aggregated>     Business:NSW             
#> [7] Business:VIC              Holiday :NSW             
#> [9] Holiday :VIC
edges(kd)
#> <edge_vec[12]>
#>  [1] [Business:<aggregated>]->[<aggregated>:<aggregated>]
#>  [2] [Holiday :<aggregated>]->[<aggregated>:<aggregated>]
#>  [3] [Business:NSW]->[<aggregated>:NSW]                  
#>  [4] [Business:VIC]->[<aggregated>:VIC]                  
#>  [5] [Holiday :NSW]->[<aggregated>:NSW]                  
#>  [6] [Holiday :VIC]->[<aggregated>:VIC]                  
#>  [7] [<aggregated>:NSW]->[<aggregated>:<aggregated>]     
#>  [8] [<aggregated>:VIC]->[<aggregated>:<aggregated>]     
#>  [9] [Business:NSW]->[Business:<aggregated>]             
#> [10] [Business:VIC]->[Business:<aggregated>]             
#> [11] [Holiday :NSW]->[Holiday :<aggregated>]             
#> [12] [Holiday :VIC]->[Holiday :<aggregated>]

igraph

node_vec(), edge_vec(), agg_vec() and agg_df() can all be converted directly to igraph objects for further analysis. The vertex count is taken from the nodes rather than inferred from the edges, so isolated nodes are preserved. Direct vectorised statistics and operations on these vectors are planned for this package in future releases.

igraph::as.igraph(nodes)
#> IGRAPH 6d4f3aa D--- 6 2 -- 
#> + edges from 6d4f3aa:
#> [1] 1->2 3->5
igraph::as.igraph(e)
#> IGRAPH d52a021 D--- 3 4 -- 
#> + edges from d52a021:
#> [1] 1->2 2->3 1->3 3->1
igraph::as.igraph(av)
#> IGRAPH e30398f D--- 3 2 -- 
#> + edges from e30398f:
#> [1] 2->1 3->1
igraph::as.igraph(kd)
#> IGRAPH 0ff9d5d D--- 9 12 -- 
#> + edges from 0ff9d5d:
#>  [1] 4->1 5->1 6->2 7->3 8->2 9->3 2->1 3->1 6->4 7->4 8->5 9->5