Repository: networkx/networkx
Pull request: Add examples to compose operation
Closes #4485

I added examples that show how to update node/edge attributes after `compose` operation.
@dschult @rossbar 

Problem statement:
Have `compose` offer an option to sum attributes.
This will need a bit of thought about how best to handle this...

Right now in `compose` where there are nodes with the same name or edges between the same nodes in both graphs and the attributes are in conflict, it takes the attributes of one of them over the other.

I can imagine many scenarios where we might want to sum the attributes if they are numeric.  I think it would be good to include an optional flag to allow summing of node or edge attributes.

Hints from issue discussion:
Wouldn’t that complicate the composition operation? It is defined as “the simple union of the node sets and edge sets” in [binary operations](https://github.com/networkx/networkx/blob/master/networkx/algorithms/operators/binary.py).
There's some choice about how to interpret the union of the two graphs both with edges 1-2 and in one case the weight is 1 and the other the weight is 2.

How does one do a simple union of this?  What does the "simple union" mean when two edges have different weights?

Right now the algorithm chooses one of them.

Is that the right result?  I think it will depend on context.  But I can definitely think of contexts where we'll want to do the sum.
While I would agree the ability to perform any binary operation on the weights of two like edges would be useful, I also think edge weight operations don’t belong in the same algorithm that takes their union.

A union of two sets not disjoint would behave like they do in this function (“losing” elements from one set or the other), so I would argue this operation behaves as expected. `Compose` produces a sort of non-commutative union but then that is something that should be handled outside of the operation in my opinion (e.g. making an isomorphic graph for preserving vertices/edges before calling).
I guess the current workaround is something like the following.
```python
GcomposeH = nx.compose(G, H)
node_data = {n: G.nodes[n].get('fun') + H.nodes[n].get('fun') for n in G.nodes & H.nodes}
nx.set_node_attributes(GcomposeH, node_data, 'fun')
edge_data = {e: G.edges[e].get('weight') + H.edges[e].get('weight') for e in G.edges & H.edges} 
nx.set_edge_attributes(GcomposeH, edge_data, 'weight')
```
It seems like it would be hard to generalize the combining of attribute dictionaries.
For example, do we sum attribute 'fun' but multiply attribute 'goodfun' and ignore another?
Perhaps an example like this would help people construct their own though.
Hello @dschult , I want to work on this issue if it is still needed.
Hello @dtekinoglu , I think what is needed here is an example in the doc_string similar to the "workaround" above that shows users ho to update their edge attributes after the `compose` operation.
Thanks!!

Commit message: Add examples to doc_string

Patch to review:
diff --git a/networkx/algorithms/operators/binary.py b/networkx/algorithms/operators/binary.py
index a3bbab36cce..2f8f4c95d4e 100644
--- a/networkx/algorithms/operators/binary.py
+++ b/networkx/algorithms/operators/binary.py
@@ -309,6 +309,30 @@ def compose(G, H):
     NodeView((0, 1, 2))
     >>> R.edges
     EdgeView([(0, 1), (0, 2), (1, 2)])
+
+    As indicated above, the attributes from H take precedent over attributes from G.
+    If you prefer another way of combining attributes, you can update them after the compose operation.
+    Consider the below graphs which have the nodes with the same labels, edge and node attributes:
+
+    >>> G = nx.Graph([('x', 'y', {'weight': 2.0}), ('x', 'z', {'weight': 1.0})])
+    >>> H = nx.Graph([('x', 'y', {'weight': 10.0}), ('y', 'z', {'weight': -1.0})])
+    >>> nx.set_node_attributes(G, {'x': "dark", 'y': "light", 'z':"neon"}, name='color')
+    >>> nx.set_node_attributes(H, {'x': "green", 'y': "orange", 'z':"yellow"}, name='color')
+    >>> GcomposeH = nx.compose(G, H)
+
+    Normally, color attribute values of nodes of GcomposeH come from H. We can workaround this as follows:
+
+    >>> node_data = {n: G.nodes[n].get('color') + " " + H.nodes[n].get('color') for n in G.nodes & H.nodes}
+    >>> nx.set_node_attributes(GcomposeH, node_data, 'color')
+    >>> GcomposeH.nodes['x']['color']
+    'dark green'
+
+    Similarly, we can update edge attributes after the compose operation in a way we prefer:
+
+    >>> edge_data = {e: G.edges[e].get('weight') * H.edges[e].get('weight') for e in G.edges & H.edges}
+    >>> nx.set_edge_attributes(GcomposeH, edge_data, 'weight')
+    >>> GcomposeH.edges[('x', 'y')]['weight']
+    20.0
     """
     return nx.compose_all([G, H])
 


Code review comments:
diff_hunk: @@ -309,6 +309,30 @@ def compose(G, H):
     NodeView((0, 1, 2))
     >>> R.edges
     EdgeView([(0, 1), (0, 2), (1, 2)])
+
+    As indicated above, the attributes from H take precedent over attributes from G.
+    If you prefer another way of combining attributes, you can update them after the compose operation.
+    Consider the below graphs which have the nodes with the same labels, edge and node attributes:
+
+    >>> G = nx.Graph([('x', 'y', {'weight': 2.0}), ('x', 'z', {'weight': 1.0})])
+    >>> H = nx.Graph([('x', 'y', {'weight': 10.0}), ('y', 'z', {'weight': -1.0})])
+    >>> nx.set_node_attributes(G, {'x': "dark", 'y': "light", 'z':"neon"}, name='color')
+    >>> nx.set_node_attributes(H, {'x': "green", 'y': "orange", 'z':"yellow"}, name='color')
path: networkx/algorithms/operators/binary.py
text: @user1:
This is entirely subjective, but I prefer using integer nodes for examples where the nodes themselves don't convey any special meaning, so I'd have a slight preference to replace `'x'`, `'y'`, and `'z'` with `0`, `1`, and `2`.

This is in no way a blocker though, so feel free to ignore!

@author:
I thought that integer nodes might make the example less readable since there are many float numbers as weights too. Nonetheless, if you prefer integer nodes I can gladly change them. @user1 

@user1:
That's a great point, and there's no objective reason to prefer one over the other - whichever way is totally fine!

@user2:
I don't think this needs to be on the same set of nodes and edges.
Can you make each graph have one node (and one edge) not in the other graph?
Then the example would show what happens when extra edges and missing edges arise.

```python 
>>> G = nx.Graph([(0, 1, {'weight': 3}, (1, 2, {'weight': 2})])
>>> H = nx.Graph([(1, 2, {'weight': 10}, (2, 3, {'weight': -1})])
```
I guess you'll need to use `.get('color', "")` and `.get('weight', 1)` so it can handle the missing cases...

@author:
I modified the example such that it also prints the updated attributes of unique node/edge. Is it clear enough? Thanks.