Skip to content

Commit

Permalink
Merge 5283faa into 7be0d8f
Browse files Browse the repository at this point in the history
  • Loading branch information
romain-fontugne committed Jul 21, 2017
2 parents 7be0d8f + 5283faa commit f53567a
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 0 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Expand Up @@ -114,6 +114,7 @@ is partially historical, and now, mostly arbitrary.
- Niels van Adrichem, GitHub: `NvanAdrichem <https://github.com/NvanAdrichem>`_
- Michael E. Rose, GitHub: `Michael-E-Rose <https://github.com/Michael-E-Rose>`_
- Jarrod Millman, GitHub: `jarrodmillman <https://github.com/jarrodmillman>`_
- Romain Fontugne

Support
-------
Expand Down
1 change: 1 addition & 0 deletions networkx/algorithms/__init__.py
Expand Up @@ -34,6 +34,7 @@
from networkx.algorithms.richclub import *
from networkx.algorithms.shortest_paths import *
from networkx.algorithms.simple_paths import *
from networkx.algorithms.smallworld import *
from networkx.algorithms.smetric import *
from networkx.algorithms.structuralholes import *
from networkx.algorithms.triads import *
Expand Down
323 changes: 323 additions & 0 deletions networkx/algorithms/smallworld.py
@@ -0,0 +1,323 @@
# Copyright (C) 2017 by
# Romain Fontugne <romain@iij.ad.jp>
# All rights reserved.
# BSD license.
"""Functions for estimating the small-world-ness of graphs.
A small world network is characterized by a small average shortest path length,
and a large clustering coefficient.
Small-worldness is commonly measured with the coefficient sigma or omega. Both
coefficients compare the average clustering coefficient and shortest path
length of a given graph against the same quantities for an equivalent random
or lattice graph; for more information,
see the Wikipedia article on small-world network [1]_.
.. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network
"""
import networkx as nx
import numpy as np
import random
__author__ = """Romain Fontugne (romain@iij.ad.jp)"""
__all__ = ['random_reference','lattice_reference','sigma', 'omega']


def random_reference(G, niter=10):
"""Compute a random graph by rewiring edges of the given graph while
keeping the same degree distribution.
Parameters
----------
G : graph
An undirected graph
niter : integer (optional, default=1)
An edge is rewired approximatively niter times.
Returns
-------
G : graph
The randomized graph.
Notes
-----
Does not enforce any connectivity constraints.
The implementation is adapted from the algorithm by Maslov and Sneppen
(2002) [1]_.
References
----------
.. [1] Maslov, Sergei, and Kim Sneppen. "Specificity and stability in
topology of protein networks." Science 296.5569 (2002): 910-913.
"""
if G.is_directed():
raise nx.NetworkXError(\
"random_reference() not defined for directed graphs.")
if len(G) < 4:
raise nx.NetworkXError("Graph has less than four nodes.")

G = G.copy()
keys,degrees = zip(*G.degree()) # keys, degree
cdf=nx.utils.cumulative_distribution(degrees) # cdf of degree
nnodes = len(G)
nedges = nx.number_of_edges(G)
niter = niter*nedges
ntries = int(nnodes*nedges/(nnodes*(nnodes-1)/2))
swapcount = 0

for i in range(niter):
n=0
while n < ntries:
# pick two random edges without creating edge list
# choose source node indices from discrete distribution
(ai,ci)=nx.utils.discrete_sequence(2,cdistribution=cdf)
if ai==ci:
continue # same source, skip
a=keys[ai] # convert index to label
c=keys[ci]
# choose target uniformly from neighbors
b = random.choice(list(G.neighbors(a)))
d = random.choice(list(G.neighbors(c)))
bi = keys.index(b)
di = keys.index(d)
if b in [a,c,d] or d in [a,b,c]:
continue # all vertices should be different

if (d not in G[a]) and (b not in G[c]): # don't create parallel edges
G.add_edge(a,d)
G.add_edge(c,b)
G.remove_edge(a,b)
G.remove_edge(c,d)
swapcount+=1
break
n+=1
return G

