Repository: networkx/networkx

Issue:
documentation for "edge_key" param in nx.from_pandas_edgelist() missing
I just noticed this while working on implementing support for geopandas (I want it to behave as similar to pandas functions). I guess it's for MultiGraphs which I can't really wrap my head around.


Discussion:
Thanks for reporting this!  
That's the result of #4076 which was recently merged after apparently not enough review. :)
It needs to be fixed.

Resolving patch:
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
--- a/networkx/convert_matrix.py
+++ b/networkx/convert_matrix.py
@@ -209,7 +209,13 @@ def from_pandas_adjacency(df, create_using=None):
 
 
 def to_pandas_edgelist(
-    G, source="source", target="target", nodelist=None, dtype=None, order=None
+    G,
+    source="source",
+    target="target",
+    nodelist=None,
+    dtype=None,
+    order=None,
+    edge_key=None,
 ):
     """Returns the graph edge list as a Pandas DataFrame.
 
@@ -229,6 +235,20 @@ def to_pandas_edgelist(
     nodelist : list, optional
        Use only nodes specified in nodelist
 
+    dtype : dtype, default None
+        Use to create the DataFrame. Data type to force.
+        Only a single dtype is allowed. If None, infer.
+
+    order : None
+        An unused parameter mistakenly included in the function.
+
+        .. deprecated:: 2.6
+            This is deprecated and will be removed in NetworkX v3.0.
+
+    edge_key : str or int or None, optional (default=None)
+        A valid column name (string or integer) for the edge keys (for the
+        multigraph case). If None, edge keys are not stored in the DataFrame.
+
     Returns
     -------
     df : Pandas DataFrame
@@ -248,6 +268,13 @@ def to_pandas_edgelist(
     0      A      B     1       7
     1      C      E     9      10
 
+    >>> G = nx.MultiGraph([('A', 'B', {'cost': 1}), ('A', 'B', {'cost': 9})])
+    >>> df = nx.to_pandas_edgelist(G, nodelist=['A', 'C'], edge_key='ekey')
+    >>> df[['source', 'target', 'cost', 'ekey']]
+      source target  cost  ekey
+    0      A      B     1     0
+    1      A      B     9     1
+
     """
     import pandas as pd
 
