Skip to content

Proposal: more readable edge mutations based on node attributes #206

Description

@Fuschi

Hi,

first of all, thanks for tidygraph, which I find very useful for network analysis within the tidyverse ecosystem.

I would like to propose a small helper for edge mutation in tidygraph, aimed at making common node-based edge operations more readable and closer to a dplyr-style expression.

Motivation

When mutating edges based on node attributes, the current idiom typically relies on .N() together with explicit indexing via from / to. For example:

flo_tidy %>%
  activate("edges") %>%
  mutate(
    to_medici = (.N()$name[from] == "Medici" | .N()$name[to]   == "Medici"))

This approach is flexible and expressive, but can feel somewhat redundant, since node attributes need to be accessed explicitly through .N() and indexing.

Proposal

I experimented with a minimal helper that precomputes the two edge endpoints and exposes a small set of functions inside mutate(), such as:

  • from(var) / to(var) to access node attributes at edge endpoints
  • either(expr), both(expr), one(expr), neither(expr) for symmetric nodes logic

Using this approach, the example above becomes:

flo_tidy %>%
  proposed_edge_mutate(
    to_medici = either(name == "Medici")
  )

Example using a node-level variable

flo_tidy %>%
  activate("nodes") %>%
  mutate(centrality = centrality_power()) %>%
  activate("edges") %>%
  mutate(
    mean_centrality = (.N()$centrality[from] + .N()$centrality[to]) / 2)

Equivalent proposed syntax:

flo_tidy %>%
  proposed_edge_mutate(
    mean_centrality = (from(centrality) + to(centrality)) / 2)

Results are identical (verified via all.equal()).

Scope

I would be interested in your thoughts on whether an interface like this could fit within tidygraph, or if it would be considered out of scope.

Best,
Alessandro


Minimal proof-of-concept implementation
proposed_edge_mutate <- function(graph, ...) {
  if (!inherits(graph, "tbl_graph")) {
    rlang::abort("`graph` must be a tidygraph `tbl_graph`.")
  }

  local_env <- environment()

  nodes <- graph %>%
    tidygraph::activate("nodes") %>%
    tibble::as_tibble() %>%
    dplyr::mutate(.node_id = dplyr::row_number())

  edges <- graph %>%
    tidygraph::activate("edges") %>%
    tibble::as_tibble()

  if (!all(c("from", "to") %in% names(edges))) {
    rlang::abort("Edge table must contain `from` and `to` columns.")
  }

  tbl_from <- edges %>%
    dplyr::select(from) %>%
    dplyr::left_join(nodes, by = dplyr::join_by(from == .node_id))

  tbl_to <- edges %>%
    dplyr::select(to) %>%
    dplyr::left_join(nodes, by = dplyr::join_by(to == .node_id))

  from <- function(var) {
    var_sym <- rlang::ensym(var)
    dplyr::pull(tbl_from, !!var_sym)
  }

  to <- function(var) {
    var_sym <- rlang::ensym(var)
    dplyr::pull(tbl_to, !!var_sym)
  }

  either <- function(expr) {
    expr_quo <- rlang::enquo(expr)
    rlang::eval_tidy(expr_quo, data = tbl_from) |
      rlang::eval_tidy(expr_quo, data = tbl_to)
  }

  both <- function(expr) {
    expr_quo <- rlang::enquo(expr)
    rlang::eval_tidy(expr_quo, data = tbl_from) &
      rlang::eval_tidy(expr_quo, data = tbl_to)
  }

  neither <- function(expr) {
    expr_quo <- rlang::enquo(expr)
    !(rlang::eval_tidy(expr_quo, data = tbl_from) |
        rlang::eval_tidy(expr_quo, data = tbl_to))
  }

  one <- function(expr) {
    expr_quo <- rlang::enquo(expr)
    rlang::eval_tidy(expr_quo, data = tbl_from) !=
      rlang::eval_tidy(expr_quo, data = tbl_to)
  }

  quos <- rlang::enquos(...)
  quos <- lapply(quos, function(q) {
    rlang::new_quosure(
      expr = rlang::quo_get_expr(q),
      env  = local_env
    )
  })

  graph %>%
    tidygraph::activate("edges") %>%
    dplyr::mutate(!!!quos)
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions