1

Im trying right now to transfer a bipartite two-mode graph to its one-mode representation. The issue is that I want to conserve node atrributes from the two-mode graph to the one-mode representations. For example a dataframe is given by:

Person EventLocation DurationEvent Peter Bar 90 Jack Bar 90 Franz Train 20 Franz Bar 90 Laurie Train 20 Jack Train 20 ...

Now I want to get persons network using the igraph function bipartite_projection() on the Person and EventLocation columns but I see no ways how to presafe additional node attributes (duration) that might be transfer to edge weights between Persons, e.g. Peter-Jack with weight 90 or Franz-Laurie with weight 20.

Edit: I´ve added the last row to be more precise. The edge "Jack-Franz" would now correspond to 90+20 = 110. But basically my issue is just related how to implement a bipartite_projection which transfers the node attribute of a bipartite igraph-network to the correspoding edge attribute in the one-mode igraph-network.

Edit 2: I just added another example. First, I create a network among persons then I want to add the budget informations to the persons edges implying how much project budget did the both attracted together (the sum of budgets only from different unique projects as weights). Then I wanted to do some further weighted centrality calculations:

person_id <- c("X","Y","Z","Q","W","E","R","X","Y")
project <- c("a","b","c","a","a","b","c","b","a")
budget <- c(100,200,300,100,100,200,300,200,100)
employ.data <- data.frame(person_id, project, budget)
View(employ.data)
sna.complete.list <- employ.data
sna.list.complete.igraph.calc <- graph.data.frame(sna.complete.list)
V(sna.list.complete.igraph.calc)$type <- V(sna.list.complete.igraph.calc)$name%in%sna.complete.list$person_id
sna.list.complete.igraph.calc.one <- try(bipartite.projection(sna.list.complete.igraph.calc, type=V(sna.list.complete.igraph.calc)$type))
sna.statistics.persons <- sna.list.complete.igraph.calc.one[[2]]
plot.igraph(sna.statistics.persons)

EDIT3: I try to reformulate my concern:

Overall Goal: Get an weighted graph (edge values between nodes weighted with some values)

Outline/Data:

  1. Data on people participating in different projects that differ in budget size

  2. Convert bipartite connection graph (People-Project) to one-mode-People-People-graph

  3. Use the budget sizes as weights for the edges between the people.

BUT for two people this value should only account for the sum of participating at unique projects. Thus, if A and B are only connected by project x of budget size 100 should result in an edge-weight of 100. If they also participate in another project with value 20, the result should be 120 etc.

I tried to transfer this information during using bipartite.projection but failed or couldn´t implement this information afterwards.

Mr.Morgan
  • 31
  • 1
  • 6
  • In your example, there is no ambiguity but what if Jack-Bar had duration 50? What would the weight be for Peter-Jack? – G5W Feb 19 '20 at 13:37
  • because it is a node attribute it is always the same for each node.... but I see my fault i will edit my question – Mr.Morgan Feb 19 '20 at 14:57
  • Node attribute, really? Don't you mean edge-attribute? – nJGL Feb 20 '20 at 12:20
  • After your second edit, it is stil very much unclear what you want. Try to formulate a clear question or perhaps divide your problem into several questions, with a minimal example of working code and a clearer idea of the the desired output. – nJGL Feb 22 '20 at 15:21
  • tried to make a clear formulation of my task. Excuse me. – Mr.Morgan Feb 23 '20 at 15:23

2 Answers2

2

The bipartite_projection() can collect only structural weights of edges, that is to say, Peter and Jack are both affiliated to both Train and Bar. To handle edge-attributes is trickier.

If you only want to perserve the node-attributes, as you write above bipartite_projection() absolutely does that for you already. Just re-project and find your attributes preserved like so:

V(unipartite_graph)$your_attributee

If you need to preserve edge-attributes when re-projecting, however, there are several questions to ask before.

  • How should multiple paths be treated when Franz-Train-Jack also has Franz-Bar_Jack?
  • What role does directionality have in the calculation

I needed the exact same thing some years back, and solved it by writing my own extended re-projection function. It is perhaps not the shortest way around this, but calculates sums of a given edge-attribute by the shortest path between each unipartite-vertex-pair in the bipartite graph and returns an graph with one edge-attribute preserved (and summarised).

