about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py5
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py122
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py302
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py255
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py160
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py127
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/base_test.py81
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_connectivity.py143
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_correlation.py123
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_mixing.py176
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_neighbor_degree.py108
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_pairs.py87
13 files changed, 1689 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py
new file mode 100644
index 00000000..4d988860
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py
@@ -0,0 +1,5 @@
+from networkx.algorithms.assortativity.connectivity import *
+from networkx.algorithms.assortativity.correlation import *
+from networkx.algorithms.assortativity.mixing import *
+from networkx.algorithms.assortativity.neighbor_degree import *
+from networkx.algorithms.assortativity.pairs import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py
new file mode 100644
index 00000000..c3fde0da
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py
@@ -0,0 +1,122 @@
+from collections import defaultdict
+
+import networkx as nx
+
+__all__ = ["average_degree_connectivity"]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def average_degree_connectivity(
+    G, source="in+out", target="in+out", nodes=None, weight=None
+):
+    r"""Compute the average degree connectivity of graph.
+
+    The average degree connectivity is the average nearest neighbor degree of
+    nodes with degree k. For weighted graphs, an analogous measure can
+    be computed using the weighted average neighbors degree defined in
+    [1]_, for a node `i`, as
+
+    .. math::
+
+        k_{nn,i}^{w} = \frac{1}{s_i} \sum_{j \in N(i)} w_{ij} k_j
+
+    where `s_i` is the weighted degree of node `i`,
+    `w_{ij}` is the weight of the edge that links `i` and `j`,
+    and `N(i)` are the neighbors of node `i`.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source :  "in"|"out"|"in+out" (default:"in+out")
+       Directed graphs only. Use "in"- or "out"-degree for source node.
+
+    target : "in"|"out"|"in+out" (default:"in+out"
+       Directed graphs only. Use "in"- or "out"-degree for target node.
+
+    nodes : list or iterable (optional)
+        Compute neighbor connectivity for these nodes. The default is all
+        nodes.
+
+    weight : string or None, optional (default=None)
+       The edge attribute that holds the numerical value used as a weight.
+       If None, then each edge has weight 1.
+
+    Returns
+    -------
+    d : dict
+       A dictionary keyed by degree k with the value of average connectivity.
+
+    Raises
+    ------
+    NetworkXError
+        If either `source` or `target` are not one of 'in',
+        'out', or 'in+out'.
+        If either `source` or `target` is passed for an undirected graph.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> G.edges[1, 2]["weight"] = 3
+    >>> nx.average_degree_connectivity(G)
+    {1: 2.0, 2: 1.5}
+    >>> nx.average_degree_connectivity(G, weight="weight")
+    {1: 2.0, 2: 1.75}
+
+    See Also
+    --------
+    average_neighbor_degree
+
+    References
+    ----------
+    .. [1] A. Barrat, M. Barthélemy, R. Pastor-Satorras, and A. Vespignani,
+       "The architecture of complex weighted networks".
+       PNAS 101 (11): 3747–3752 (2004).
+    """
+    # First, determine the type of neighbors and the type of degree to use.
+    if G.is_directed():
+        if source not in ("in", "out", "in+out"):
+            raise nx.NetworkXError('source must be one of "in", "out", or "in+out"')
+        if target not in ("in", "out", "in+out"):
+            raise nx.NetworkXError('target must be one of "in", "out", or "in+out"')
+        direction = {"out": G.out_degree, "in": G.in_degree, "in+out": G.degree}
+        neighbor_funcs = {
+            "out": G.successors,
+            "in": G.predecessors,
+            "in+out": G.neighbors,
+        }
+        source_degree = direction[source]
+        target_degree = direction[target]
+        neighbors = neighbor_funcs[source]
+        # `reverse` indicates whether to look at the in-edge when
+        # computing the weight of an edge.
+        reverse = source == "in"
+    else:
+        if source != "in+out" or target != "in+out":
+            raise nx.NetworkXError(
+                f"source and target arguments are only supported for directed graphs"
+            )
+        source_degree = G.degree
+        target_degree = G.degree
+        neighbors = G.neighbors
+        reverse = False
+    dsum = defaultdict(int)
+    dnorm = defaultdict(int)
+    # Check if `source_nodes` is actually a single node in the graph.
+    source_nodes = source_degree(nodes)
+    if nodes in G:
+        source_nodes = [(nodes, source_degree(nodes))]
+    for n, k in source_nodes:
+        nbrdeg = target_degree(neighbors(n))
+        if weight is None:
+            s = sum(d for n, d in nbrdeg)
+        else:  # weight nbr degree by weight of (n,nbr) edge
+            if reverse:
+                s = sum(G[nbr][n].get(weight, 1) * d for nbr, d in nbrdeg)
+            else:
+                s = sum(G[n][nbr].get(weight, 1) * d for nbr, d in nbrdeg)
+        dnorm[k] += source_degree(n, weight=weight)
+        dsum[k] += s
+
+    # normalize
+    return {k: avg if dnorm[k] == 0 else avg / dnorm[k] for k, avg in dsum.items()}
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py
new file mode 100644
index 00000000..52ae7a12
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py
@@ -0,0 +1,302 @@
+"""Node assortativity coefficients and correlation measures."""
+
+import networkx as nx
+from networkx.algorithms.assortativity.mixing import (
+    attribute_mixing_matrix,
+    degree_mixing_matrix,
+)
+from networkx.algorithms.assortativity.pairs import node_degree_xy
+
+__all__ = [
+    "degree_pearson_correlation_coefficient",
+    "degree_assortativity_coefficient",
+    "attribute_assortativity_coefficient",
+    "numeric_assortativity_coefficient",
+]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def degree_assortativity_coefficient(G, x="out", y="in", weight=None, nodes=None):
+    """Compute degree assortativity of graph.
+
+    Assortativity measures the similarity of connections
+    in the graph with respect to the node degree.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    x: string ('in','out')
+       The degree type for source node (directed graphs only).
+
+    y: string ('in','out')
+       The degree type for target node (directed graphs only).
+
+    weight: string or None, optional (default=None)
+       The edge attribute that holds the numerical value used
+       as a weight.  If None, then each edge has weight 1.
+       The degree is the sum of the edge weights adjacent to the node.
+
+    nodes: list or iterable (optional)
+        Compute degree assortativity only for nodes in container.
+        The default is all nodes.
+
+    Returns
+    -------
+    r : float
+       Assortativity of graph by degree.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> r = nx.degree_assortativity_coefficient(G)
+    >>> print(f"{r:3.1f}")
+    -0.5
+
+    See Also
+    --------
+    attribute_assortativity_coefficient
+    numeric_assortativity_coefficient
+    degree_mixing_dict
+    degree_mixing_matrix
+
+    Notes
+    -----
+    This computes Eq. (21) in Ref. [1]_ , where e is the joint
+    probability distribution (mixing matrix) of the degrees.  If G is
+    directed than the matrix e is the joint probability of the
+    user-specified degree type for the source and target.
+
+    References
+    ----------
+    .. [1] M. E. J. Newman, Mixing patterns in networks,
+       Physical Review E, 67 026126, 2003
+    .. [2] Foster, J.G., Foster, D.V., Grassberger, P. & Paczuski, M.
+       Edge direction and the structure of networks, PNAS 107, 10815-20 (2010).
+    """
+    if nodes is None:
+        nodes = G.nodes
+
+    degrees = None
+
+    if G.is_directed():
+        indeg = (
+            {d for _, d in G.in_degree(nodes, weight=weight)}
+            if "in" in (x, y)
+            else set()
+        )
+        outdeg = (
+            {d for _, d in G.out_degree(nodes, weight=weight)}
+            if "out" in (x, y)
+            else set()
+        )
+        degrees = set.union(indeg, outdeg)
+    else:
+        degrees = {d for _, d in G.degree(nodes, weight=weight)}
+
+    mapping = {d: i for i, d in enumerate(degrees)}
+    M = degree_mixing_matrix(G, x=x, y=y, nodes=nodes, weight=weight, mapping=mapping)
+
+    return _numeric_ac(M, mapping=mapping)
+
+
+@nx._dispatchable(edge_attrs="weight")
+def degree_pearson_correlation_coefficient(G, x="out", y="in", weight=None, nodes=None):
+    """Compute degree assortativity of graph.
+
+    Assortativity measures the similarity of connections
+    in the graph with respect to the node degree.
+
+    This is the same as degree_assortativity_coefficient but uses the
+    potentially faster scipy.stats.pearsonr function.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    x: string ('in','out')
+       The degree type for source node (directed graphs only).
+
+    y: string ('in','out')
+       The degree type for target node (directed graphs only).
+
+    weight: string or None, optional (default=None)
+       The edge attribute that holds the numerical value used
+       as a weight.  If None, then each edge has weight 1.
+       The degree is the sum of the edge weights adjacent to the node.
+
+    nodes: list or iterable (optional)
+        Compute pearson correlation of degrees only for specified nodes.
+        The default is all nodes.
+
+    Returns
+    -------
+    r : float
+       Assortativity of graph by degree.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> r = nx.degree_pearson_correlation_coefficient(G)
+    >>> print(f"{r:3.1f}")
+    -0.5
+
+    Notes
+    -----
+    This calls scipy.stats.pearsonr.
+
+    References
+    ----------
+    .. [1] M. E. J. Newman, Mixing patterns in networks
+           Physical Review E, 67 026126, 2003
+    .. [2] Foster, J.G., Foster, D.V., Grassberger, P. & Paczuski, M.
+       Edge direction and the structure of networks, PNAS 107, 10815-20 (2010).
+    """
+    import scipy as sp
+
+    xy = node_degree_xy(G, x=x, y=y, nodes=nodes, weight=weight)
+    x, y = zip(*xy)
+    return float(sp.stats.pearsonr(x, y)[0])
+
+
+@nx._dispatchable(node_attrs="attribute")
+def attribute_assortativity_coefficient(G, attribute, nodes=None):
+    """Compute assortativity for node attributes.
+
+    Assortativity measures the similarity of connections
+    in the graph with respect to the given attribute.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    attribute : string
+        Node attribute key
+
+    nodes: list or iterable (optional)
+        Compute attribute assortativity for nodes in container.
+        The default is all nodes.
+
+    Returns
+    -------
+    r: float
+       Assortativity of graph for given attribute
+
+    Examples
+    --------
+    >>> G = nx.Graph()
+    >>> G.add_nodes_from([0, 1], color="red")
+    >>> G.add_nodes_from([2, 3], color="blue")
+    >>> G.add_edges_from([(0, 1), (2, 3)])
+    >>> print(nx.attribute_assortativity_coefficient(G, "color"))
+    1.0
+
+    Notes
+    -----
+    This computes Eq. (2) in Ref. [1]_ , (trace(M)-sum(M^2))/(1-sum(M^2)),
+    where M is the joint probability distribution (mixing matrix)
+    of the specified attribute.
+
+    References
+    ----------
+    .. [1] M. E. J. Newman, Mixing patterns in networks,
+       Physical Review E, 67 026126, 2003
+    """
+    M = attribute_mixing_matrix(G, attribute, nodes)
+    return attribute_ac(M)
+
+
+@nx._dispatchable(node_attrs="attribute")
+def numeric_assortativity_coefficient(G, attribute, nodes=None):
+    """Compute assortativity for numerical node attributes.
+
+    Assortativity measures the similarity of connections
+    in the graph with respect to the given numeric attribute.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    attribute : string
+        Node attribute key.
+
+    nodes: list or iterable (optional)
+        Compute numeric assortativity only for attributes of nodes in
+        container. The default is all nodes.
+
+    Returns
+    -------
+    r: float
+       Assortativity of graph for given attribute
+
+    Examples
+    --------
+    >>> G = nx.Graph()
+    >>> G.add_nodes_from([0, 1], size=2)
+    >>> G.add_nodes_from([2, 3], size=3)
+    >>> G.add_edges_from([(0, 1), (2, 3)])
+    >>> print(nx.numeric_assortativity_coefficient(G, "size"))
+    1.0
+
+    Notes
+    -----
+    This computes Eq. (21) in Ref. [1]_ , which is the Pearson correlation
+    coefficient of the specified (scalar valued) attribute across edges.
+
+    References
+    ----------
+    .. [1] M. E. J. Newman, Mixing patterns in networks
+           Physical Review E, 67 026126, 2003
+    """
+    if nodes is None:
+        nodes = G.nodes
+    vals = {G.nodes[n][attribute] for n in nodes}
+    mapping = {d: i for i, d in enumerate(vals)}
+    M = attribute_mixing_matrix(G, attribute, nodes, mapping)
+    return _numeric_ac(M, mapping)
+
+
+def attribute_ac(M):
+    """Compute assortativity for attribute matrix M.
+
+    Parameters
+    ----------
+    M : numpy.ndarray
+        2D ndarray representing the attribute mixing matrix.
+
+    Notes
+    -----
+    This computes Eq. (2) in Ref. [1]_ , (trace(e)-sum(e^2))/(1-sum(e^2)),
+    where e is the joint probability distribution (mixing matrix)
+    of the specified attribute.
+
+    References
+    ----------
+    .. [1] M. E. J. Newman, Mixing patterns in networks,
+       Physical Review E, 67 026126, 2003
+    """
+    if M.sum() != 1.0:
+        M = M / M.sum()
+    s = (M @ M).sum()
+    t = M.trace()
+    r = (t - s) / (1 - s)
+    return float(r)
+
+
+def _numeric_ac(M, mapping):
+    # M is a 2D numpy array
+    # numeric assortativity coefficient, pearsonr
+    import numpy as np
+
+    if M.sum() != 1.0:
+        M = M / M.sum()
+    x = np.array(list(mapping.keys()))
+    y = x  # x and y have the same support
+    idx = list(mapping.values())
+    a = M.sum(axis=0)
+    b = M.sum(axis=1)
+    vara = (a[idx] * x**2).sum() - ((a[idx] * x).sum()) ** 2
+    varb = (b[idx] * y**2).sum() - ((b[idx] * y).sum()) ** 2
+    xy = np.outer(x, y)
+    ab = np.outer(a[idx], b[idx])
+    return float((xy * (M - ab)).sum() / np.sqrt(vara * varb))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py
new file mode 100644
index 00000000..1762d4e5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py
@@ -0,0 +1,255 @@
+"""
+Mixing matrices for node attributes and degree.
+"""
+
+import networkx as nx
+from networkx.algorithms.assortativity.pairs import node_attribute_xy, node_degree_xy
+from networkx.utils import dict_to_numpy_array
+
+__all__ = [
+    "attribute_mixing_matrix",
+    "attribute_mixing_dict",
+    "degree_mixing_matrix",
+    "degree_mixing_dict",
+    "mixing_dict",
+]
+
+
+@nx._dispatchable(node_attrs="attribute")
+def attribute_mixing_dict(G, attribute, nodes=None, normalized=False):
+    """Returns dictionary representation of mixing matrix for attribute.
+
+    Parameters
+    ----------
+    G : graph
+       NetworkX graph object.
+
+    attribute : string
+       Node attribute key.
+
+    nodes: list or iterable (optional)
+        Unse nodes in container to build the dict. The default is all nodes.
+
+    normalized : bool (default=False)
+       Return counts if False or probabilities if True.
+
+    Examples
+    --------
+    >>> G = nx.Graph()
+    >>> G.add_nodes_from([0, 1], color="red")
+    >>> G.add_nodes_from([2, 3], color="blue")
+    >>> G.add_edge(1, 3)
+    >>> d = nx.attribute_mixing_dict(G, "color")
+    >>> print(d["red"]["blue"])
+    1
+    >>> print(d["blue"]["red"])  # d symmetric for undirected graphs
+    1
+
+    Returns
+    -------
+    d : dictionary
+       Counts or joint probability of occurrence of attribute pairs.
+    """
+    xy_iter = node_attribute_xy(G, attribute, nodes)
+    return mixing_dict(xy_iter, normalized=normalized)
+
+
+@nx._dispatchable(node_attrs="attribute")
+def attribute_mixing_matrix(G, attribute, nodes=None, mapping=None, normalized=True):
+    """Returns mixing matrix for attribute.
+
+    Parameters
+    ----------
+    G : graph
+       NetworkX graph object.
+
+    attribute : string
+       Node attribute key.
+
+    nodes: list or iterable (optional)
+        Use only nodes in container to build the matrix. The default is
+        all nodes.
+
+    mapping : dictionary, optional
+       Mapping from node attribute to integer index in matrix.
+       If not specified, an arbitrary ordering will be used.
+
+    normalized : bool (default=True)
+       Return counts if False or probabilities if True.
+
+    Returns
+    -------
+    m: numpy array
+       Counts or joint probability of occurrence of attribute pairs.
+
+    Notes
+    -----
+    If each node has a unique attribute value, the unnormalized mixing matrix
+    will be equal to the adjacency matrix. To get a denser mixing matrix,
+    the rounding can be performed to form groups of nodes with equal values.
+    For example, the exact height of persons in cm (180.79155222, 163.9080892,
+    163.30095355, 167.99016217, 168.21590163, ...) can be rounded to (180, 163,
+    163, 168, 168, ...).
+
+    Definitions of attribute mixing matrix vary on whether the matrix
+    should include rows for attribute values that don't arise. Here we
+    do not include such empty-rows. But you can force them to appear
+    by inputting a `mapping` that includes those values.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(3)
+    >>> gender = {0: "male", 1: "female", 2: "female"}
+    >>> nx.set_node_attributes(G, gender, "gender")
+    >>> mapping = {"male": 0, "female": 1}
+    >>> mix_mat = nx.attribute_mixing_matrix(G, "gender", mapping=mapping)
+    >>> mix_mat
+    array([[0.  , 0.25],
+           [0.25, 0.5 ]])
+    """
+    d = attribute_mixing_dict(G, attribute, nodes)
+    a = dict_to_numpy_array(d, mapping=mapping)
+    if normalized:
+        a = a / a.sum()
+    return a
+
+
+@nx._dispatchable(edge_attrs="weight")
+def degree_mixing_dict(G, x="out", y="in", weight=None, nodes=None, normalized=False):
+    """Returns dictionary representation of mixing matrix for degree.
+
+    Parameters
+    ----------
+    G : graph
+        NetworkX graph object.
+
+    x: string ('in','out')
+       The degree type for source node (directed graphs only).
+
+    y: string ('in','out')
+       The degree type for target node (directed graphs only).
+
+    weight: string or None, optional (default=None)
+       The edge attribute that holds the numerical value used
+       as a weight.  If None, then each edge has weight 1.
+       The degree is the sum of the edge weights adjacent to the node.
+
+    normalized : bool (default=False)
+        Return counts if False or probabilities if True.
+
+    Returns
+    -------
+    d: dictionary
+       Counts or joint probability of occurrence of degree pairs.
+    """
+    xy_iter = node_degree_xy(G, x=x, y=y, nodes=nodes, weight=weight)
+    return mixing_dict(xy_iter, normalized=normalized)
+
+
+@nx._dispatchable(edge_attrs="weight")
+def degree_mixing_matrix(
+    G, x="out", y="in", weight=None, nodes=None, normalized=True, mapping=None
+):
+    """Returns mixing matrix for attribute.
+
+    Parameters
+    ----------
+    G : graph
+       NetworkX graph object.
+
+    x: string ('in','out')
+       The degree type for source node (directed graphs only).
+
+    y: string ('in','out')
+       The degree type for target node (directed graphs only).
+
+    nodes: list or iterable (optional)
+        Build the matrix using only nodes in container.
+        The default is all nodes.
+
+    weight: string or None, optional (default=None)
+       The edge attribute that holds the numerical value used
+       as a weight.  If None, then each edge has weight 1.
+       The degree is the sum of the edge weights adjacent to the node.
+
+    normalized : bool (default=True)
+       Return counts if False or probabilities if True.
+
+    mapping : dictionary, optional
+       Mapping from node degree to integer index in matrix.
+       If not specified, an arbitrary ordering will be used.
+
+    Returns
+    -------
+    m: numpy array
+       Counts, or joint probability, of occurrence of node degree.
+
+    Notes
+    -----
+    Definitions of degree mixing matrix vary on whether the matrix
+    should include rows for degree values that don't arise. Here we
+    do not include such empty-rows. But you can force them to appear
+    by inputting a `mapping` that includes those values. See examples.
+
+    Examples
+    --------
+    >>> G = nx.star_graph(3)
+    >>> mix_mat = nx.degree_mixing_matrix(G)
+    >>> mix_mat
+    array([[0. , 0.5],
+           [0.5, 0. ]])
+
+    If you want every possible degree to appear as a row, even if no nodes
+    have that degree, use `mapping` as follows,
+
+    >>> max_degree = max(deg for n, deg in G.degree)
+    >>> mapping = {x: x for x in range(max_degree + 1)}  # identity mapping
+    >>> mix_mat = nx.degree_mixing_matrix(G, mapping=mapping)
+    >>> mix_mat
+    array([[0. , 0. , 0. , 0. ],
+           [0. , 0. , 0. , 0.5],
+           [0. , 0. , 0. , 0. ],
+           [0. , 0.5, 0. , 0. ]])
+    """
+    d = degree_mixing_dict(G, x=x, y=y, nodes=nodes, weight=weight)
+    a = dict_to_numpy_array(d, mapping=mapping)
+    if normalized:
+        a = a / a.sum()
+    return a
+
+
+def mixing_dict(xy, normalized=False):
+    """Returns a dictionary representation of mixing matrix.
+
+    Parameters
+    ----------
+    xy : list or container of two-tuples
+       Pairs of (x,y) items.
+
+    attribute : string
+       Node attribute key
+
+    normalized : bool (default=False)
+       Return counts if False or probabilities if True.
+
+    Returns
+    -------
+    d: dictionary
+       Counts or Joint probability of occurrence of values in xy.
+    """
+    d = {}
+    psum = 0.0
+    for x, y in xy:
+        if x not in d:
+            d[x] = {}
+        if y not in d:
+            d[y] = {}
+        v = d[x].get(y, 0)
+        d[x][y] = v + 1
+        psum += 1
+
+    if normalized:
+        for _, jdict in d.items():
+            for j in jdict:
+                jdict[j] /= psum
+    return d
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py
new file mode 100644
index 00000000..6488d041
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py
@@ -0,0 +1,160 @@
+import networkx as nx
+
+__all__ = ["average_neighbor_degree"]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def average_neighbor_degree(G, source="out", target="out", nodes=None, weight=None):
+    r"""Returns the average degree of the neighborhood of each node.
+
+    In an undirected graph, the neighborhood `N(i)` of node `i` contains the
+    nodes that are connected to `i` by an edge.
+
+    For directed graphs, `N(i)` is defined according to the parameter `source`:
+
+        - if source is 'in', then `N(i)` consists of predecessors of node `i`.
+        - if source is 'out', then `N(i)` consists of successors of node `i`.
+        - if source is 'in+out', then `N(i)` is both predecessors and successors.
+
+    The average neighborhood degree of a node `i` is
+
+    .. math::
+
+        k_{nn,i} = \frac{1}{|N(i)|} \sum_{j \in N(i)} k_j
+
+    where `N(i)` are the neighbors of node `i` and `k_j` is
+    the degree of node `j` which belongs to `N(i)`. For weighted
+    graphs, an analogous measure can be defined [1]_,
+
+    .. math::
+
+        k_{nn,i}^{w} = \frac{1}{s_i} \sum_{j \in N(i)} w_{ij} k_j
+
+    where `s_i` is the weighted degree of node `i`, `w_{ij}`
+    is the weight of the edge that links `i` and `j` and
+    `N(i)` are the neighbors of node `i`.
+
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    source : string ("in"|"out"|"in+out"), optional (default="out")
+       Directed graphs only.
+       Use "in"- or "out"-neighbors of source node.
+
+    target : string ("in"|"out"|"in+out"), optional (default="out")
+       Directed graphs only.
+       Use "in"- or "out"-degree for target node.
+
+    nodes : list or iterable, optional (default=G.nodes)
+        Compute neighbor degree only for specified nodes.
+
+    weight : string or None, optional (default=None)
+       The edge attribute that holds the numerical value used as a weight.
+       If None, then each edge has weight 1.
+
+    Returns
+    -------
+    d: dict
+       A dictionary keyed by node to the average degree of its neighbors.
+
+    Raises
+    ------
+    NetworkXError
+        If either `source` or `target` are not one of 'in', 'out', or 'in+out'.
+        If either `source` or `target` is passed for an undirected graph.
+
+    Examples
+    --------
+    >>> G = nx.path_graph(4)
+    >>> G.edges[0, 1]["weight"] = 5
+    >>> G.edges[2, 3]["weight"] = 3
+
+    >>> nx.average_neighbor_degree(G)
+    {0: 2.0, 1: 1.5, 2: 1.5, 3: 2.0}
+    >>> nx.average_neighbor_degree(G, weight="weight")
+    {0: 2.0, 1: 1.1666666666666667, 2: 1.25, 3: 2.0}
+
+    >>> G = nx.DiGraph()
+    >>> nx.add_path(G, [0, 1, 2, 3])
+    >>> nx.average_neighbor_degree(G, source="in", target="in")
+    {0: 0.0, 1: 0.0, 2: 1.0, 3: 1.0}
+
+    >>> nx.average_neighbor_degree(G, source="out", target="out")
+    {0: 1.0, 1: 1.0, 2: 0.0, 3: 0.0}
+
+    See Also
+    --------
+    average_degree_connectivity
+
+    References
+    ----------
+    .. [1] A. Barrat, M. Barthélemy, R. Pastor-Satorras, and A. Vespignani,
+       "The architecture of complex weighted networks".
+       PNAS 101 (11): 3747–3752 (2004).
+    """
+    if G.is_directed():
+        if source == "in":
+            source_degree = G.in_degree
+        elif source == "out":
+            source_degree = G.out_degree
+        elif source == "in+out":
+            source_degree = G.degree
+        else:
+            raise nx.NetworkXError(
+                f"source argument {source} must be 'in', 'out' or 'in+out'"
+            )
+
+        if target == "in":
+            target_degree = G.in_degree
+        elif target == "out":
+            target_degree = G.out_degree
+        elif target == "in+out":
+            target_degree = G.degree
+        else:
+            raise nx.NetworkXError(
+                f"target argument {target} must be 'in', 'out' or 'in+out'"
+            )
+    else:
+        if source != "out" or target != "out":
+            raise nx.NetworkXError(
+                f"source and target arguments are only supported for directed graphs"
+            )
+        source_degree = target_degree = G.degree
+
+    # precompute target degrees -- should *not* be weighted degree
+    t_deg = dict(target_degree())
+
+    # Set up both predecessor and successor neighbor dicts leaving empty if not needed
+    G_P = G_S = {n: {} for n in G}
+    if G.is_directed():
+        # "in" or "in+out" cases: G_P contains predecessors
+        if "in" in source:
+            G_P = G.pred
+        # "out" or "in+out" cases: G_S contains successors
+        if "out" in source:
+            G_S = G.succ
+    else:
+        # undirected leave G_P empty but G_S is the adjacency
+        G_S = G.adj
+
+    # Main loop: Compute average degree of neighbors
+    avg = {}
+    for n, deg in source_degree(nodes, weight=weight):
+        # handle degree zero average
+        if deg == 0:
+            avg[n] = 0.0
+            continue
+
+        # we sum over both G_P and G_S, but one of the two is usually empty.
+        if weight is None:
+            avg[n] = (
+                sum(t_deg[nbr] for nbr in G_S[n]) + sum(t_deg[nbr] for nbr in G_P[n])
+            ) / deg
+        else:
+            avg[n] = (
+                sum(dd.get(weight, 1) * t_deg[nbr] for nbr, dd in G_S[n].items())
+                + sum(dd.get(weight, 1) * t_deg[nbr] for nbr, dd in G_P[n].items())
+            ) / deg
+    return avg
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py
new file mode 100644
index 00000000..ea5fd287
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py
@@ -0,0 +1,127 @@
+"""Generators of x-y pairs of node data."""
+
+import networkx as nx
+
+__all__ = ["node_attribute_xy", "node_degree_xy"]
+
+
+@nx._dispatchable(node_attrs="attribute")
+def node_attribute_xy(G, attribute, nodes=None):
+    """Yields 2-tuples of node attribute values for all edges in `G`.
+
+    This generator yields, for each edge in `G` incident to a node in `nodes`,
+    a 2-tuple of form ``(attribute value,  attribute value)`` for the parameter
+    specified node-attribute.
+
+    Parameters
+    ----------
+    G: NetworkX graph
+
+    attribute: key
+        The node attribute key.
+
+    nodes: list or iterable (optional)
+        Use only edges that are incident to specified nodes.
+        The default is all nodes.
+
+    Yields
+    ------
+    (x, y): 2-tuple
+        Generates 2-tuple of (attribute, attribute) values.
+
+    Examples
+    --------
+    >>> G = nx.DiGraph()
+    >>> G.add_node(1, color="red")
+    >>> G.add_node(2, color="blue")
+    >>> G.add_node(3, color="green")
+    >>> G.add_edge(1, 2)
+    >>> list(nx.node_attribute_xy(G, "color"))
+    [('red', 'blue')]
+
+    Notes
+    -----
+    For undirected graphs, each edge is produced twice, once for each edge
+    representation (u, v) and (v, u), with the exception of self-loop edges
+    which only appear once.
+    """
+    if nodes is None:
+        nodes = set(G)
+    else:
+        nodes = set(nodes)
+    Gnodes = G.nodes
+    for u, nbrsdict in G.adjacency():
+        if u not in nodes:
+            continue
+        uattr = Gnodes[u].get(attribute, None)
+        if G.is_multigraph():
+            for v, keys in nbrsdict.items():
+                vattr = Gnodes[v].get(attribute, None)
+                for _ in keys:
+                    yield (uattr, vattr)
+        else:
+            for v in nbrsdict:
+                vattr = Gnodes[v].get(attribute, None)
+                yield (uattr, vattr)
+
+
+@nx._dispatchable(edge_attrs="weight")
+def node_degree_xy(G, x="out", y="in", weight=None, nodes=None):
+    """Yields 2-tuples of ``(degree, degree)`` values for edges in `G`.
+
+    This generator yields, for each edge in `G` incident to a node in `nodes`,
+    a 2-tuple of form ``(degree, degree)``. The node degrees are weighted
+    when a `weight` attribute is specified.
+
+    Parameters
+    ----------
+    G: NetworkX graph
+
+    x: string ('in','out')
+       The degree type for source node (directed graphs only).
+
+    y: string ('in','out')
+       The degree type for target node (directed graphs only).
+
+    weight: string or None, optional (default=None)
+       The edge attribute that holds the numerical value used
+       as a weight.  If None, then each edge has weight 1.
+       The degree is the sum of the edge weights adjacent to the node.
+
+    nodes: list or iterable (optional)
+        Use only edges that are adjacency to specified nodes.
+        The default is all nodes.
+
+    Yields
+    ------
+    (x, y): 2-tuple
+        Generates 2-tuple of (degree, degree) values.
+
+    Examples
+    --------
+    >>> G = nx.DiGraph()
+    >>> G.add_edge(1, 2)
+    >>> list(nx.node_degree_xy(G, x="out", y="in"))
+    [(1, 1)]
+    >>> list(nx.node_degree_xy(G, x="in", y="out"))
+    [(0, 0)]
+
+    Notes
+    -----
+    For undirected graphs, each edge is produced twice, once for each edge
+    representation (u, v) and (v, u), with the exception of self-loop edges
+    which only appear once.
+    """
+    nodes = set(G) if nodes is None else set(nodes)
+    if G.is_directed():
+        direction = {"out": G.out_degree, "in": G.in_degree}
+        xdeg = direction[x]
+        ydeg = direction[y]
+    else:
+        xdeg = ydeg = G.degree
+
+    for u, degu in xdeg(nodes, weight=weight):
+        # use G.edges to treat multigraphs correctly
+        neighbors = (nbr for _, nbr in G.edges(u) if nbr in nodes)
+        for _, degv in ydeg(neighbors, weight=weight):
+            yield degu, degv
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/base_test.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/base_test.py
new file mode 100644
index 00000000..46d63006
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/base_test.py
@@ -0,0 +1,81 @@
+import networkx as nx
+
+
+class BaseTestAttributeMixing:
+    @classmethod
+    def setup_class(cls):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1], fish="one")
+        G.add_nodes_from([2, 3], fish="two")
+        G.add_nodes_from([4], fish="red")
+        G.add_nodes_from([5], fish="blue")
+        G.add_edges_from([(0, 1), (2, 3), (0, 4), (2, 5)])
+        cls.G = G
+
+        D = nx.DiGraph()
+        D.add_nodes_from([0, 1], fish="one")
+        D.add_nodes_from([2, 3], fish="two")
+        D.add_nodes_from([4], fish="red")
+        D.add_nodes_from([5], fish="blue")
+        D.add_edges_from([(0, 1), (2, 3), (0, 4), (2, 5)])
+        cls.D = D
+
+        M = nx.MultiGraph()
+        M.add_nodes_from([0, 1], fish="one")
+        M.add_nodes_from([2, 3], fish="two")
+        M.add_nodes_from([4], fish="red")
+        M.add_nodes_from([5], fish="blue")
+        M.add_edges_from([(0, 1), (0, 1), (2, 3)])
+        cls.M = M
+
+        S = nx.Graph()
+        S.add_nodes_from([0, 1], fish="one")
+        S.add_nodes_from([2, 3], fish="two")
+        S.add_nodes_from([4], fish="red")
+        S.add_nodes_from([5], fish="blue")
+        S.add_edge(0, 0)
+        S.add_edge(2, 2)
+        cls.S = S
+
+        N = nx.Graph()
+        N.add_nodes_from([0, 1], margin=-2)
+        N.add_nodes_from([2, 3], margin=-2)
+        N.add_nodes_from([4], margin=-3)
+        N.add_nodes_from([5], margin=-4)
+        N.add_edges_from([(0, 1), (2, 3), (0, 4), (2, 5)])
+        cls.N = N
+
+        F = nx.Graph()
+        F.add_edges_from([(0, 3), (1, 3), (2, 3)], weight=0.5)
+        F.add_edge(0, 2, weight=1)
+        nx.set_node_attributes(F, dict(F.degree(weight="weight")), "margin")
+        cls.F = F
+
+        K = nx.Graph()
+        K.add_nodes_from([1, 2], margin=-1)
+        K.add_nodes_from([3], margin=1)
+        K.add_nodes_from([4], margin=2)
+        K.add_edges_from([(3, 4), (1, 2), (1, 3)])
+        cls.K = K
+
+
+class BaseTestDegreeMixing:
+    @classmethod
+    def setup_class(cls):
+        cls.P4 = nx.path_graph(4)
+        cls.D = nx.DiGraph()
+        cls.D.add_edges_from([(0, 2), (0, 3), (1, 3), (2, 3)])
+        cls.D2 = nx.DiGraph()
+        cls.D2.add_edges_from([(0, 3), (1, 0), (1, 2), (2, 4), (4, 1), (4, 3), (4, 2)])
+        cls.M = nx.MultiGraph()
+        nx.add_path(cls.M, range(4))
+        cls.M.add_edge(0, 1)
+        cls.S = nx.Graph()
+        cls.S.add_edges_from([(0, 0), (1, 1)])
+        cls.W = nx.Graph()
+        cls.W.add_edges_from([(0, 3), (1, 3), (2, 3)], weight=0.5)
+        cls.W.add_edge(0, 2, weight=1)
+        S1 = nx.star_graph(4)
+        S2 = nx.star_graph(4)
+        cls.DS = nx.disjoint_union(S1, S2)
+        cls.DS.add_edge(4, 5)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_connectivity.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_connectivity.py
new file mode 100644
index 00000000..21c6287b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_connectivity.py
@@ -0,0 +1,143 @@
+from itertools import permutations
+
+import pytest
+
+import networkx as nx
+
+
+class TestNeighborConnectivity:
+    def test_degree_p4(self):
+        G = nx.path_graph(4)
+        answer = {1: 2.0, 2: 1.5}
+        nd = nx.average_degree_connectivity(G)
+        assert nd == answer
+
+        D = G.to_directed()
+        answer = {2: 2.0, 4: 1.5}
+        nd = nx.average_degree_connectivity(D)
+        assert nd == answer
+
+        answer = {1: 2.0, 2: 1.5}
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(D, source="in", target="in")
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(D, source="in", target="in")
+        assert nd == answer
+
+    def test_degree_p4_weighted(self):
+        G = nx.path_graph(4)
+        G[1][2]["weight"] = 4
+        answer = {1: 2.0, 2: 1.8}
+        nd = nx.average_degree_connectivity(G, weight="weight")
+        assert nd == answer
+        answer = {1: 2.0, 2: 1.5}
+        nd = nx.average_degree_connectivity(G)
+        assert nd == answer
+
+        D = G.to_directed()
+        answer = {2: 2.0, 4: 1.8}
+        nd = nx.average_degree_connectivity(D, weight="weight")
+        assert nd == answer
+
+        answer = {1: 2.0, 2: 1.8}
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(
+            D, weight="weight", source="in", target="in"
+        )
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(
+            D, source="in", target="out", weight="weight"
+        )
+        assert nd == answer
+
+    def test_weight_keyword(self):
+        G = nx.path_graph(4)
+        G[1][2]["other"] = 4
+        answer = {1: 2.0, 2: 1.8}
+        nd = nx.average_degree_connectivity(G, weight="other")
+        assert nd == answer
+        answer = {1: 2.0, 2: 1.5}
+        nd = nx.average_degree_connectivity(G, weight=None)
+        assert nd == answer
+
+        D = G.to_directed()
+        answer = {2: 2.0, 4: 1.8}
+        nd = nx.average_degree_connectivity(D, weight="other")
+        assert nd == answer
+
+        answer = {1: 2.0, 2: 1.8}
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(D, weight="other", source="in", target="in")
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_degree_connectivity(D, weight="other", source="in", target="in")
+        assert nd == answer
+
+    def test_degree_barrat(self):
+        G = nx.star_graph(5)
+        G.add_edges_from([(5, 6), (5, 7), (5, 8), (5, 9)])
+        G[0][5]["weight"] = 5
+        nd = nx.average_degree_connectivity(G)[5]
+        assert nd == 1.8
+        nd = nx.average_degree_connectivity(G, weight="weight")[5]
+        assert nd == pytest.approx(3.222222, abs=1e-5)
+
+    def test_zero_deg(self):
+        G = nx.DiGraph()
+        G.add_edge(1, 2)
+        G.add_edge(1, 3)
+        G.add_edge(1, 4)
+        c = nx.average_degree_connectivity(G)
+        assert c == {1: 0, 3: 1}
+        c = nx.average_degree_connectivity(G, source="in", target="in")
+        assert c == {0: 0, 1: 0}
+        c = nx.average_degree_connectivity(G, source="in", target="out")
+        assert c == {0: 0, 1: 3}
+        c = nx.average_degree_connectivity(G, source="in", target="in+out")
+        assert c == {0: 0, 1: 3}
+        c = nx.average_degree_connectivity(G, source="out", target="out")
+        assert c == {0: 0, 3: 0}
+        c = nx.average_degree_connectivity(G, source="out", target="in")
+        assert c == {0: 0, 3: 1}
+        c = nx.average_degree_connectivity(G, source="out", target="in+out")
+        assert c == {0: 0, 3: 1}
+
+    def test_in_out_weight(self):
+        G = nx.DiGraph()
+        G.add_edge(1, 2, weight=1)
+        G.add_edge(1, 3, weight=1)
+        G.add_edge(3, 1, weight=1)
+        for s, t in permutations(["in", "out", "in+out"], 2):
+            c = nx.average_degree_connectivity(G, source=s, target=t)
+            cw = nx.average_degree_connectivity(G, source=s, target=t, weight="weight")
+            assert c == cw
+
+    def test_invalid_source(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.DiGraph()
+            nx.average_degree_connectivity(G, source="bogus")
+
+    def test_invalid_target(self):
+        with pytest.raises(nx.NetworkXError):
+            G = nx.DiGraph()
+            nx.average_degree_connectivity(G, target="bogus")
+
+    def test_invalid_undirected_graph(self):
+        G = nx.Graph()
+        with pytest.raises(nx.NetworkXError):
+            nx.average_degree_connectivity(G, target="bogus")
+        with pytest.raises(nx.NetworkXError):
+            nx.average_degree_connectivity(G, source="bogus")
+
+    def test_single_node(self):
+        # TODO Is this really the intended behavior for providing a
+        # single node as the argument `nodes`? Shouldn't the function
+        # just return the connectivity value itself?
+        G = nx.trivial_graph()
+        conn = nx.average_degree_connectivity(G, nodes=0)
+        assert conn == {0: 0}
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_correlation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_correlation.py
new file mode 100644
index 00000000..5203f944
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_correlation.py
@@ -0,0 +1,123 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+
+import networkx as nx
+from networkx.algorithms.assortativity.correlation import attribute_ac
+
+from .base_test import BaseTestAttributeMixing, BaseTestDegreeMixing
+
+
+class TestDegreeMixingCorrelation(BaseTestDegreeMixing):
+    def test_degree_assortativity_undirected(self):
+        r = nx.degree_assortativity_coefficient(self.P4)
+        np.testing.assert_almost_equal(r, -1.0 / 2, decimal=4)
+
+    def test_degree_assortativity_node_kwargs(self):
+        G = nx.Graph()
+        edges = [(0, 1), (0, 3), (1, 2), (1, 3), (1, 4), (5, 9), (9, 0)]
+        G.add_edges_from(edges)
+        r = nx.degree_assortativity_coefficient(G, nodes=[1, 2, 4])
+        np.testing.assert_almost_equal(r, -1.0, decimal=4)
+
+    def test_degree_assortativity_directed(self):
+        r = nx.degree_assortativity_coefficient(self.D)
+        np.testing.assert_almost_equal(r, -0.57735, decimal=4)
+
+    def test_degree_assortativity_directed2(self):
+        """Test degree assortativity for a directed graph where the set of
+        in/out degree does not equal the total degree."""
+        r = nx.degree_assortativity_coefficient(self.D2)
+        np.testing.assert_almost_equal(r, 0.14852, decimal=4)
+
+    def test_degree_assortativity_multigraph(self):
+        r = nx.degree_assortativity_coefficient(self.M)
+        np.testing.assert_almost_equal(r, -1.0 / 7.0, decimal=4)
+
+    def test_degree_pearson_assortativity_undirected(self):
+        r = nx.degree_pearson_correlation_coefficient(self.P4)
+        np.testing.assert_almost_equal(r, -1.0 / 2, decimal=4)
+
+    def test_degree_pearson_assortativity_directed(self):
+        r = nx.degree_pearson_correlation_coefficient(self.D)
+        np.testing.assert_almost_equal(r, -0.57735, decimal=4)
+
+    def test_degree_pearson_assortativity_directed2(self):
+        """Test degree assortativity with Pearson for a directed graph where
+        the set of in/out degree does not equal the total degree."""
+        r = nx.degree_pearson_correlation_coefficient(self.D2)
+        np.testing.assert_almost_equal(r, 0.14852, decimal=4)
+
+    def test_degree_pearson_assortativity_multigraph(self):
+        r = nx.degree_pearson_correlation_coefficient(self.M)
+        np.testing.assert_almost_equal(r, -1.0 / 7.0, decimal=4)
+
+    def test_degree_assortativity_weighted(self):
+        r = nx.degree_assortativity_coefficient(self.W, weight="weight")
+        np.testing.assert_almost_equal(r, -0.1429, decimal=4)
+
+    def test_degree_assortativity_double_star(self):
+        r = nx.degree_assortativity_coefficient(self.DS)
+        np.testing.assert_almost_equal(r, -0.9339, decimal=4)
+
+
+class TestAttributeMixingCorrelation(BaseTestAttributeMixing):
+    def test_attribute_assortativity_undirected(self):
+        r = nx.attribute_assortativity_coefficient(self.G, "fish")
+        assert r == 6.0 / 22.0
+
+    def test_attribute_assortativity_directed(self):
+        r = nx.attribute_assortativity_coefficient(self.D, "fish")
+        assert r == 1.0 / 3.0
+
+    def test_attribute_assortativity_multigraph(self):
+        r = nx.attribute_assortativity_coefficient(self.M, "fish")
+        assert r == 1.0
+
+    def test_attribute_assortativity_coefficient(self):
+        # from "Mixing patterns in networks"
+        # fmt: off
+        a = np.array([[0.258, 0.016, 0.035, 0.013],
+                      [0.012, 0.157, 0.058, 0.019],
+                      [0.013, 0.023, 0.306, 0.035],
+                      [0.005, 0.007, 0.024, 0.016]])
+        # fmt: on
+        r = attribute_ac(a)
+        np.testing.assert_almost_equal(r, 0.623, decimal=3)
+
+    def test_attribute_assortativity_coefficient2(self):
+        # fmt: off
+        a = np.array([[0.18, 0.02, 0.01, 0.03],
+                      [0.02, 0.20, 0.03, 0.02],
+                      [0.01, 0.03, 0.16, 0.01],
+                      [0.03, 0.02, 0.01, 0.22]])
+        # fmt: on
+        r = attribute_ac(a)
+        np.testing.assert_almost_equal(r, 0.68, decimal=2)
+
+    def test_attribute_assortativity(self):
+        a = np.array([[50, 50, 0], [50, 50, 0], [0, 0, 2]])
+        r = attribute_ac(a)
+        np.testing.assert_almost_equal(r, 0.029, decimal=3)
+
+    def test_attribute_assortativity_negative(self):
+        r = nx.numeric_assortativity_coefficient(self.N, "margin")
+        np.testing.assert_almost_equal(r, -0.2903, decimal=4)
+
+    def test_assortativity_node_kwargs(self):
+        G = nx.Graph()
+        G.add_nodes_from([0, 1], size=2)
+        G.add_nodes_from([2, 3], size=3)
+        G.add_edges_from([(0, 1), (2, 3)])
+        r = nx.numeric_assortativity_coefficient(G, "size", nodes=[0, 3])
+        np.testing.assert_almost_equal(r, 1.0, decimal=4)
+
+    def test_attribute_assortativity_float(self):
+        r = nx.numeric_assortativity_coefficient(self.F, "margin")
+        np.testing.assert_almost_equal(r, -0.1429, decimal=4)
+
+    def test_attribute_assortativity_mixed(self):
+        r = nx.numeric_assortativity_coefficient(self.K, "margin")
+        np.testing.assert_almost_equal(r, 0.4340, decimal=4)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_mixing.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_mixing.py
new file mode 100644
index 00000000..9af09867
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_mixing.py
@@ -0,0 +1,176 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+
+
+import networkx as nx
+
+from .base_test import BaseTestAttributeMixing, BaseTestDegreeMixing
+
+
+class TestDegreeMixingDict(BaseTestDegreeMixing):
+    def test_degree_mixing_dict_undirected(self):
+        d = nx.degree_mixing_dict(self.P4)
+        d_result = {1: {2: 2}, 2: {1: 2, 2: 2}}
+        assert d == d_result
+
+    def test_degree_mixing_dict_undirected_normalized(self):
+        d = nx.degree_mixing_dict(self.P4, normalized=True)
+        d_result = {1: {2: 1.0 / 3}, 2: {1: 1.0 / 3, 2: 1.0 / 3}}
+        assert d == d_result
+
+    def test_degree_mixing_dict_directed(self):
+        d = nx.degree_mixing_dict(self.D)
+        print(d)
+        d_result = {1: {3: 2}, 2: {1: 1, 3: 1}, 3: {}}
+        assert d == d_result
+
+    def test_degree_mixing_dict_multigraph(self):
+        d = nx.degree_mixing_dict(self.M)
+        d_result = {1: {2: 1}, 2: {1: 1, 3: 3}, 3: {2: 3}}
+        assert d == d_result
+
+    def test_degree_mixing_dict_weighted(self):
+        d = nx.degree_mixing_dict(self.W, weight="weight")
+        d_result = {0.5: {1.5: 1}, 1.5: {1.5: 6, 0.5: 1}}
+        assert d == d_result
+
+
+class TestDegreeMixingMatrix(BaseTestDegreeMixing):
+    def test_degree_mixing_matrix_undirected(self):
+        # fmt: off
+        a_result = np.array([[0, 2],
+                             [2, 2]]
+                            )
+        # fmt: on
+        a = nx.degree_mixing_matrix(self.P4, normalized=False)
+        np.testing.assert_equal(a, a_result)
+        a = nx.degree_mixing_matrix(self.P4)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_degree_mixing_matrix_directed(self):
+        # fmt: off
+        a_result = np.array([[0, 0, 2],
+                             [1, 0, 1],
+                             [0, 0, 0]]
+                            )
+        # fmt: on
+        a = nx.degree_mixing_matrix(self.D, normalized=False)
+        np.testing.assert_equal(a, a_result)
+        a = nx.degree_mixing_matrix(self.D)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_degree_mixing_matrix_multigraph(self):
+        # fmt: off
+        a_result = np.array([[0, 1, 0],
+                             [1, 0, 3],
+                             [0, 3, 0]]
+                            )
+        # fmt: on
+        a = nx.degree_mixing_matrix(self.M, normalized=False)
+        np.testing.assert_equal(a, a_result)
+        a = nx.degree_mixing_matrix(self.M)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_degree_mixing_matrix_selfloop(self):
+        # fmt: off
+        a_result = np.array([[2]])
+        # fmt: on
+        a = nx.degree_mixing_matrix(self.S, normalized=False)
+        np.testing.assert_equal(a, a_result)
+        a = nx.degree_mixing_matrix(self.S)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_degree_mixing_matrix_weighted(self):
+        a_result = np.array([[0.0, 1.0], [1.0, 6.0]])
+        a = nx.degree_mixing_matrix(self.W, weight="weight", normalized=False)
+        np.testing.assert_equal(a, a_result)
+        a = nx.degree_mixing_matrix(self.W, weight="weight")
+        np.testing.assert_equal(a, a_result / float(a_result.sum()))
+
+    def test_degree_mixing_matrix_mapping(self):
+        a_result = np.array([[6.0, 1.0], [1.0, 0.0]])
+        mapping = {0.5: 1, 1.5: 0}
+        a = nx.degree_mixing_matrix(
+            self.W, weight="weight", normalized=False, mapping=mapping
+        )
+        np.testing.assert_equal(a, a_result)
+
+
+class TestAttributeMixingDict(BaseTestAttributeMixing):
+    def test_attribute_mixing_dict_undirected(self):
+        d = nx.attribute_mixing_dict(self.G, "fish")
+        d_result = {
+            "one": {"one": 2, "red": 1},
+            "two": {"two": 2, "blue": 1},
+            "red": {"one": 1},
+            "blue": {"two": 1},
+        }
+        assert d == d_result
+
+    def test_attribute_mixing_dict_directed(self):
+        d = nx.attribute_mixing_dict(self.D, "fish")
+        d_result = {
+            "one": {"one": 1, "red": 1},
+            "two": {"two": 1, "blue": 1},
+            "red": {},
+            "blue": {},
+        }
+        assert d == d_result
+
+    def test_attribute_mixing_dict_multigraph(self):
+        d = nx.attribute_mixing_dict(self.M, "fish")
+        d_result = {"one": {"one": 4}, "two": {"two": 2}}
+        assert d == d_result
+
+
+class TestAttributeMixingMatrix(BaseTestAttributeMixing):
+    def test_attribute_mixing_matrix_undirected(self):
+        mapping = {"one": 0, "two": 1, "red": 2, "blue": 3}
+        a_result = np.array([[2, 0, 1, 0], [0, 2, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]])
+        a = nx.attribute_mixing_matrix(
+            self.G, "fish", mapping=mapping, normalized=False
+        )
+        np.testing.assert_equal(a, a_result)
+        a = nx.attribute_mixing_matrix(self.G, "fish", mapping=mapping)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_attribute_mixing_matrix_directed(self):
+        mapping = {"one": 0, "two": 1, "red": 2, "blue": 3}
+        a_result = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]])
+        a = nx.attribute_mixing_matrix(
+            self.D, "fish", mapping=mapping, normalized=False
+        )
+        np.testing.assert_equal(a, a_result)
+        a = nx.attribute_mixing_matrix(self.D, "fish", mapping=mapping)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_attribute_mixing_matrix_multigraph(self):
+        mapping = {"one": 0, "two": 1, "red": 2, "blue": 3}
+        a_result = np.array([[4, 0, 0, 0], [0, 2, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
+        a = nx.attribute_mixing_matrix(
+            self.M, "fish", mapping=mapping, normalized=False
+        )
+        np.testing.assert_equal(a, a_result)
+        a = nx.attribute_mixing_matrix(self.M, "fish", mapping=mapping)
+        np.testing.assert_equal(a, a_result / a_result.sum())
+
+    def test_attribute_mixing_matrix_negative(self):
+        mapping = {-2: 0, -3: 1, -4: 2}
+        a_result = np.array([[4.0, 1.0, 1.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
+        a = nx.attribute_mixing_matrix(
+            self.N, "margin", mapping=mapping, normalized=False
+        )
+        np.testing.assert_equal(a, a_result)
+        a = nx.attribute_mixing_matrix(self.N, "margin", mapping=mapping)
+        np.testing.assert_equal(a, a_result / float(a_result.sum()))
+
+    def test_attribute_mixing_matrix_float(self):
+        mapping = {0.5: 1, 1.5: 0}
+        a_result = np.array([[6.0, 1.0], [1.0, 0.0]])
+        a = nx.attribute_mixing_matrix(
+            self.F, "margin", mapping=mapping, normalized=False
+        )
+        np.testing.assert_equal(a, a_result)
+        a = nx.attribute_mixing_matrix(self.F, "margin", mapping=mapping)
+        np.testing.assert_equal(a, a_result / a_result.sum())
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_neighbor_degree.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_neighbor_degree.py
new file mode 100644
index 00000000..bf1252d5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_neighbor_degree.py
@@ -0,0 +1,108 @@
+import pytest
+
+import networkx as nx
+
+
+class TestAverageNeighbor:
+    def test_degree_p4(self):
+        G = nx.path_graph(4)
+        answer = {0: 2, 1: 1.5, 2: 1.5, 3: 2}
+        nd = nx.average_neighbor_degree(G)
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D)
+        assert nd == answer
+
+        D = nx.DiGraph(G.edges(data=True))
+        nd = nx.average_neighbor_degree(D)
+        assert nd == {0: 1, 1: 1, 2: 0, 3: 0}
+        nd = nx.average_neighbor_degree(D, "in", "out")
+        assert nd == {0: 0, 1: 1, 2: 1, 3: 1}
+        nd = nx.average_neighbor_degree(D, "out", "in")
+        assert nd == {0: 1, 1: 1, 2: 1, 3: 0}
+        nd = nx.average_neighbor_degree(D, "in", "in")
+        assert nd == {0: 0, 1: 0, 2: 1, 3: 1}
+
+    def test_degree_p4_weighted(self):
+        G = nx.path_graph(4)
+        G[1][2]["weight"] = 4
+        answer = {0: 2, 1: 1.8, 2: 1.8, 3: 2}
+        nd = nx.average_neighbor_degree(G, weight="weight")
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D, weight="weight")
+        assert nd == answer
+
+        D = nx.DiGraph(G.edges(data=True))
+        print(D.edges(data=True))
+        nd = nx.average_neighbor_degree(D, weight="weight")
+        assert nd == {0: 1, 1: 1, 2: 0, 3: 0}
+        nd = nx.average_neighbor_degree(D, "out", "out", weight="weight")
+        assert nd == {0: 1, 1: 1, 2: 0, 3: 0}
+        nd = nx.average_neighbor_degree(D, "in", "in", weight="weight")
+        assert nd == {0: 0, 1: 0, 2: 1, 3: 1}
+        nd = nx.average_neighbor_degree(D, "in", "out", weight="weight")
+        assert nd == {0: 0, 1: 1, 2: 1, 3: 1}
+        nd = nx.average_neighbor_degree(D, "out", "in", weight="weight")
+        assert nd == {0: 1, 1: 1, 2: 1, 3: 0}
+        nd = nx.average_neighbor_degree(D, source="in+out", weight="weight")
+        assert nd == {0: 1.0, 1: 1.0, 2: 0.8, 3: 1.0}
+        nd = nx.average_neighbor_degree(D, target="in+out", weight="weight")
+        assert nd == {0: 2.0, 1: 2.0, 2: 1.0, 3: 0.0}
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D, weight="weight")
+        assert nd == answer
+        nd = nx.average_neighbor_degree(D, source="out", target="out", weight="weight")
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D, source="in", target="in", weight="weight")
+        assert nd == answer
+
+    def test_degree_k4(self):
+        G = nx.complete_graph(4)
+        answer = {0: 3, 1: 3, 2: 3, 3: 3}
+        nd = nx.average_neighbor_degree(G)
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D)
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D)
+        assert nd == answer
+
+        D = G.to_directed()
+        nd = nx.average_neighbor_degree(D, source="in", target="in")
+        assert nd == answer
+
+    def test_degree_k4_nodes(self):
+        G = nx.complete_graph(4)
+        answer = {1: 3.0, 2: 3.0}
+        nd = nx.average_neighbor_degree(G, nodes=[1, 2])
+        assert nd == answer
+
+    def test_degree_barrat(self):
+        G = nx.star_graph(5)
+        G.add_edges_from([(5, 6), (5, 7), (5, 8), (5, 9)])
+        G[0][5]["weight"] = 5
+        nd = nx.average_neighbor_degree(G)[5]
+        assert nd == 1.8
+        nd = nx.average_neighbor_degree(G, weight="weight")[5]
+        assert nd == pytest.approx(3.222222, abs=1e-5)
+
+    def test_error_invalid_source_target(self):
+        G = nx.path_graph(4)
+        with pytest.raises(nx.NetworkXError):
+            nx.average_neighbor_degree(G, "error")
+        with pytest.raises(nx.NetworkXError):
+            nx.average_neighbor_degree(G, "in", "error")
+        G = G.to_directed()
+        with pytest.raises(nx.NetworkXError):
+            nx.average_neighbor_degree(G, "error")
+        with pytest.raises(nx.NetworkXError):
+            nx.average_neighbor_degree(G, "in", "error")
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_pairs.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_pairs.py
new file mode 100644
index 00000000..3984292b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/assortativity/tests/test_pairs.py
@@ -0,0 +1,87 @@
+import networkx as nx
+
+from .base_test import BaseTestAttributeMixing, BaseTestDegreeMixing
+
+
+class TestAttributeMixingXY(BaseTestAttributeMixing):
+    def test_node_attribute_xy_undirected(self):
+        attrxy = sorted(nx.node_attribute_xy(self.G, "fish"))
+        attrxy_result = sorted(
+            [
+                ("one", "one"),
+                ("one", "one"),
+                ("two", "two"),
+                ("two", "two"),
+                ("one", "red"),
+                ("red", "one"),
+                ("blue", "two"),
+                ("two", "blue"),
+            ]
+        )
+        assert attrxy == attrxy_result
+
+    def test_node_attribute_xy_undirected_nodes(self):
+        attrxy = sorted(nx.node_attribute_xy(self.G, "fish", nodes=["one", "yellow"]))
+        attrxy_result = sorted([])
+        assert attrxy == attrxy_result
+
+    def test_node_attribute_xy_directed(self):
+        attrxy = sorted(nx.node_attribute_xy(self.D, "fish"))
+        attrxy_result = sorted(
+            [("one", "one"), ("two", "two"), ("one", "red"), ("two", "blue")]
+        )
+        assert attrxy == attrxy_result
+
+    def test_node_attribute_xy_multigraph(self):
+        attrxy = sorted(nx.node_attribute_xy(self.M, "fish"))
+        attrxy_result = [
+            ("one", "one"),
+            ("one", "one"),
+            ("one", "one"),
+            ("one", "one"),
+            ("two", "two"),
+            ("two", "two"),
+        ]
+        assert attrxy == attrxy_result
+
+    def test_node_attribute_xy_selfloop(self):
+        attrxy = sorted(nx.node_attribute_xy(self.S, "fish"))
+        attrxy_result = [("one", "one"), ("two", "two")]
+        assert attrxy == attrxy_result
+
+
+class TestDegreeMixingXY(BaseTestDegreeMixing):
+    def test_node_degree_xy_undirected(self):
+        xy = sorted(nx.node_degree_xy(self.P4))
+        xy_result = sorted([(1, 2), (2, 1), (2, 2), (2, 2), (1, 2), (2, 1)])
+        assert xy == xy_result
+
+    def test_node_degree_xy_undirected_nodes(self):
+        xy = sorted(nx.node_degree_xy(self.P4, nodes=[0, 1, -1]))
+        xy_result = sorted([(1, 2), (2, 1)])
+        assert xy == xy_result
+
+    def test_node_degree_xy_directed(self):
+        xy = sorted(nx.node_degree_xy(self.D))
+        xy_result = sorted([(2, 1), (2, 3), (1, 3), (1, 3)])
+        assert xy == xy_result
+
+    def test_node_degree_xy_multigraph(self):
+        xy = sorted(nx.node_degree_xy(self.M))
+        xy_result = sorted(
+            [(2, 3), (2, 3), (3, 2), (3, 2), (2, 3), (3, 2), (1, 2), (2, 1)]
+        )
+        assert xy == xy_result
+
+    def test_node_degree_xy_selfloop(self):
+        xy = sorted(nx.node_degree_xy(self.S))
+        xy_result = sorted([(2, 2), (2, 2)])
+        assert xy == xy_result
+
+    def test_node_degree_xy_weighted(self):
+        G = nx.Graph()
+        G.add_edge(1, 2, weight=7)
+        G.add_edge(2, 3, weight=10)
+        xy = sorted(nx.node_degree_xy(G, weight="weight"))
+        xy_result = sorted([(7, 17), (17, 10), (17, 7), (10, 17)])
+        assert xy == xy_result