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:
rossbar: > As far as the kwarg name, the term label could be confusing since sometimes label is an attribute of the node rather than the node itself. The choice nodelist matches the kwarg in the function to_numpy_array, so might be good. But of course it doesn't have to be of a list. So ... ???

I totally agree, thanks for the suggestion - renamed to nodelist in 86fee52. +1 for consistencey with `to_numpy_array`, even if the **list** part is not strictly the most precise term. I definitely prefer it to `nodesequence=` :)

The docstring has been updated as well from `list` -> `sequence`, which I think is the right abstraction. On this front, it's worth noting that the decision to require the `nodelist` object to have a length (thus excluding generators) was made arbitrarily. My thinking was that it was best to start with the most restrictive option which allows for some minor input validation and raise exceptions when the sequence is a different length than the adjacency matrix dimension. It's not a necessary restriction though - it could be removed to allow iterators as well. If that's something that's desired now I'm happy to try to add it, but I'm also content to leave it as a follow-up enhancement w/ a separate, more focused discussion!

Patch:
diff --git a/networkx/classes/coreviews.py b/networkx/classes/coreviews.py
index f4b54c7b7..9cbf57850 100644
--- a/networkx/classes/coreviews.py
+++ b/networkx/classes/coreviews.py
@@ -281,7 +281,13 @@ class FilterAtlas(Mapping):  # nodedict, nbrdict, keydict
         self.NODE_OK = NODE_OK
 
     def __len__(self):
-        return sum(1 for n in self)
+        # check whether NODE_OK stores the number of nodes as `length`
+        # or the nodes themselves as a set `nodes`. If not, count the nodes.
+        if hasattr(self.NODE_OK, "length"):
+            return self.NODE_OK.length
+        if hasattr(self.NODE_OK, "nodes"):
+            return len(self.NODE_OK.nodes & self._atlas.keys())
+        return sum(1 for n in self._atlas if self.NODE_OK(n))
 
     def __iter__(self):
         try:  # check that NODE_OK has attr 'nodes'
@@ -324,7 +330,13 @@ class FilterAdjacency(Mapping):  # edgedict
         self.EDGE_OK = EDGE_OK
 
     def __len__(self):
-        return sum(1 for n in self)
+        # check whether NODE_OK stores the number of nodes as `length`
+        # or the nodes themselves as a set `nodes`. If not, count the nodes.
+        if hasattr(self.NODE_OK, "length"):
+            return self.NODE_OK.length
+        if hasattr(self.NODE_OK, "nodes"):
+            return len(self.NODE_OK.nodes & self._atlas.keys())
+        return sum(1 for n in self._atlas if self.NODE_OK(n))
 
     def __iter__(self):
         try:  # check that NODE_OK has attr 'nodes'
diff --git a/networkx/classes/filters.py b/networkx/classes/filters.py
index 215aa375d..f88bbcb2e 100644
--- a/networkx/classes/filters.py
+++ b/networkx/classes/filters.py
@@ -54,7 +54,14 @@ def hide_multiedges(edges):
 
 # write show_nodes as a class to make SubGraph pickleable
 class show_nodes:
-    """Filter class to show specific nodes."""
+    """Filter class to show specific nodes.
+
+    Attach the set of nodes as an attribute to speed up this commonly used filter
+
+    Note that another allowed attribute for filters is to store the number of nodes
+    on the filter as attribute `length` (used in `__len__`). It is a user
+    responsibility to ensure this attribute is accurate if present.
+    """
 
     def __init__(self, nodes):
         self.nodes = set(nodes)
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
index 6165ac18e..d4c924a85 100644
--- 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
index ab73172a4..1c39afded 100644
--- 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
index ca8d08c70..7076eb57a 100644
--- 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)