Reprojection with summarised edge attributes

Note that the function does not calculate the unipartite Laurie-Peter. You could manipulate the function to your liking.

This reproduces your example data and applies my function

# Reproduce your data
df <- data.frame(Person = c("Peter","Jack","Franz","Franz","Laurie","Jack"),
                 EventLocation = c("Bar","Bar","Train","Bar","Train","Train"),
                 DurationEvent = c(90,90,20,90,20,20), stringsAsFactors = F)


## Make bipartite graph from example data
g <- graph_from_data_frame(df, directed=FALSE)
# Set vertex type using bipartite.mapping() (OBS type should be boolean for bipartite_projection())
V(g)$type <- bipartite.mapping(g)$type


## Plot Bipartite graph
E(g)$label <- E(g)$DurationEvent
V(g)$color <- ifelse(V(g)$type, "red", "yellow")
V(g)$size <- ifelse(V(g)$type, 40, 20)
plot(g, edge.label.color="gray", vertex.label.color="black")

# Function to reproject a bipartite graph to unipartite projection while
# calculating an attribute-value sum between reprojected vertecies.
unipartite_projection_attr <- function(graph_bi, attribute, projection=FALSE){

  ## Make initial unipartite projection
  graph_uni <- bipartite_projection(graph_bi, which=FALSE)

  ## List paths in bipartite-graph along which to summarise selected attribute
  el <- as_edgelist(graph_uni)
  el <- matrix(sapply(el, function(x) as.numeric(which(x == V(graph_bi)$name))), ncol=2)

  ## Function to summarise given atribute-value
  summarise_graph_attribute_along_path <- function(source, target, attribute){
    attr_value <- edge_attr(g, attribute)
    path <- get.shortest.paths(g, source, target, output="epath")$epath[[1]]
    sum(E(g)$DurationEvent[path])
  }

  attr_uni <- mapply(summarise_graph_attribute_along_path, el[,1], el[,2], attribute)
  graph_uni <- set_edge_attr(graph_uni, attribute, value=attr_uni)

  (graph_uni)
}

# Use function to make unipartite projection
gg <- unipartite_projection_attr(g, "DurationEvent", FALSE)

# Visualise
V(gg)$color <- "yellow"
E(gg)$label <- E(gg)$DurationEvent
plot(gg, edge.label.color="gray", vertex.label.color="black")

Best of luck

nJGL
  • 819
  • 5
  • 17
  • thank you very much this is really close to what i was looking for. Only one thing to comment. I turns out that I basically wanted the graph on the right but with the edge wheigts corresponding to only the sum of unique weights on the left, meaning that Jack-Franz, 90+20 = 110, Peter-Franz = 90, Jack-Peter = 90, Jack-Laurie = 20, Franz-Laurie = 20. – Mr.Morgan Feb 20 '20 at 15:42
  • In the first graph, Jack and Franz are connected 1) through Bar 90+90, or 2) through Train 20+20. The latter is the shorter path, but you can tweak the function to consider other paths than the shortest. The bipartite data you posted above does *not* allow to travel from Jack to Franz over edge-attribute values of 90+20. I'm confused. – nJGL Feb 21 '20 at 09:32
  • Sorry for confusing you. Basically, this was the reason i spoke about a node attribute. Perhaps a better example would be. There are teams of engineers working on different projects (bipartite graph and every project has some monetary budget, like before the "quality-time" they meet at the Bar or Train). After transfering the bipartite graph to the person-level i would like to do some centrality calculations but also scale this calculation with the suggested edge-weights with the corresponding aforementioned attributes (monetary budget for project, quality-time) – Mr.Morgan Feb 21 '20 at 11:25
  • It's very difficult to read between the lines of what you'd *actually* want to achieve. What would be a relevant node-attribute in your bipartite graph? Your initial question of transforming node and/or edge-attributes from bipartite projections when re-projecting to unipartite graphs, I think, have been answered. – nJGL Feb 21 '20 at 13:09
  • edit with a new description. sorry for the inconvenience – Mr.Morgan Feb 21 '20 at 15:00
  • @nJGL: I am facing the exact same problem, so let me try to rephrase. In a normal bipartite projection Persons are connected when they share at least one common Location. iGraph offers now the option "multiplicity" to account for the fact that Franz and Jack met twice, i.e., there are 2 possible paths. So the weight of Edge Franz <-> Jack would be 2 and for the other edges 1. We want the multiplicity argument not only to account for the "count" of the edges between Frank and Jack in the original bipartite graph, but also for the cumulative weight of their original edges (ie 90 Bar + 20 Train) – JNWHH Dec 14 '20 at 09:39
  • @Mr.Morgan: did you solve this? In Python it is available in networkX, but I haven't foud anything in R (-> https://stackoverflow.com/questions/27265242/weighted-bimodal-bipartite-graph-projection-conserving-original-weights?rq=1 ; btw also a great explanation of what we want to achieve) – JNWHH Dec 14 '20 at 09:46
