Repository: networkx/networkx

Issue:
networkx.exception.NetworkXUnfeasible when creating a graph from pandas
### Current Behavior

When creating a graph from a pandas dataframe, if the row/columns names are integer less than the dimensionality of the frame, an error occurs:
```
  raise nx.NetworkXUnfeasible(
networkx.exception.NetworkXUnfeasible: The node label sets are overlapping and no ordering can resolve the mapping. Use copy=True.
```
### Expected Behavior
Graph is created without errors
To fix it:
`nx.relabel.relabel_nodes(G, dict(enumerate(df.columns)), copy=False`) in convert_matrix.py should have copy=True otherwise there's a duplication of labels

### Steps to Reproduce

```
M = pd.DataFrame(0.0, index=[1010001,2,1,1010002], columns=[1010001,2,1,1010002])
G = nx.from_pandas_adjacency(M, create_using = nx.DiGraph)
```

Possible fix appears in #7408 

### Environment

Python version: 3.9, 3.10
NetworkX version: 3.1, 3.3



Discussion:

Resolving patch:
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
--- a/networkx/convert_matrix.py
+++ b/networkx/convert_matrix.py
@@ -214,9 +214,8 @@ def from_pandas_adjacency(df, create_using=None):
         raise nx.NetworkXError("Columns must match Indices.", msg) from err
 
     A = df.values
-    G = from_numpy_array(A, create_using=create_using)
+    G = from_numpy_array(A, create_using=create_using, nodelist=df.columns)
 
-    nx.relabel.relabel_nodes(G, dict(enumerate(df.columns)), copy=False)
     return G
 
 
