Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add assortativity module #122

Merged
merged 19 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
61 changes: 61 additions & 0 deletions tests/algorithms/test_assortativity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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
4 changes: 3 additions & 1 deletion xgi/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from . import connected
from . import assortativity, connected

from .assortativity import *
from .connected import *
152 changes: 152 additions & 0 deletions xgi/algorithms/assortativity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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]
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
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