@@ -255,21 +282,28 @@ def to_pandas_edgelist(
         edgelist = G.edges(data=True)
     else:
         edgelist = G.edges(nodelist, data=True)
-    source_nodes = [s for s, t, d in edgelist]
-    target_nodes = [t for s, t, d in edgelist]
+    source_nodes = [s for s, _, _ in edgelist]
+    target_nodes = [t for _, t, _ in edgelist]
 
-    all_keys = set().union(*(d.keys() for s, t, d in edgelist))
-    if source in all_keys:
+    all_attrs = set().union(*(d.keys() for _, _, d in edgelist))
+    if source in all_attrs:
         raise nx.NetworkXError(f"Source name '{source}' is an edge attr name")
-    if target in all_keys:
+    if target in all_attrs:
         raise nx.NetworkXError(f"Target name '{target}' is an edge attr name")
 
     nan = float("nan")
-    edge_attr = {k: [d.get(k, nan) for s, t, d in edgelist] for k in all_keys}
+    edge_attr = {k: [d.get(k, nan) for _, _, d in edgelist] for k in all_attrs}
+
+    if G.is_multigraph() and edge_key is not None:
+        if edge_key in all_attrs:
+            raise nx.NetworkXError(f"Edge key name '{edge_key}' is an edge attr name")
+        edge_keys = [k for _, _, k in G.edges(keys=True)]
+        edgelistdict = {source: source_nodes, target: target_nodes, edge_key: edge_keys}
+    else:
+        edgelistdict = {source: source_nodes, target: target_nodes}
 
-    edgelistdict = {source: source_nodes, target: target_nodes}
     edgelistdict.update(edge_attr)
-    return pd.DataFrame(edgelistdict)
+    return pd.DataFrame(edgelistdict, dtype=dtype)
 
 
 def from_pandas_edgelist(


Test patch:
diff --git a/networkx/tests/test_convert_pandas.py b/networkx/tests/test_convert_pandas.py
--- a/networkx/tests/test_convert_pandas.py
+++ b/networkx/tests/test_convert_pandas.py
@@ -200,6 +200,14 @@ def test_to_edgelist_custom_source_or_target_col_exists(self):
             nx.NetworkXError, nx.to_pandas_edgelist, G, target="target_col_name"
         )
 
+    def test_to_edgelist_edge_key_col_exists(self):
+        G = nx.path_graph(10, create_using=nx.MultiGraph)
+        G.add_weighted_edges_from((u, v, u) for u, v in list(G.edges()))
+        nx.set_edge_attributes(G, 0, name="edge_key_name")
+        pytest.raises(
+            nx.NetworkXError, nx.to_pandas_edgelist, G, edge_key="edge_key_name"
+        )
+
     def test_from_adjacency(self):
         nodelist = [1, 2]
         dftrue = pd.DataFrame(
@@ -209,17 +217,18 @@ def test_from_adjacency(self):
         df = nx.to_pandas_adjacency(G, dtype=int)
         pd.testing.assert_frame_equal(df, dftrue)
 
-    def test_roundtrip(self):
+    @pytest.mark.parametrize("graph", [nx.Graph, nx.MultiGraph])
+    def test_roundtrip(self, graph):
         # edgelist
-        Gtrue = nx.Graph([(1, 1), (1, 2)])
+        Gtrue = graph([(1, 1), (1, 2)])
         df = nx.to_pandas_edgelist(Gtrue)
-        G = nx.from_pandas_edgelist(df)
+        G = nx.from_pandas_edgelist(df, create_using=graph)
         assert_graphs_equal(Gtrue, G)
         # adjacency
         adj = {1: {1: {"weight": 1}, 2: {"weight": 1}}, 2: {1: {"weight": 1}}}
-        Gtrue = nx.Graph(adj)
+        Gtrue = graph(adj)
         df = nx.to_pandas_adjacency(Gtrue, dtype=int)
-        G = nx.from_pandas_adjacency(df)
+        G = nx.from_pandas_adjacency(df, create_using=graph)
         assert_graphs_equal(Gtrue, G)
 
     def test_from_adjacency_named(self):
@@ -238,18 +247,19 @@ def test_from_adjacency_named(self):
     def test_edgekey_with_multigraph(self):
         df = pd.DataFrame(
             {
-                "attr1": {"A": "F1", "B": "F2", "C": "F3"},
-                "attr2": {"A": 1, "B": 0, "C": 0},
-                "attr3": {"A": 0, "B": 1, "C": 0},
-                "source": {"A": "N1", "B": "N2", "C": "N1"},
-                "target": {"A": "N2", "B": "N3", "C": "N1"},
+                "source": {"A": "N1", "B": "N2", "C": "N1", "D": "N1"},
+                "target": {"A": "N2", "B": "N3", "C": "N1", "D": "N2"},
+                "attr1": {"A": "F1", "B": "F2", "C": "F3", "D": "F4"},
+                "attr2": {"A": 1, "B": 0, "C": 0, "D": 0},
+                "attr3": {"A": 0, "B": 1, "C": 0, "D": 1},
             }
         )
-        Gtrue = nx.Graph(
+        Gtrue = nx.MultiGraph(
             [
-                ("N1", "N2", {"F1": {"attr2": 1, "attr3": 0}}),
-                ("N2", "N3", {"F2": {"attr2": 0, "attr3": 1}}),
-                ("N1", "N1", {"F3": {"attr2": 0, "attr3": 0}}),
+                ("N1", "N2", "F1", {"attr2": 1, "attr3": 0}),
+                ("N2", "N3", "F2", {"attr2": 0, "attr3": 1}),
+                ("N1", "N1", "F3", {"attr2": 0, "attr3": 0}),
+                ("N1", "N2", "F4", {"attr2": 0, "attr3": 1}),
             ]
         )
         # example from issue #4065
@@ -263,6 +273,13 @@ def test_edgekey_with_multigraph(self):
         )
         assert_graphs_equal(G, Gtrue)
 
+        df_roundtrip = nx.to_pandas_edgelist(G, edge_key="attr1")
+        df_roundtrip = df_roundtrip.sort_values("attr1")
+        df_roundtrip.index = ["A", "B", "C", "D"]
+        pd.testing.assert_frame_equal(
+            df, df_roundtrip[["source", "target", "attr1", "attr2", "attr3"]]
+        )
+
     def test_edgekey_with_normal_graph_no_action(self):
         Gtrue = nx.Graph(
             [
diff --git a/networkx/tests/test_convert_scipy.py b/networkx/tests/test_convert_scipy.py
--- a/networkx/tests/test_convert_scipy.py
+++ b/networkx/tests/test_convert_scipy.py
@@ -2,7 +2,7 @@
 
 np = pytest.importorskip("numpy")
 sp = pytest.importorskip("scipy")
-sparse = sp.sparse
+sp_sparse = sp.sparse
 npt = np.testing
 
 import networkx as nx
@@ -213,7 +213,7 @@ def test_from_scipy_sparse_matrix_parallel_edges(self):
         creating a multigraph.
 
         """
-        A = sparse.csr_matrix([[1, 1], [1, 2]])
+        A = sp_sparse.csr_matrix([[1, 1], [1, 2]])
         # First, with a simple graph, each integer entry in the adjacency
         # matrix is interpreted as the weight of a single edge in the graph.
         expected = nx.DiGraph()
@@ -253,7 +253,7 @@ def test_symmetric(self):
         :func:`networkx.from_scipy_sparse_matrix`.
 
         """
-        A = sparse.csr_matrix([[0, 1], [1, 0]])
+        A = sp_sparse.csr_matrix([[0, 1], [1, 0]])
         G = nx.from_scipy_sparse_matrix(A, create_using=nx.MultiGraph)
         expected = nx.MultiGraph()
         expected.add_edge(0, 1, weight=1)
@@ -275,5 +275,5 @@ def test_from_scipy_sparse_matrix_formats(sparse_format):
             (2, 1, {"weight": 1}),
         ]
     )
-    A = sparse.coo_matrix([[0, 3, 2], [3, 0, 1], [2, 1, 0]]).asformat(sparse_format)
+    A = sp_sparse.coo_matrix([[0, 3, 2], [3, 0, 1], [2, 1, 0]]).asformat(sparse_format)
     assert_graphs_equal(expected, nx.from_scipy_sparse_matrix(A))