@@ -1022,7 +1021,9 @@ def to_numpy_array(
 
 
 @nx._dispatchable(graphs=None, returns_graph=True)
-def from_numpy_array(A, parallel_edges=False, create_using=None, edge_attr="weight"):
+def from_numpy_array(
+    A, parallel_edges=False, create_using=None, edge_attr="weight", *, nodelist=None
+):
     """Returns a graph from a 2D NumPy array.
 
     The 2D NumPy array is interpreted as an adjacency matrix for the graph.
@@ -1046,6 +1047,11 @@ def from_numpy_array(A, parallel_edges=False, create_using=None, edge_attr="weig
         The attribute to which the array values are assigned on each edge. If
         it is None, edge attributes will not be assigned.
 
+    nodelist : sequence of nodes, optional
+        A sequence of objects to use as the nodes in the graph. If provided, the
+        list of nodes must be the same length as the dimensions of `A`. The
+        default is `None`, in which case the nodes are drawn from ``range(n)``.
+
     Notes
     -----
     For directed graphs, explicitly mention create_using=nx.DiGraph,
@@ -1139,9 +1145,14 @@ def from_numpy_array(A, parallel_edges=False, create_using=None, edge_attr="weig
         python_type = kind_to_python_type[dt.kind]
     except Exception as err:
         raise TypeError(f"Unknown numpy data type: {dt}") from err
+    if _default_nodes := (nodelist is None):
+        nodelist = range(n)
+    else:
+        if len(nodelist) != n:
+            raise ValueError("nodelist must have the same length as A.shape[0]")
 
     # Make sure we get even the isolated nodes of the graph.
-    G.add_nodes_from(range(n))
+    G.add_nodes_from(nodelist)
     # Get a list of all the entries in the array with nonzero entries. These
     # coordinates become edges in the graph. (convert to int from np.int64)
     edges = ((int(e[0]), int(e[1])) for e in zip(*A.nonzero()))
@@ -1198,5 +1209,9 @@ def from_numpy_array(A, parallel_edges=False, create_using=None, edge_attr="weig
     # when `G.add_edges_from()` is invoked below.
     if G.is_multigraph() and not G.is_directed():
         triples = ((u, v, d) for u, v, d in triples if u <= v)
+    # Remap nodes if user provided custom `nodelist`
+    if not _default_nodes:
+        idx_to_node = dict(enumerate(nodelist))
+        triples = ((idx_to_node[u], idx_to_node[v], d) for u, v, d in triples)
     G.add_edges_from(triples)
     return G


Test patch:
diff --git a/networkx/tests/test_convert_numpy.py b/networkx/tests/test_convert_numpy.py
--- a/networkx/tests/test_convert_numpy.py
+++ b/networkx/tests/test_convert_numpy.py
@@ -1,3 +1,5 @@
+import itertools
+
 import pytest
 
 np = pytest.importorskip("numpy")
@@ -393,3 +395,138 @@ def test_to_numpy_array_structured_multigraph_raises(graph_type):
     dtype = np.dtype([("weight", int), ("cost", int)])
     with pytest.raises(nx.NetworkXError, match="Structured arrays are not supported"):
         nx.to_numpy_array(G, dtype=dtype, weight=None)
+
+
+def test_from_numpy_array_nodelist_bad_size():
+    """An exception is raised when `len(nodelist) != A.shape[0]`."""
+    n = 5  # Number of nodes
+    A = np.diag(np.ones(n - 1), k=1)  # Adj. matrix for P_n
+    expected = nx.path_graph(n)
+
+    assert graphs_equal(nx.from_numpy_array(A, edge_attr=None), expected)
+    nodes = list(range(n))
+    assert graphs_equal(
+        nx.from_numpy_array(A, edge_attr=None, nodelist=nodes), expected
+    )
+
+    # Too many node labels
+    nodes = list(range(n + 1))
+    with pytest.raises(ValueError, match="nodelist must have the same length as A"):
+        nx.from_numpy_array(A, nodelist=nodes)
+
+    # Too few node labels
+    nodes = list(range(n - 1))
+    with pytest.raises(ValueError, match="nodelist must have the same length as A"):
+        nx.from_numpy_array(A, nodelist=nodes)
+
+
+@pytest.mark.parametrize(
+    "nodes",
+    (
+        [4, 3, 2, 1, 0],
+        [9, 7, 1, 2, 8],
+        ["a", "b", "c", "d", "e"],
+        [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)],
+        ["A", 2, 7, "spam", (1, 3)],
+    ),
+)
+def test_from_numpy_array_nodelist(nodes):
+    A = np.diag(np.ones(4), k=1)
+    # Without edge attributes
+    expected = nx.relabel_nodes(
+        nx.path_graph(5), mapping=dict(enumerate(nodes)), copy=True
+    )
+    G = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes)
+    assert graphs_equal(G, expected)
+
+    # With edge attributes
+    nx.set_edge_attributes(expected, 1.0, name="weight")
+    G = nx.from_numpy_array(A, nodelist=nodes)
+    assert graphs_equal(G, expected)
+
+
+@pytest.mark.parametrize(
+    "nodes",
+    (
+        [4, 3, 2, 1, 0],
+        [9, 7, 1, 2, 8],
+        ["a", "b", "c", "d", "e"],
+        [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)],
+        ["A", 2, 7, "spam", (1, 3)],
+    ),
+)
+def test_from_numpy_array_nodelist_directed(nodes):
+    A = np.diag(np.ones(4), k=1)
+    # Without edge attributes
+    H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4)])
+    expected = nx.relabel_nodes(H, mapping=dict(enumerate(nodes)), copy=True)
+    G = nx.from_numpy_array(A, create_using=nx.DiGraph, edge_attr=None, nodelist=nodes)
+    assert graphs_equal(G, expected)
+
+    # With edge attributes
+    nx.set_edge_attributes(expected, 1.0, name="weight")
+    G = nx.from_numpy_array(A, create_using=nx.DiGraph, nodelist=nodes)
+    assert graphs_equal(G, expected)
+
+
+@pytest.mark.parametrize(
+    "nodes",
+    (
+        [4, 3, 2, 1, 0],
+        [9, 7, 1, 2, 8],
+        ["a", "b", "c", "d", "e"],
+        [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)],
+        ["A", 2, 7, "spam", (1, 3)],
+    ),
+)
+def test_from_numpy_array_nodelist_multigraph(nodes):
+    A = np.array(
+        [
+            [0, 1, 0, 0, 0],
+            [1, 0, 2, 0, 0],
+            [0, 2, 0, 3, 0],
+            [0, 0, 3, 0, 4],
+            [0, 0, 0, 4, 0],
+        ]
+    )
+
+    H = nx.MultiGraph()
+    for i, edge in enumerate(((0, 1), (1, 2), (2, 3), (3, 4))):
+        H.add_edges_from(itertools.repeat(edge, i + 1))
+    expected = nx.relabel_nodes(H, mapping=dict(enumerate(nodes)), copy=True)
+
+    G = nx.from_numpy_array(
+        A,
+        parallel_edges=True,
+        create_using=nx.MultiGraph,
+        edge_attr=None,
+        nodelist=nodes,
+    )
+    assert graphs_equal(G, expected)
+
+
+@pytest.mark.parametrize(
+    "nodes",
+    (
+        [4, 3, 2, 1, 0],
+        [9, 7, 1, 2, 8],
+        ["a", "b", "c", "d", "e"],
+        [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)],
+        ["A", 2, 7, "spam", (1, 3)],
+    ),
+)
+@pytest.mark.parametrize("graph", (nx.complete_graph, nx.cycle_graph, nx.wheel_graph))
+def test_from_numpy_array_nodelist_rountrip(graph, nodes):
+    G = graph(5)
+    A = nx.to_numpy_array(G)
+    expected = nx.relabel_nodes(G, mapping=dict(enumerate(nodes)), copy=True)
+    H = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes)
+    assert graphs_equal(H, expected)
+
+    # With an isolated node
+    G = graph(4)
+    G.add_node("foo")
+    A = nx.to_numpy_array(G)
+    expected = nx.relabel_nodes(G, mapping=dict(zip(G.nodes, nodes)), copy=True)
+    H = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes)
+    assert graphs_equal(H, expected)
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
@@ -318,3 +318,21 @@ def test_to_pandas_edgelist_with_nodelist():
     df = nx.to_pandas_edgelist(G, nodelist=[1, 2])
     assert 0 not in df["source"].to_numpy()
     assert 100 not in df["weight"].to_numpy()
+
+
+def test_from_pandas_adjacency_with_index_collisions():
+    """See gh-7407"""
+    df = pd.DataFrame(
+        [
+            [0, 1, 0, 0],
+            [0, 0, 1, 0],
+            [0, 0, 0, 1],
+            [0, 0, 0, 0],
+        ],
+        index=[1010001, 2, 1, 1010002],
+        columns=[1010001, 2, 1, 1010002],
+    )
+    G = nx.from_pandas_adjacency(df, create_using=nx.DiGraph)
+    expected = nx.DiGraph([(1010001, 2), (2, 1), (1, 1010002)])
+    assert nodes_equal(G.nodes, expected.nodes)
+    assert edges_equal(G.edges, expected.edges)