def lattice_reference(G, niter=10, D=None):
"""Latticize the given graph by rewiring edges while keeping the same
degree distribution.
Parameters
----------
G : graph
An undirected graph
niter : integer (optional, default=1)
An edge is rewired approximatively niter times.
D: numpy.array (optional, default=None)
Distance to the diagonal matrix.
Returns
-------
G : graph
The randomized graph.
Notes
-----
Does not enforce any connectivity constraints.
The implementation is adapted from the algorithm by Sporns et al. which is
inspired from the original work from Maslov and Sneppen (2002) [2]_.
References
----------
.. [1] Sporns, Olaf, and Jonathan D. Zwi. "The small world of the cerebral
cortex." Neuroinformatics 2.2 (2004): 145-162.
.. [2] Maslov, Sergei, and Kim Sneppen. "Specificity and stability in
topology of protein networks." Science 296.5569 (2002): 910-913.
"""
if G.is_directed():
raise nx.NetworkXError(\
"lattice_reference() not defined for directed graphs.")
if len(G) < 4:
raise nx.NetworkXError("Graph has less than four nodes.")
# Instead of choosing uniformly at random from a generated edge list,
# this algorithm chooses nonuniformly from the set of nodes with
# probability weighted by degree.
G = G.copy()
keys,degrees = zip(*G.degree()) # keys, degree
cdf=nx.utils.cumulative_distribution(degrees) # cdf of degree

nnodes = len(G)
nedges = nx.number_of_edges(G)
if D is None:
D = np.zeros((nnodes, nnodes))
un = np.arange(1, nnodes)
um = np.arange(nnodes - 1, 0, -1)
u = np.append((0,), np.where(un < um, un, um))

for v in range(int(np.ceil(nnodes / 2))):
D[nnodes - v - 1, :] = np.append(u[v + 1:], u[:v + 1])
D[v, :] = D[nnodes - v - 1, :][::-1]

niter = niter*nedges
ntries = int(nnodes*nedges/(nnodes*(nnodes-1)/2))
swapcount = 0

for i in range(niter):
n=0
while n < ntries:
# pick two random edges without creating edge list
# choose source node indices from discrete distribution
(ui,xi)=nx.utils.discrete_sequence(2,cdistribution=cdf)
if ui==xi:
continue # same source, skip
u=keys[ui] # convert index to label
x=keys[xi]
# choose target uniformly from neighbors
v = random.choice(list(G.neighbors(u)))
y = random.choice(list(G.neighbors(x)))
vi = keys.index(v)
yi = keys.index(y)
# if random.random() < 0.5:
# ui,u,vi,v = vi,v,ui,u
if v==y or v==u or v==x or y==u or y==x:
continue # all vertices should be different

if (x not in G[u]) and (y not in G[v]): # don't create parallel edges
if D[ui, vi] + D[xi, yi] >= D[ui, xi] + D[vi, yi]:
# only swap if we get
# closer to the diagonal
G.add_edge(u,x)
G.add_edge(v,y)
G.remove_edge(u,v)
G.remove_edge(x,y)
swapcount+=1
break
n+=1

return G



def sigma(G, niter=100, nrand=10):
"""Return the small-world coefficient (sigma) of the given graph.
The small-world coefficient is defined as:
sigma = C/Cr / L/Lr
where C and L are respectively the average clustering coefficient and
average shortest path length of G. Cr and Lr are respectively the average
clustering coefficient and average shortest path length of an equivalent
random graph.
A graph is commonly classified as small-world if sigma>1.
Parameters
----------
G : NetworkX graph
An undirected graph.
niter: integer (optional, default=100)
Approximate number of rewiring per edge to compute the equivalent
random graph.
nrand: integer (optional, default=10)
Number of random graphs generated to compute the average clustering
coefficient (Cr) and average shortest path length (Lr).
Returns
-------
sigma
The small-world coefficient of G.
Notes
-----
The implementation is adapted from the algorithm by Humphries et al.
[1]_[2]_.
References
----------
.. [1] The brainstem reticular formation is a small-world, not scale-free,
network M. D. Humphries, K. Gurney and T. J. Prescott, Proc. Roy. Soc. B
2006 273, 503–511, doi:10.1098/rspb.2005.3354
.. [2] Humphries and Gurney (2008). "Network 'Small-World-Ness': A
Quantitative Method for Determining Canonical Network Equivalence". PLoS
One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051.
"""

