Repository: networkx/networkx

Issue:
Question: MultiGraph with to_dict_of_dicts with edge data
I was reviewing the tests for `nx.convert.to_dict_of_dicts` which led to a question about what the expected behavior of `nx.to_dict_of_dicts(G)` is when G is a MultiGraph and `edge_data` is not None.

Things seem to make sense when `edge_data` is None:

```python
>>> G = nx.MultiGraph()
>>> G.add_edge(0, 1, key='a')
'a'
>>> G.add_edge(0, 1, key='b')
'b'
>>> nx.to_dict_of_dicts(G, edge_data=None)
{0: {1: {'a': {}, 'b': {}}}, 1: {0: {'a': {}, 'b': {}}}}
```

In the above example, if I call `nx.to_dict_of_dicts(G, edge_data={'weight': 1})`, my naive expectation was that i would get

```python
{0: {1: {'a': {'weight': 1}, 'b': {'weight': 1}}}, 1: {0: {'a': {'weight': 1}, 'b': {'weight': 1}}}}
```

Instead, you get:

```python
>>> nx.to_dict_of_dicts(G, edge_data={'weight': 1})
{0: {1: {'weight': 1}}, 1: {0: {'weight': 1}}}
```
which seems to clobber the multiple edges. [The code](https://github.com/networkx/networkx/blob/9aee8ef9739913ebd1e34b1be97cd91a7d6cc520/networkx/convert.py#L240-L277) doesn't have any special cases for MultiGraphs, and I couldn't tell from there and/or docstring whether this behavior is expected.

I expect I'm missing something, so I thought I'd ask! Adding examples to the docstring would also be a nice improvement to show the expected behavior.


Discussion:
Hi @rossbar. The [original code](https://github.com/networkx/networkx/blob/9aee8ef9739913ebd1e34b1be97cd91a7d6cc520/networkx/convert.py#L263) in convert.py directly replace **edge_data** with original edge attributes. When the input is multiGraph, it is natural that we lose the multi-edge information.

``` python
# original version
# edge_data is not None
for u, nbrdict in G.adjacency():
    dod[u] = dod.fromkeys(nbrdict, edge_data)
```

I think the naive method to solve this issue is to consider Single-Graph and Multi-Graphs separately. We should keep different edge keys in MultiGraphs, like the modifications below.  Hope to get any feedback or good ideas. Thank you.

``` python
# revised version draft
# edge_data is not None
for u, nbrdict in G.adjacency():
    if 'Multi' in nx.info(G).split("\n", 2)[1]: # multi-graphs
        udict = {}
        for nbr, edgedict in nbrdict.items():
            udict[nbr] = dict.fromkeys(edgedict, edge_data)
        dod[u] = udict
    else: # single graph
        dod[u] = dod.fromkeys(nbrdict, edge_data)
```
First of all, the docs are incorrect when they say that ```edge_data``` should be: ```list, optional```.  It can actually be anything. It is the value in the dict-of-dict.  The text talks about making it the value ```1```, so it clearly is not intended to be a list.  When ```edge_data is None``` we put the edge data dictionary into the dict_of_dict structure. That makes this a dict-of-dict-of-dict structure. But the edge_data input allows any **single** value in the dict-of-dict output.  I am NOT saying this is what it should be. Only what it is.   Trying to include a dict or list there is misleading because it will be the SAME DICT FOR EVERY EDGE!  Changing ```dod[u][v]['weight']``` will change the weight for ALL EDGES simultaneously because there is only one dict.

I suspect historically this was created for symmetry with ```from_dict_of_dict```.  And since it wasn't clear what people might want -- except to make a value of ```1```, the interface left a way to do that (and maybe more).  But I suspect that ```to_dict_of_dict``` has not been used very often. @berlincho have you used this function?

This is an artifact that shows our initial neglect of multigraphs and edge attributes. I think v3.0 and a reconsideration of the matrix interface might give us a chance to enrich the treatment of attributes. For example, requiring ```edge_data``` to be fa function of u,v here could be helpful. But would make the code more tricky...  Perhaps the interface from #4217 could be used for this ```to_dict_of_dict``` too.

We could also remove the function -- or at least the ```edge_data``` optional argument. We've never received any comlaints about it despite it being incorrectly documented and not useful the way it is documented (you'd have the same list for every edge in the dod).
After more thought: `to_dict_of_dict` should return a simple dict-of-dict representation of the graph/multigraph.
edge_data should be a singleton (usually `True` or `1`) to indicate an edge.  The default can remain that we
create a dict-of-dict-of-dict with edge data in the inner dict. But the docs should change.

Let's change the doc_string of `to_dict_of_dict` from:
```
  edge_data : list, optional
       If provided,  the value of the dictionary will be
       set to edge_data for all edges.  This is useful to make
       an adjacency matrix type representation with 1 as the edge data.
       If edgedata is None, the edgedata in G is used to fill the values.
       If G is a multigraph, the edgedata is a dict for each pair (u,v).
```

to a more accurate and help version including a one-liner to build your own:
```
  edge_data : singleton, optional
      If provided, the value of the dictionary will be set to `edge_data` for all edges.
      Usual values could be `1` or `True`.
      If edgedata is None, the edgedata dictionary in G is used creating a dict-of-dict-of-dicts.
      Note that if G is a multigraph, this will be a dict-of-dict-of-dict-of-dicts.
      If you want something more custom made try:
      dod = {n: {nbr: custom(n,nbr,dd) for nbr, dd in nbrdict.items()} for n, nbrdict in G.adj.items()}
```

Maybe the one-liner should be used in an example in the doc_string.

Resolving patch:
diff --git a/networkx/convert.py b/networkx/convert.py
--- a/networkx/convert.py
+++ b/networkx/convert.py
@@ -248,12 +248,93 @@ def to_dict_of_dicts(G, nodelist=None, edge_data=None):
     nodelist : list
        Use only nodes specified in nodelist
 
-    edge_data : list, optional
-       If provided,  the value of the dictionary will be
-       set to edge_data for all edges.  This is useful to make
-       an adjacency matrix type representation with 1 as the edge data.
-       If edgedata is None, the edgedata in G is used to fill the values.
-       If G is a multigraph, the edgedata is a dict for each pair (u,v).
+    edge_data : scalar, optional
+       If provided, the value of the dictionary will be set to `edge_data` for
+       all edges. Usual values could be `1` or `True`. If `edge_data` is
+       `None` (the default), the edgedata in `G` is used, resulting in a
+       dict-of-dict-of-dicts. If `G` is a MultiGraph, the result will be a
+       dict-of-dict-of-dict-of-dicts. See Notes for an approach to customize
+       handling edge data. `edge_data` should *not* be a container.
+
+    Returns
+    -------
+    dod : dict
+       A nested dictionary representation of `G`. Note that the level of
+       nesting depends on the type of `G` and the value of `edge_data`
+       (see Examples).
+
+    See Also
+    --------
+    from_dict_of_dicts, to_dict_of_lists
+
+    Notes
+    -----
+    For a more custom approach to handling edge data, try::
+
+        dod = {
+            n: {
+                nbr: custom(n, nbr, dd) for nbr, dd in nbrdict.items()
+            }
+            for n, nbrdict in G.adj.items()
+        }
+
+    where `custom` returns the desired edge data for each edge between `n` and
+    `nbr`, given existing edge data `dd`.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(3)
+    >>> nx.to_dict_of_dicts(G)
+    {0: {1: {}}, 1: {0: {}, 2: {}}, 2: {1: {}}}
+
+    Edge data is preserved by default (``edge_data=None``), resulting
+    in dict-of-dict-of-dicts where the innermost dictionary contains the
+    edge data:
+
+    >>> G = nx.Graph()
+    >>> G.add_edges_from(
+    ...     [
+    ...         (0, 1, {'weight': 1.0}),
+    ...         (1, 2, {'weight': 2.0}),
+    ...         (2, 0, {'weight': 1.0}),
+    ...     ]
+    ... )
+    >>> d = nx.to_dict_of_dicts(G)
+    >>> d  # doctest: +SKIP
+    {0: {1: {'weight': 1.0}, 2: {'weight': 1.0}},
+     1: {0: {'weight': 1.0}, 2: {'weight': 2.0}},
+     2: {1: {'weight': 2.0}, 0: {'weight': 1.0}}}
+    >>> d[1][2]['weight']
+    2.0
+
+    If `edge_data` is not `None`, edge data in the original graph (if any) is
+    replaced:
+
+    >>> d = nx.to_dict_of_dicts(G, edge_data=1)
+    >>> d
+    {0: {1: 1, 2: 1}, 1: {0: 1, 2: 1}, 2: {1: 1, 0: 1}}
+    >>> d[1][2]
+    1
+
+    This also applies to MultiGraphs: edge data is preserved by default:
+
+    >>> G = nx.MultiGraph()
+    >>> G.add_edge(0, 1, key='a', weight=1.0)
+    'a'
+    >>> G.add_edge(0, 1, key='b', weight=5.0)
+    'b'
+    >>> d = nx.to_dict_of_dicts(G)
+    >>> d  # doctest: +SKIP
+    {0: {1: {'a': {'weight': 1.0}, 'b': {'weight': 5.0}}},
+     1: {0: {'a': {'weight': 1.0}, 'b': {'weight': 5.0}}}}
+    >>> d[0][1]['b']['weight']
+    5.0
+
+    But multi edge data is lost if `edge_data` is not `None`:
+
+    >>> d = nx.to_dict_of_dicts(G, edge_data=10)
+    >>> d
+    {0: {1: 10}, 1: {0: 10}}
     """
     dod = {}
     if nodelist is None:


Test patch:
diff --git a/networkx/tests/test_convert.py b/networkx/tests/test_convert.py
--- a/networkx/tests/test_convert.py
+++ b/networkx/tests/test_convert.py
@@ -279,3 +279,38 @@ class Custom(nx.Graph):
         # this raise exception
         # h._node.update((n, dd.copy()) for n, dd in g.nodes.items())
         # assert isinstance(h._node[1], custom_dict)
+
+
+@pytest.mark.parametrize(
+    "edgelist",
+    (
+        # Graph with no edge data
+        [(0, 1), (1, 2)],
+        # Graph with edge data
+        [(0, 1, {"weight": 1.0}), (1, 2, {"weight": 2.0})],
+    ),
+)
+def test_to_dict_of_dicts_with_edgedata_param(edgelist):
+    G = nx.Graph()
+    G.add_edges_from(edgelist)
+    # Innermost dict value == edge_data when edge_data != None.
+    # In the case when G has edge data, it is overwritten
+    expected = {0: {1: 10}, 1: {0: 10, 2: 10}, 2: {1: 10}}
+    assert nx.to_dict_of_dicts(G, edge_data=10) == expected
+
+
+def test_to_dict_of_dicts_with_edgedata_and_nodelist():
+    G = nx.path_graph(5)
+    nodelist = [2, 3, 4]
+    expected = {2: {3: 10}, 3: {2: 10, 4: 10}, 4: {3: 10}}
+    assert nx.to_dict_of_dicts(G, nodelist=nodelist, edge_data=10) == expected
+
+
+def test_to_dict_of_dicts_with_edgedata_multigraph():
+    """Multi edge data overwritten when edge_data != None"""
+    G = nx.MultiGraph()
+    G.add_edge(0, 1, key="a")
+    G.add_edge(0, 1, key="b")
+    # Multi edge data lost when edge_data is not None
+    expected = {0: {1: 10}, 1: {0: 10}}
+    assert nx.to_dict_of_dicts(G, edge_data=10) == expected