thomasp85 / tidygraph

A tidy API for graph manipulation
https://tidygraph.data-imaginist.com
Other
546 stars 61 forks source link

Multiple/nested `morph()` calls? #145

Open chadbot opened 3 years ago

chadbot commented 3 years ago

Hello,

I'd like to temporarily convert a directed graph to a simple, undirected graph. I attempted this with the following code:

g %>%
  morph(to_undirected) %>%
  morph(to_simple) %>%
  activate(nodes) %>%
  mutate(community = group_fast_greedy()) %>%
  unmorph() %>%
  unmorph() 

However, this doesn't seem to work. (The documentation does not indicate that it should work, but it seemed worth a try.)

The code below does the trick by using convert twice. But since this is the kind of use case morph specifically addresses, it feels more like a workaround than a proper solution.

g %>%
  convert(to_undirected) %>%
  morph(to_simple) %>%
  activate(nodes) %>%
  mutate(community = group_fast_greedy()) %>%
  unmorph()  %>%
  convert(to_directed)

Is there a better way to conduct multiple morphs? And if not, might this be considered in a future update?

gregleleu commented 2 years ago

If it's any help, morphed graph are lists (in most cases at least), so I tried running the nested morph on the list elements. However this fails when unmorphing because of the way the merge back to the original data is made (multiple/overwritten .tidygraph_node_index / .tidygraph_edge_index aren't handled)

g %>%
  morph(to_undirected) %>%
  map(~{
    .x %>%
    morph(to_simple) %>%
    activate(nodes) %>%
    mutate(community = group_fast_greedy()) %>%
    unmorph()
  }) %>%
  unmorph() 
gregleleu commented 2 years ago

Also tried the following approach in my case – nested morph to subgraph filtering on edges and then on nodes – by making my own morpher. In theory the morpher uses multiple calls to convert, which apply the filters (losing info but that's okay), wrapped in a morph/unmorph which makes sure we don't actually losing info. But the merge back creates duplicates I think because internally the index columns get messed up

to_my_morpher <- function(graph) {
  subset <- 
    graph %>% 
    activate(nodes) %>% 
    convert(to_subgraph, [nodes filter]) %>% 
    activate(edges) %>% 
    convert(to_subgraph, [edges filter]) 

  list(
    subgraph = subset
  )
}

my_graph %>% 
  activate(nodes) %>% 
  morph(to_my_morpher) %>% 
  mutate(
    group_id = group_components() 
  ) %>% 
  unmorph()
gregleleu commented 2 years ago

Get it to work by manually protecting the index columns. Not very pretty.

to_my_morpher <- function(graph) {
  subset <- 
    graph %>% 

    ## protecting the indexes
    activate(nodes) %>% 
    mutate(.tidygraph_node_index_protect = .tidygraph_node_index) %>% 
    activate(edges) %>% 
    mutate(.tidygraph_edge_index_protect = .tidygraph_edge_index) %>% 

    ## doing the morph
    convert(to_subgraph, [nodes filter], subset_by = "nodes") %>% 
    convert(to_subgraph, [edges filter], subset_by = "edges") %>% 

    ## putting the indexes back in place
    activate(nodes) %>% 
    mutate(.tidygraph_node_index = .tidygraph_node_index_protect,
           .tidygraph_node_index_protect = NULL
           ) %>% 
    activate(edges) %>% 
    mutate(.tidygraph_edge_index = .tidygraph_edge_index_protect,
           .tidygraph_edge_index_protect = NULL
           )

  list(
    subgraph = subset
  )
}
chadbot commented 2 years ago

Not exactly pretty, but you've provided some nice insight into how to think about this. Thanks for taking the time to explain, @gregleleu!

gregleleu commented 2 years ago

Sure. A more general solution could be added to the package, by merging the two approaches:

On that last point, maybe just not letting subsequent morphs overwrite indexes is enough (and would make sense) but I'm not familiar enough with the internals of the package to know if that 100% works.

One caveat: the initial graph stays as an attribute to subsequent morphs, so it could get very big.