# Compute the mean clustering coefficient and average shortest path length
# for an equivalent random graph
randMetrics = {"C":[], "L": []}
for i in range(nrand):
Gr = random_reference(G, niter=niter)
randMetrics["C"].append(nx.algorithms.average_clustering(Gr))
randMetrics["L"].append(nx.algorithms.average_shortest_path_length(Gr))

C = nx.algorithms.average_clustering(G)
L = nx.algorithms.average_shortest_path_length(G)
Cr = np.mean(randMetrics["C"])
Lr = np.mean(randMetrics["L"])

sigma = (C/Cr)/(L/Lr)

return sigma


def omega(G, niter=100, nrand=10):
"""Return the small-world coefficient (omega) of the given graph, which is:
omega = Lr/L - C/Cl
where C and L are respectively the average clustering coefficient and
average shortest path length of G. Lr is the average shortest path length
of an equivalent random graph and Cl is the average clustering coefficient
of an equivalent lattice graph.
The small-world coefficient (omega) ranges between -1 and 1. Values close
to 0 means the G features small-world characteristics. Values close to -1
means G has a lattice shape whereas values close to 1 means G is a random
graph.
Parameters
----------
G : NetworkX graph
An undirected graph.
niter: integer (optional, default=100)
Approximate number of rewiring per edge to compute the equivalent
random graph.
nrand: integer (optional, default=10)
Number of random graphs generated to compute the average clustering
coefficient (Cr) and average shortest path length (Lr).
Returns
-------
omega
The small-work coefficient (omega)
Notes
-----
The implementation is adapted from the algorithm by Telesford et al. [1]_.
References
----------
.. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011). "The
Ubiquity of Small-World Networks". Brain Connectivity. 1 (0038): 367–75.
PMC 3604768. PMID 22432451. doi:10.1089/brain.2011.0038.
"""

# Compute the mean clustering coefficient and average shortest path length
# for an equivalent random graph
randMetrics = {"C":[], "L": []}
for i in range(nrand):
Gr = random_reference(G, niter=niter)
Gl = lattice_reference(G, niter=niter)
randMetrics["C"].append(nx.algorithms.average_clustering(Gl))
randMetrics["L"].append(nx.algorithms.average_shortest_path_length(Gr))

C = nx.algorithms.average_clustering(G)
L = nx.algorithms.average_shortest_path_length(G)
Cl = np.mean(randMetrics["C"])
Lr = np.mean(randMetrics["L"])

omega = (Lr/L) - (C/Cl)

return omega
37 changes: 37 additions & 0 deletions networkx/algorithms/tests/test_smallworld.py
@@ -0,0 +1,37 @@
#!/usr/bin/env python
from nose.tools import *
from networkx.algorithms.smallworld import *
import networkx as nx

import random
random.seed(0)

def test_random_reference():
G = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,0.1)
Gr = random_reference(G)
C = nx.average_clustering(G)
Cr = nx.average_clustering(Gr)
assert_true(C>Cr)

def test_lattice_reference():
G = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,1)
Gl = lattice_reference(G)
L = nx.average_shortest_path_length(G)
Ll = nx.average_shortest_path_length(Gl)
assert_true(Ll>L)

def test_sigma():
Gs = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,0.1)
Gr = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,1)
sigmas = sigma(Gs)
sigmar = sigma(Gr)
assert_true(sigmar<sigmas)

def test_omega():
Gs = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,0.1)
Gl = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,0)
Gr = nx.generators.random_graphs.connected_watts_strogatz_graph(100,6,1)
omegas = omega(Gs)
omegal = omega(Gl)
omegar = omega(Gr)
assert_true(omegal<omegas and omegas<omegar)

0 comments on commit f53567a

Please sign in to comment.