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 EThis 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 DThese 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 5Since 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.
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 BAn 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 :VICnodes()/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