0

Heavily borrowing from @nGL's answer, I changed the code a bit to account for all the shortest paths between 2 Persons and taking their cumulative Event Duration as their edge weight in the projected graph.

Resulting graph looks like this (eg edge weight between Jack and Franz = 110):

enter image description here

One word of caution: this assumes that the original weights are equally distributed between Persons (ie Jack and Franz meet for 90 minutes in the Bar). In other situations, Jack and Franz might visit the same Bar but for Jack the Duation is 70 and for Franz it is 110. Then one would need to think about whether taking the average is appropriate or another measure (e.g., min or max).

# Reproduce your data
df <- data.frame(Person = c("Peter","Jack","Franz","Franz","Laurie","Jack"),
                 EventLocation = c("Bar","Bar","Train","Bar","Train","Train"),
                 DurationEvent = c(90,90,20,90,20,20), stringsAsFactors = F)


## Make bipartite graph from example data
g <- graph_from_data_frame(df, directed=FALSE)
# Set vertex type using bipartite.mapping() (OBS type should be boolean for bipartite_projection())
V(g)$type <- bipartite.mapping(g)$type


## Plot Bipartite graph
E(g)$label <- E(g)$DurationEvent
V(g)$color <- ifelse(V(g)$type, "red", "yellow")
V(g)$size <- ifelse(V(g)$type, 40, 20)
plot(g, edge.label.color="gray", vertex.label.color="black")

# Function to reproject a bipartite graph to unipartite projection while
# calculating an attribute-value sum between reprojected vertecies.
unipartite_projection_attr <- function(graph_bi, attribute, projection=FALSE){
  
  ## Make initial unipartite projection
  graph_uni <- bipartite_projection(graph_bi, which=projection)
  
  ## List paths in bipartite-graph along which to summarise selected attribute
  el <- as_edgelist(graph_uni)
  el <- matrix(sapply(el, function(x) as.numeric(which(x == V(graph_bi)$name))), ncol=2)
  
  ## Function to summarise given atribute-value
  summarise_graph_attribute_along_path <- function(source, target, attribute){
    attr_value <- edge_attr(graph_bi, attribute)
    path <- lapply(get.all.shortest.paths(graph_bi, source, target)$res, function(x) E(g, path=x))
    sum(unlist(lapply(path, function (x) mean(attr_value[x]))))
  }
  
  attr_uni <- mapply(summarise_graph_attribute_along_path, el[,1], el[,2], attribute)
  graph_uni <- set_edge_attr(graph_uni, attribute, value=attr_uni)
  
  (graph_uni)
}

# Use function to make unipartite projection
gg <- unipartite_projection_attr(graph_bi = g, attribute = "DurationEvent", projection = FALSE)

# Visualise
V(gg)$color <- "yellow"
E(gg)$label <- E(gg)$DurationEvent
plot(gg, edge.label.color="gray", vertex.label.color="black")

FYI: I also changed the code at a few lines to ensure it is fully reproducable when using other attributes (e.g., replacing E(g)$DurationEvent with attr_value)

Additional word of caution: if your graph already has a weight argument, you need to set weights = NA in get.all.shortest.paths(graph_bi, from = source, to = target, weights = NA)

JNWHH
  • 721
  • 5
  • 11
  • Sorry for my late response, but this is basically a very good solution. And to be honest, you´re right according the first word of caution. This is actually an important thing. – Mr.Morgan Apr 21 '21 at 09:21