Skip to content

Commit

Permalink
Add assortativity module (#122)
Browse files Browse the repository at this point in the history
Added functions for computing the dynamical, top-2, top-bottom, and uniform degree assortativity.
  • Loading branch information
nwlandry authored Jun 14, 2022
1 parent 6484a95 commit 07e3ebd
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 21 deletions.
8 changes: 4 additions & 4 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,19 @@
(
master_doc,
"xgi.tex",
u"XGI Documentation",
u"Nicholas W. Landry and Leo Torres",
"XGI Documentation",
"Nicholas W. Landry and Leo Torres",
"manual",
),
]

man_pages = [(master_doc, "xgi", u"XGI Documentation", [author], 1)]
man_pages = [(master_doc, "xgi", "XGI Documentation", [author], 1)]

texinfo_documents = [
(
master_doc,
"XGI",
u"XGI Documentation",
"XGI Documentation",
author,
"XGI",
"One line description of project.",
Expand Down
2 changes: 1 addition & 1 deletion requirements/developer.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
black==21.5b1
black==22.3.0
pre-commit>=2.12
isort==5.10.1
pylint>=2.10
60 changes: 60 additions & 0 deletions tests/algorithms/test_assortativity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import numpy as np
import pytest

import xgi
from xgi.algorithms.assortativity import choose_degrees
from xgi.exception import XGIError


def test_dynamical_assortativity(edgelist1, edgelist6):

H = xgi.Hypergraph()
with pytest.raises(XGIError):
xgi.dynamical_assortativity(H)

H.add_nodes_from([0, 1, 2])

with pytest.raises(XGIError):
xgi.dynamical_assortativity(H)

with pytest.raises(XGIError):
H1 = xgi.Hypergraph(edgelist1)
xgi.dynamical_assortativity(H1)

H1 = xgi.Hypergraph(edgelist6)

assert abs(xgi.dynamical_assortativity(H1) - -0.0526) < 1e-3


def test_degree_assortativity(edgelist1, edgelist6):
H1 = xgi.Hypergraph(edgelist1)
assert -1 <= xgi.degree_assortativity(H1, kind="uniform") <= 1
assert -1 <= xgi.degree_assortativity(H1, kind="top-2") <= 1
assert -1 <= xgi.degree_assortativity(H1, kind="top-bottom") <= 1

H2 = xgi.Hypergraph(edgelist6)
assert -1 <= xgi.degree_assortativity(H2, kind="uniform") <= 1
assert -1 <= xgi.degree_assortativity(H2, kind="top-2") <= 1
assert -1 <= xgi.degree_assortativity(H2, kind="top-bottom") <= 1


def test_choose_degrees(edgelist1, edgelist6):
H1 = xgi.Hypergraph(edgelist1)
k = H1.degree()

with pytest.raises(XGIError):
e = H1.edges.members(1)
choose_degrees(e, k)

e = H1.edges.members(0)
assert np.all(np.array(choose_degrees(e, k)) == 1)

e = H1.edges.members(3)
assert set(choose_degrees(e, k, kind="top-2")) == {1, 2}
assert set(choose_degrees(e, k, kind="top-bottom")) == {1, 2}

H2 = xgi.Hypergraph(edgelist6)
e = H2.edges.members(2)
k = H2.degree()
assert set(choose_degrees(e, k, kind="top-2")) == {2, 3}
assert set(choose_degrees(e, k, kind="top-bottom")) == {1, 3}
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import xgi
import networkx as nx
import numpy as np
import pandas as pd
import pytest

import xgi


@pytest.fixture
def edgelist1():
Expand Down
3 changes: 2 additions & 1 deletion tests/stats/test_edgestats.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import pandas as pd
import pytest

import xgi
from xgi.exception import IDNotFound

Expand Down
3 changes: 2 additions & 1 deletion tests/stats/test_nodestats.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import pandas as pd
import pytest

import xgi
from xgi.exception import IDNotFound

Expand Down
4 changes: 2 additions & 2 deletions xgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
generators,
linalg,
readwrite,
utils,
stats,
utils,
)
from .algorithms import *
from .classes import *
Expand All @@ -18,7 +18,7 @@
from .generators import *
from .linalg import *
from .readwrite import *
from .utils import *
from .stats import *
from .utils import *

__version__ = pkg_resources.require("xgi")[0].version
3 changes: 2 additions & 1 deletion xgi/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import connected
from . import assortativity, connected
from .assortativity import *
from .connected import *
155 changes: 155 additions & 0 deletions xgi/algorithms/assortativity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import random
from itertools import combinations

import numpy as np

import xgi
from xgi.exception import XGIError

__all__ = ["dynamical_assortativity", "degree_assortativity"]


def dynamical_assortativity(H):
"""Computes the dynamical assortativity of a uniform hypergraph.
Parameters
----------
H : xgi.Hypergraph
Hypergraph of interest
Returns
-------
float
The dynamical assortativity
Raises
------
XGIError
If the hypergraph is not uniform, or if there are no nodes
or no edges
References
----------
Nicholas Landry and Juan G. Restrepo,
Hypergraph assortativity: A dynamical systems perspective,
Chaos 2022.
DOI: 10.1063/5.0086905
"""
if not xgi.is_uniform(H):
raise XGIError("Hypergraph must be uniform!")

if H.num_nodes == 0 or H.num_edges == 0:
raise XGIError("Hypergraph must contain nodes and edges!")

degs = H.degree()
k1 = np.mean(list(degs.values()))
k2 = np.mean(np.power(list(degs.values()), 2))
kk1 = np.mean(
[
degs[n1] * degs[n2]
for e in H.edges
for n1, n2 in combinations(H.edges.members(e), 2)
]
)

return kk1 * k1**2 / k2**2 - 1


def degree_assortativity(H, kind="uniform", exact=False, num_samples=1000):
"""Computes the degree assortativity of a hypergraph
Parameters
----------
H : Hypergraph
The hypergraph of interest
kind : str, default: "uniform"
the type of degree assortativity. valid choices are
"uniform", "top-2", and "top-bottom".
exact : bool, default: False
whether to compute over all edges or
sample randomly from the set of edges
num_samples : int, default: 1000
if not exact, specify the number of samples for the computation.
Returns
-------
float
the degree assortativity
References
----------
Phil Chodrow,
Configuration models of random hypergraphs,
Journal of Complex Networks 2020.
DOI: 10.1093/comnet/cnaa018
"""
degs = H.degree()
if exact:
k1k2 = [
choose_degrees(H.edges.members(e), degs, kind)
for e in H.edges
if len(H.edges.members(e)) > 1
]
else:
edges = [e for e in H.edges if len(H.edges.members(e)) > 1]
k1k2 = [
choose_degrees(H.edges.members(random.choice(edges)), degs, kind)
for _ in range(num_samples)
]
return np.corrcoef(np.array(k1k2).T)[0, 1]


def choose_degrees(e, k, kind="uniform"):
"""Choose the degrees of two nodes in a hyperedge.
Parameters
----------
e : iterable
the members in a hyperedge
k : dict
the degrees where keys are node IDs and values are degrees
kind : str, default: "uniform"
the type of degree assortativity, options are
"uniform", "top-2", and "top-bottom".
Returns
-------
tuple
two degrees selected from the edge
Raises
------
XGIError
if invalid assortativity function chosen
References
----------
Phil Chodrow,
Configuration models of random hypergraphs,
Journal of Complex Networks 2020.
DOI: 10.1093/comnet/cnaa018
"""
if len(e) > 1:
if kind == "uniform":
i = np.random.randint(len(e))
j = i
while i == j:
j = np.random.randint(len(e))
return (k[e[i]], k[e[j]])

elif kind == "top-2":
degs = sorted([k[i] for i in e])[-2:]
random.shuffle(degs)
return degs

elif kind == "top-bottom":
# this selects the largest and smallest degrees in one line
degs = sorted([k[i] for i in e])[:: len(e) - 1]
random.shuffle(degs)
return degs

else:
raise XGIError("Invalid choice function!")
else:
raise XGIError("Edge must have more than one member!")
4 changes: 1 addition & 3 deletions xgi/classes/reportviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
"""
from collections.abc import Mapping, Set

import numpy as np

from xgi.stats import NodeStatDispatcher, EdgeStatDispatcher
from xgi.exception import IDNotFound, XGIError
from xgi.stats import EdgeStatDispatcher, NodeStatDispatcher

__all__ = [
"NodeView",
Expand Down
4 changes: 2 additions & 2 deletions xgi/generators/nonuniform.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import numpy as np

from ..classes import SimplicialComplex
from ..utils import py_random_state, np_random_state
from .classic import empty_hypergraph, empty_simplicial_complex
from ..utils import np_random_state, py_random_state
from .classic import empty_hypergraph, lattice

__all__ = [
"chung_lu_hypergraph",
Expand Down
9 changes: 4 additions & 5 deletions xgi/stats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,15 @@
"""

from collections import defaultdict
from typing import Callable

import numpy as np
import pandas as pd
from typing import Callable
from collections import defaultdict

from xgi.exception import IDNotFound

from . import nodestats
from . import edgestats

from . import edgestats, nodestats

__all__ = ["nodestat_func", "edgestat_func", "EdgeStatDispatcher", "NodeStatDispatcher"]

Expand Down

0 comments on commit 07e3ebd

Please sign in to comment.