Repository: networkx/networkx

Issue:
Allow caller to opt out of adding `weight` attributes in `from_numpy_array`
### Current Behavior

<!--- Tell us what happens instead of the expected behavior -->

Right now, calls to `from_numpy_array` add a `weight` attribute to all edges in the newly created (non-multi)graph. 

### Expected Behavior

<!--- Tell us what should happen -->
It would be nice to allow the user to opt out of this behavior, or to specify an attribute that should receive this weight.

### Steps to Reproduce

<!--- Provide a minimal example that reproduces the bug -->
```python
import networkx as nx
import numpy as np

mat = np.array([
    [0, 1],
    [1, 0]
])
g = nx.from_numpy_array(mat, create_using=nx.DiGraph)
print(g.edges(data=True))
```

```
[(0, 1, {'weight': 1}), (1, 0, {'weight': 1})]
```

### Environment

<!--- Please provide details about your local environment -->

* Python version: 3.10
* NetworkX version: All (2.6.3)

### Additional context

<!--- Add any other context about the problem here, screenshots, etc. -->

This is confusing behavior because the caller does not explicitly request attribute creation. I would propose the following PREFERRED behavior:

#### Preferred

Creating a graph with an array of `dtype` ∈ {int, float} creates an attribute of name `weight_attribute`, which defaults to `w` for backwards compat.

```python
g = nx.from_numpy_array(mat, create_using=nx.DiGraph, weight_attribute="w")
print(g.edges(data=True))
```

```
[(0, 1, {'w': 1}), (1, 0, {'w': 1})]
```

```

Creating a graph with an array of `dtype` = bool treats each value as an existence flag and does not create an attribute:


```python
g = nx.from_numpy_array(mat==1, create_using=nx.DiGraph, weight_attribute="w")
print(g.edges(data=True))
```

```
[(0, 1, {}), (1, 0, {})]
```

#### Also seems fine but changes fn signature:

Perhaps better for backwards compat is to allow the user to opt-out of the current behavior with a new argument `weight_attribute` which defaults to `weight` but can be set to `None` to remove this behavior. (Perhaps a bit more predictable than the above option?)

#### Seems kinda jank but is totally back-compat:

Setting `parallel_edges=True` and `create_using=nx.DiGraph` currently doesn't do anything, and could be repurposed to perform this behavior. Eh. This is kinda gross.

Discussion:
rossbar: The currently failing test (as of b9218fb) illustrates an interesting corner case: what happens with `edge_attr=None` (indicating the user doesn't want any attributes) but the dtype of the array is structured (which is currently interpreted as "map all named attributes to edge attributes in the graph").

I *think* what we want is for `edge_attr` to override the `dtype` - in which case it looks like the code may need to be adjusted to handle this case.
j6k4m8: Indeed, I was wodnering about how best to handle this! Very nice to have such great unit tests on this repo :) My PhD qualifying exam is tomorrow so I'm unlikely to hit this before thursday (eek!) but I will address asap! Thank you for the guidance!

Patch:
diff --git a/networkx/convert_matrix.py b/networkx/convert_matrix.py
index 7540ad780..8f8202f69 100644
--- a/networkx/convert_matrix.py
+++ b/networkx/convert_matrix.py
@@ -1000,7 +1000,7 @@ def to_numpy_array(
     return A
 
 
-def from_numpy_array(A, parallel_edges=False, create_using=None):
+def from_numpy_array(A, parallel_edges=False, create_using=None, edge_attr="weight"):
     """Returns a graph from a 2D NumPy array.
 
     The 2D NumPy array is interpreted as an adjacency matrix for the graph.
@@ -1020,6 +1020,10 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
     create_using : NetworkX graph constructor, optional (default=nx.Graph)
        Graph type to create. If graph instance, then cleared before populated.
 
+    edge_attr : String, optional (default="weight")
+        The attribute to which the array values are assigned on each edge. If
+        it is None, edge attributes will not be assigned.
+
     Notes
     -----
     For directed graphs, explicitly mention create_using=nx.DiGraph,
@@ -1034,6 +1038,11 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
     indicated by the upper triangle of the array `A` will be added to the
     graph.
 
+    If `edge_attr` is Falsy (False or None), edge attributes will not be
+    assigned, and the array data will be treated like a binary mask of
+    edge presence or absence. Otherwise, the attributes will be assigned
+    as follows:
+
     If the NumPy array has a single data type for each array entry it
     will be converted to an appropriate Python data type.
 
@@ -1127,7 +1136,9 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
                 {
                     name: kind_to_python_type[dtype.kind](val)
                     for (_, dtype, name), val in zip(fields, A[u, v])
-                },
+                }
+                if edge_attr
+                else {},
             )
             for u, v in edges
         )
@@ -1144,11 +1155,17 @@ def from_numpy_array(A, parallel_edges=False, create_using=None):
         #         for d in range(A[u, v]):
         #             G.add_edge(u, v, weight=1)
         #
-        triples = chain(
-            ((u, v, {"weight": 1}) for d in range(A[u, v])) for (u, v) in edges
-        )
+        if edge_attr:
+            triples = chain(
+                ((u, v, {edge_attr: 1}) for d in range(A[u, v])) for (u, v) in edges
+            )
+        else:
+            triples = chain(((u, v, {}) for d in range(A[u, v])) for (u, v) in edges)
     else:  # basic data type
-        triples = ((u, v, {"weight": python_type(A[u, v])}) for u, v in edges)
+        if edge_attr:
+            triples = ((u, v, {edge_attr: python_type(A[u, v])}) for u, v in edges)
+        else:
+            triples = ((u, v, {}) for u, v in edges)
     # If we are creating an undirected multigraph, only add the edges from the
     # upper triangle of the matrix. Otherwise, add all the edges. This relies
     # on the fact that the vertices created in the


Test patch:
diff --git a/networkx/tests/test_convert_numpy.py b/networkx/tests/test_convert_numpy.py
index 5a3b88e26..eee6ded1d 100644
--- a/networkx/tests/test_convert_numpy.py
+++ b/networkx/tests/test_convert_numpy.py
@@ -164,6 +164,34 @@ class TestConvertNumpyArray:
         )
         assert graphs_equal(actual, expected)
 
+    @pytest.mark.parametrize(
+        "dt",
+        (
+            None,  # default
+            int,  # integer dtype
+            np.dtype(
+                [("weight", "f8"), ("color", "i1")]
+            ),  # Structured dtype with named fields
+        ),
+    )
+    def test_from_numpy_array_no_edge_attr(self, dt):
+        A = np.array([[0, 1], [1, 0]], dtype=dt)
+        G = nx.from_numpy_array(A, edge_attr=None)
+        assert "weight" not in G.edges[0, 1]
+        assert len(G.edges[0, 1]) == 0
+
+    def test_from_numpy_array_multiedge_no_edge_attr(self):
+        A = np.array([[0, 2], [2, 0]])
+        G = nx.from_numpy_array(A, create_using=nx.MultiDiGraph, edge_attr=None)
+        assert all("weight" not in e for _, e in G[0][1].items())
+        assert len(G[0][1][0]) == 0
+
+    def test_from_numpy_array_custom_edge_attr(self):
+        A = np.array([[0, 2], [3, 0]])
+        G = nx.from_numpy_array(A, edge_attr="cost")
+        assert "weight" not in G.edges[0, 1]
+        assert G.edges[0, 1]["cost"] == 3
+
     def test_symmetric(self):
         """Tests that a symmetric array has edges added only once to an
         undirected multigraph when using :func:`networkx.from_numpy_array`.