Repository: networkx/networkx

Issue:
multipartite_layout() fails when some node labels are not float
<!--- Provide a general summary of the issue in the Title above -->
Dear contributors,
I am facing currently the following issue which I believe is a bug.
Calling `multipartite_layout()` on a graph which has some nodes with labels which are not float (or integers) will result in an error.

### Current Behavior
Calling `multipartite_layout()` on a graph which has some nodes with hashable labels which are not float (or integers) will result in the following exception:
```
numpy.core._exceptions.UFuncTypeError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('<U32'), dtype('<U32')) -> dtype('<U32')
```
The traceback points at line 1101 of drawing/layout.py, in multipartite_layout:  `layer_pos = np.column_stack([xs, ys]) - offset`

### Expected Behavior
Calling `multipartite_layout()` on a graph which has some nodes with hashable labels which are not float (or integers) should return a `pos` allowing to draw the graph as a multipartite graph.

### Steps to Reproduce
Here is a minimum non-working example:
```python
G = nx.Graph()
G.add_node(0, subset='s0')
G.add_node("b", subset='s0')
G.add_node(2, subset='s1')
G.add_node("d", subset='s1')
G.add_edges_from([(0, 2), (0, "d"), ("b", 2)])
pos = nx.multipartite_layout(G)  # <-- raise the exception mentioned above
```

### Environment
<!--- Please provide details about your local environment -->
Python version: 3.8.11
NetworkX version: 2.6.3
NumPy version: 1.20.3


### Additional context
I fixed this problem with the following workaround (from `multipartite_layout` code, in drawing/layout.py):
```python
import numpy as np

if align not in ("vertical", "horizontal"):
    msg = "align must be either vertical or horizontal."
    raise ValueError(msg)

G, center = _process_params(G, center=center, dim=2)
if len(G) == 0:
    return {}

layers = {}
for v, data in G.nodes(data=True):
    try:
        layer = data[subset_key]
    except KeyError:
        msg = "all nodes must have subset_key (default='subset') as data"
        raise ValueError(msg)
    layers[layer] = [v] + layers.get(layer, [])

pos = None
nodes = []

width = len(layers)
# for i, layer in layers.items():  # <-- original code
for i, layer in enumerate(layers.values()):  # <-- modification
    height = len(layer)
    xs = np.repeat(i, height)
    ys = np.arange(0, height, dtype=float)
    offset = ((width - 1) / 2, (height - 1) / 2)
    layer_pos = np.column_stack([xs, ys]) - offset
    if pos is None:
        pos = layer_pos
    else:
        pos = np.concatenate([pos, layer_pos])
    nodes.extend(layer)
pos = rescale_layout(pos, scale=scale) + center
if align == "horizontal":
    pos = np.flip(pos, 1)
pos = dict(zip(nodes, pos))
return pos
```
However, being new to using NetworkX, I cannot guarantee that it is a reliable fix.


Discussion:
Thanks for the report - I think you're right that the problem here stems from the fact that `multipartite_layout` assumes that the partition identifiers (denoted by default by the `"subset"` node attribute) are numeric. I agree that it'd be good to support non-numeric values for the partition label.

For anyone interested, this should be a pretty straightforward fix - I'd start by looking at this line:

https://github.com/networkx/networkx/blob/92225435bbb1461391289bd5b168717e2318e8bb/networkx/drawing/layout.py#L1095

It'd also be important to add a test for `multipartite_layout` with non-numeric (e.g. string) partition labels.
> I think you're right that the problem here stems from the fact that multipartite_layout assumes that the partition identifiers (denoted by default by the "subset" node attribute) are numeric.

Yes this is what I figured out. Just to clarify what I think the problem is : the node names, and not the partition values (with default key "subset"), are assumed to be numeric.

Hence, the following fails (some node have names which are string)
```python
G = nx.Graph()
G.add_node(0, subset='s0')
G.add_node("b", subset='s0')
G.add_node(2, subset='s1')
G.add_node("d", subset='s1')
G.add_edges_from([(0, 2), (0, "d"), ("b", 2)])
pos = nx.multipartite_layout(G)  # <-- raise the exception mentioned above
```
while the following will work (all the nodes' names are numeric)
```python
G = nx.Graph()
G.add_node(0, subset='s0')
G.add_node(1, subset='s0')
G.add_node(2, subset='s1')
G.add_node(3, subset='s1')
G.add_edges_from([(0, 2), (0, 3), (1, 2)])
pos = nx.multipartite_layout(G)
```
> while the following will work (all the nodes' names are numeric)

I recommend running this second example :)

Resolving patch:
diff --git a/networkx/drawing/layout.py b/networkx/drawing/layout.py
--- a/networkx/drawing/layout.py
+++ b/networkx/drawing/layout.py
@@ -1092,7 +1092,7 @@ def multipartite_layout(G, subset_key="subset", align="vertical", scale=1, cente
     nodes = []
 
     width = len(layers)
-    for i, layer in layers.items():
+    for i, layer in enumerate(layers.values()):
         height = len(layer)
         xs = np.repeat(i, height)
         ys = np.arange(0, height, dtype=float)


Test patch:
diff --git a/networkx/drawing/tests/test_layout.py b/networkx/drawing/tests/test_layout.py
--- a/networkx/drawing/tests/test_layout.py
+++ b/networkx/drawing/tests/test_layout.py
@@ -412,3 +412,15 @@ def test_rescale_layout_dict(self):
         }
         for k, v in expectation.items():
             assert (s_vpos[k] == v).all()
+
+
+def test_multipartite_layout_nonnumeric_partition_labels():
+    """See gh-5123."""
+    G = nx.Graph()
+    G.add_node(0, subset="s0")
+    G.add_node(1, subset="s0")
+    G.add_node(2, subset="s1")
+    G.add_node(3, subset="s1")
+    G.add_edges_from([(0, 2), (0, 3), (1, 2)])
+    pos = nx.multipartite_layout(G)
+    assert len(pos) == len(G)