From a20247d6d37a3ae4f52a1a5e2eb0b303a200503d Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Wed, 22 Nov 2023 14:15:57 +0100 Subject: [PATCH] Add random_walk_rank --- NAMESPACE | 4 ++ NEWS.md | 1 + R/random_walk.R | 65 +++++++++++++++++++++++++++++++ man/random_walk_rank.Rd | 49 +++++++++++++++++++++++ tests/testthat/test-random-walk.R | 21 ++++++++++ 5 files changed, 140 insertions(+) create mode 100644 R/random_walk.R create mode 100644 man/random_walk_rank.Rd create mode 100644 tests/testthat/test-random-walk.R diff --git a/NAMESPACE b/NAMESPACE index 07327b4..962d62e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -352,6 +352,7 @@ export(play_preference_asym) export(play_smallworld) export(play_traits) export(pull) +export(random_walk_rank) export(rename) export(replace_na) export(reroute) @@ -558,6 +559,8 @@ importFrom(igraph,page_rank) importFrom(igraph,permute) importFrom(igraph,power_centrality) importFrom(igraph,radius) +importFrom(igraph,random_edge_walk) +importFrom(igraph,random_walk) importFrom(igraph,reciprocity) importFrom(igraph,sample_asym_pref) importFrom(igraph,sample_bipartite) @@ -620,6 +623,7 @@ importFrom(rlang,eval_tidy) importFrom(rlang,is_bare_list) importFrom(rlang,list2) importFrom(rlang,quo) +importFrom(rlang,quo_is_null) importFrom(rlang,quo_text) importFrom(rlang,quos) importFrom(rlang,sym) diff --git a/NEWS.md b/NEWS.md index 677291f..a87dcab 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,6 +27,7 @@ calculations * Added `to_random_spanning_tree()` morpher * Added `min_order` argument to `to_components()` morpher +* Added `random_walk_rank()` to perform random walks on the graph # tidygraph 1.2.3 diff --git a/R/random_walk.R b/R/random_walk.R new file mode 100644 index 0000000..5584451 --- /dev/null +++ b/R/random_walk.R @@ -0,0 +1,65 @@ +#' Perform a random walk on the graph and return encounter rank +#' +#' A random walk is a traversal of the graph starting from a node and going a +#' number of steps by picking an edge at random (potentially weighted). +#' `random_walk()` can be called both when nodes and edges are active and will +#' adapt to return a value fitting to the currently active part. As the +#' walk order cannot be directly encoded in the graph the return value is a list +#' giving a vector of positions along the walk of each node or edge. +#' +#' @param n The number of steps to perform. If the walk gets stuck before +#' reaching this number the walk is terminated +#' @param root The node to start the walk at. If `NULL` a random node will be +#' used +#' @param mode How edges are followed in the search if the graph is directed. +#' `"out"` only follows outbound edges, `"in"` only follows inbound edges, and +#' `"all"` or `"total"` follows all edges. This is ignored for undirected +#' graphs. +#' @param weights The weights to use for edges when selecting the next step of +#' the walk. Currently only used when edges are active +#' +#' @return A list with an integer vector for each node or edge (depending on +#' what is active) each element encode the time the node/edge is encountered +#' along the walk +#' +#' @importFrom igraph random_walk random_edge_walk gorder gsize +#' @importFrom rlang enquo quo_is_null eval_tidy +#' @export +#' +#' @examples +#' graph <- create_notable("zachary") +#' +#' # Random walk returning node order +#' graph |> +#' mutate(walk_rank = random_walk_rank(200)) +#' +#' # Rank edges instead +#' graph |> +#' activate(edges) |> +#' mutate(walk_rank = random_walk_rank(200)) +#' +random_walk_rank <- function(n, root = NULL, mode = "out", weights = NULL) { + graph <- .G() + if (is.null(root)) { + root <- sample(gorder(graph), 1) + } else { + root <- as_node_ind(root, graph) + } + weights <- enquo(weights) + if (active(graph) == "nodes") { + if (!quo_is_null(weights)) { + cli::cli_warn('{.arg weights} is ignored when doing a random walk on nodes') + } + walk <- as.integer(random_walk(graph, root, n, mode)) + len_out <- gorder(graph) + } else { + weights <- eval_tidy(weights, .E()) + if (is.null(weights)) weights <- NA + walk <- as.integer(random_edge_walk(graph, root, n, weights, mode)) + len_out <- gsize(graph) + } + res <- rep(list(integer()), len_out) + ord <- split(seq_len(n), walk) + res[as.integer(names(ord))] <- ord + res[focus_ind(graph, active(graph))] +} diff --git a/man/random_walk_rank.Rd b/man/random_walk_rank.Rd new file mode 100644 index 0000000..0863461 --- /dev/null +++ b/man/random_walk_rank.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/random_walk.R +\name{random_walk_rank} +\alias{random_walk_rank} +\title{Perform a random walk on the graph and return encounter rank} +\usage{ +random_walk_rank(n, root = NULL, mode = "out", weights = NULL) +} +\arguments{ +\item{n}{The number of steps to perform. If the walk gets stuck before +reaching this number the walk is terminated} + +\item{root}{The node to start the walk at. If \code{NULL} a random node will be +used} + +\item{mode}{How edges are followed in the search if the graph is directed. +\code{"out"} only follows outbound edges, \code{"in"} only follows inbound edges, and +\code{"all"} or \code{"total"} follows all edges. This is ignored for undirected +graphs.} + +\item{weights}{The weights to use for edges when selecting the next step of +the walk. Currently only used when edges are active} +} +\value{ +A list with an integer vector for each node or edge (depending on +what is active) each element encode the time the node/edge is encountered +along the walk +} +\description{ +A random walk is a traversal of the graph starting from a node and going a +number of steps by picking an edge at random (potentially weighted). +\code{random_walk()} can be called both when nodes and edges are active and will +adapt to return a value fitting to the currently active part. As the +walk order cannot be directly encoded in the graph the return value is a list +giving a vector of positions along the walk of each node or edge. +} +\examples{ +graph <- create_notable("zachary") + +# Random walk returning node order +graph |> + mutate(walk_rank = random_walk_rank(200)) + +# Rank edges instead +graph |> + activate(edges) |> + mutate(walk_rank = random_walk_rank(200)) + +} diff --git a/tests/testthat/test-random-walk.R b/tests/testthat/test-random-walk.R new file mode 100644 index 0000000..7004df8 --- /dev/null +++ b/tests/testthat/test-random-walk.R @@ -0,0 +1,21 @@ +test_that("random_walk_rank returns correct data", { + set.seed(1) + node_walk <- create_notable('zachary') |> + mutate(walk_rank = random_walk_rank(30, 5)) |> + pull(walk_rank) + + edge_walk <- create_notable('zachary') |> + activate(edges) |> + mutate(walk_rank = random_walk_rank(30, 5)) |> + pull(walk_rank) + + expect_length(node_walk, 34) + expect_length(edge_walk, 78) + expect_type(node_walk, 'list') + expect_type(edge_walk, 'list') + expect_equal(node_walk[[5]], c(1, 12)) + expect_equal(edge_walk[[36]], 1:4) + expect_equal(edge_walk[[36]], 1:4) + expect_equal(node_walk[[2]], integer()) + expect_equal(edge_walk[[1]], integer()) +})