Skip to content

Commit

Permalink
Merge pull request #30 from KrishnaswamyLab/dev
Browse files Browse the repository at this point in the history
graphtools v0.2.1
  • Loading branch information
scottgigante authored Nov 30, 2018
2 parents 9480f9a + 4e261d7 commit de4a123
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 25 deletions.
10 changes: 3 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ graphtools
.. image:: https://img.shields.io/pypi/v/graphtools.svg
:target: https://pypi.org/project/graphtools/
:alt: Latest PyPi version
.. image:: https://anaconda.org/conda-forge/tasklogger/badges/version.svg
:target: https://anaconda.org/conda-forge/tasklogger/
.. image:: https://anaconda.org/conda-forge/graphtools/badges/version.svg
:target: https://anaconda.org/conda-forge/graphtools/
:alt: Latest Conda version
.. image:: https://api.travis-ci.com/KrishnaswamyLab/graphtools.svg?branch=master
:target: https://travis-ci.com/KrishnaswamyLab/graphtools
Expand Down Expand Up @@ -39,11 +39,7 @@ Alternatively, graphtools can be installed using `Conda <https://conda.io/docs/>

Or, to install the latest version from github::

pip install --user git+git://github.com/KrishnaswamyLab/graphtools.git
Alternatively, graphtools can be installed using [Conda](https://conda.io/docs/) (most easily obtained via the [Miniconda Python distribution](https://conda.io/miniconda.html)):

conda install -c conda-forge graphtools
pip install --user git+git://github.com/KrishnaswamyLab/graphtools.git

Usage example
-------------
Expand Down
4 changes: 4 additions & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ graphtools

<a href="https://pypi.org/project/graphtools/"><img src="https://img.shields.io/pypi/v/graphtools.svg" alt="Latest PyPi version"></a>

.. raw:: html

<a href="https://anaconda.org/conda-forge/graphtools/"><img src="https://anaconda.org/conda-forge/graphtools/badges/version.svg" alt="Latest Conda version"></a>

.. raw:: html

<a href="https://travis-ci.com/KrishnaswamyLab/graphtools"><img src="https://api.travis-ci.com/KrishnaswamyLab/graphtools.svg?branch=master" alt="Travis CI Build"></a>
Expand Down
20 changes: 18 additions & 2 deletions graphtools/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def Graph(data,
knn=5,
decay=10,
bandwidth=None,
anisotropy=0,
distance='euclidean',
thresh=1e-4,
kernel_symm='+',
Expand Down Expand Up @@ -68,6 +69,10 @@ def Graph(data,
bandwidth or a list-like (shape=[n_samples]) of bandwidths for each
sample.
anisotropy : float, optional (default: 0)
Level of anisotropy between 0 and 1
(alpha in Coifman & Lafon, 2006)
distance : `str`, optional (default: `'euclidean'`)
Any metric from `scipy.spatial.distance` can be used
distance metric for building kNN graph.
Expand Down Expand Up @@ -230,7 +235,7 @@ def Graph(data,
return Graph(**params)


def from_igraph(G, **kwargs):
def from_igraph(G, attribute="weight", **kwargs):
"""Convert an igraph.Graph to a graphtools.Graph
Creates a graphtools.graphs.TraditionalGraph with a
Expand All @@ -240,6 +245,9 @@ def from_igraph(G, **kwargs):
----------
G : igraph.Graph
Graph to be converted
attribute : str, optional (default: "weight")
attribute containing edge weights, if any.
If None, unweighted graph is built
kwargs
keyword arguments for graphtools.Graph
Expand All @@ -254,5 +262,13 @@ def from_igraph(G, **kwargs):
"Use 'adjacency' instead.".format(kwargs['precomputed']),
UserWarning)
del kwargs['precomputed']
return Graph(sparse.coo_matrix(G.get_adjacency().data),
try:
K = G.get_adjacency(attribute=attribute).data
except ValueError as e:
if str(e) == "Attribute does not exist":
warnings.warn("Edge attribute {} not found. "
"Returning unweighted graph".format(attribute),
UserWarning)
K = G.get_adjacency(attribute=None).data
return Graph(sparse.coo_matrix(K),
precomputed='adjacency', **kwargs)
68 changes: 60 additions & 8 deletions graphtools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
# anndata not installed
pass

from .utils import (elementwise_minimum,
elementwise_maximum,
set_diagonal)
from . import utils


class Base(object):
Expand Down Expand Up @@ -318,6 +316,10 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
Min-max symmetrization constant.
K = `theta * min(K, K.T) + (1 - theta) * max(K, K.T)`
anisotropy : float, optional (default: 0)
Level of anisotropy between 0 and 1
(alpha in Coifman & Lafon, 2006)
initialize : `bool`, optional (default : `True`)
if false, don't create the kernel matrix.
Expand All @@ -336,8 +338,10 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
diff_op : synonym for `P`
"""

def __init__(self, kernel_symm='+',
def __init__(self,
kernel_symm='+',
theta=None,
anisotropy=0,
gamma=None,
initialize=True, **kwargs):
if gamma is not None:
Expand All @@ -351,6 +355,10 @@ def __init__(self, kernel_symm='+',
self.kernel_symm = kernel_symm
self.theta = theta
self._check_symmetrization(kernel_symm, theta)
if not (isinstance(anisotropy, numbers.Real) and 0 <= anisotropy <= 1):
raise ValueError("Expected 0 <= anisotropy <= 1. "
"Got {}".format(anisotropy))
self.anisotropy = anisotropy

if initialize:
tasklogger.log_debug("Initializing kernel...")
Expand Down Expand Up @@ -395,6 +403,7 @@ def _build_kernel(self):
"""
kernel = self.build_kernel()
kernel = self.symmetrize_kernel(kernel)
kernel = self.apply_anisotropy(kernel)
if (kernel - kernel.T).max() > 1e-5:
warnings.warn("K should be symmetric", RuntimeWarning)
if np.any(kernel.diagonal == 0):
Expand All @@ -412,8 +421,8 @@ def symmetrize_kernel(self, K):
elif self.kernel_symm == 'theta':
tasklogger.log_debug(
"Using theta symmetrization (theta = {}).".format(self.theta))
K = self.theta * elementwise_minimum(K, K.T) + \
(1 - self.theta) * elementwise_maximum(K, K.T)
K = self.theta * utils.elementwise_minimum(K, K.T) + \
(1 - self.theta) * utils.elementwise_maximum(K, K.T)
elif self.kernel_symm is None:
tasklogger.log_debug("Using no symmetrization.")
pass
Expand All @@ -424,11 +433,27 @@ def symmetrize_kernel(self, K):
"Got {}".format(self.theta))
return K

def apply_anisotropy(self, K):
if self.anisotropy == 0:
# do nothing
return K
else:
if sparse.issparse(K):
d = np.array(K.sum(1)).flatten()
K = K.tocoo()
K.data = K.data / ((d[K.row] * d[K.col]) ** self.anisotropy)
K = K.tocsr()
else:
d = K.sum(1)
K = K / (np.outer(d, d) ** self.anisotropy)
return K

def get_params(self):
"""Get parameters from this object
"""
return {'kernel_symm': self.kernel_symm,
'theta': self.theta}
'theta': self.theta,
'anisotropy': self.anisotropy}

def set_params(self, **params):
"""Set parameters on this object
Expand All @@ -450,6 +475,9 @@ def set_params(self, **params):
"""
if 'theta' in params and params['theta'] != self.theta:
raise ValueError("Cannot update theta. Please create a new graph")
if 'anisotropy' in params and params['anisotropy'] != self.anisotropy:
raise ValueError(
"Cannot update anisotropy. Please create a new graph")
if 'kernel_symm' in params and \
params['kernel_symm'] != self.kernel_symm:
raise ValueError(
Expand Down Expand Up @@ -580,6 +608,30 @@ def to_pygsp(self, **kwargs):
precomputed="affinity", use_pygsp=True,
**kwargs)

def to_igraph(self, attribute="weight", **kwargs):
"""Convert to an igraph Graph
Uses the igraph.Graph.Weighted_Adjacency constructor
Parameters
----------
attribute : str, optional (default: "weight")
kwargs : additional arguments for igraph.Graph.Weighted_Adjacency
"""
try:
import igraph as ig
except ImportError:
raise ImportError("Please install igraph with "
"`pip install --user python-igraph`.")
try:
W = self.W
except AttributeError:
# not a pygsp graph
W = self.K.copy()
W = utils.set_diagonal(W, 0)
return ig.Graph.Weighted_Adjacency(utils.to_dense(W).tolist(),
attr=attribute, **kwargs)


class PyGSPGraph(with_metaclass(abc.ABCMeta, pygsp.graphs.Graph, Base)):
"""Interface between BaseGraph and PyGSP.
Expand Down Expand Up @@ -634,7 +686,7 @@ def _build_weight_from_kernel(self, kernel):

weight = kernel.copy()
self._diagonal = weight.diagonal().copy()
weight = set_diagonal(weight, 0)
weight = utils.set_diagonal(weight, 0)
return weight


Expand Down
3 changes: 2 additions & 1 deletion graphtools/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class kNNGraph(DataGraph):
distance : `str`, optional (default: `'euclidean'`)
Any metric from `scipy.spatial.distance` can be used
distance metric for building kNN graph.
distance metric for building kNN graph. Custom distance
functions of form `f(x, y) = d` are also accepted.
TODO: actually sklearn.neighbors has even more choices
thresh : `float`, optional (default: `1e-4`)
Expand Down
6 changes: 6 additions & 0 deletions graphtools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ def set_diagonal(X, diag):
def set_submatrix(X, i, j, values):
X[np.ix_(i, j)] = values
return X


def to_dense(X):
if sparse.issparse(X):
X = X.toarray()
return X
2 changes: 1 addition & 1 deletion graphtools/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.0"
__version__ = "0.2.1"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
numpy>=1.14.0
scipy>=1.1.0
pygsp>=>=0.5.1
scikit-learn>=0.19.1
scikit-learn>=0.20.0
future
tasklogger>=0.4.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'numpy>=1.14.0',
'scipy>=1.1.0',
'pygsp>=0.5.1',
'scikit-learn>=0.19.1',
'scikit-learn>=0.20.0',
'future',
'tasklogger>=0.4.0',
]
Expand Down
8 changes: 8 additions & 0 deletions test/load_tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def reset_warnings():
warnings.simplefilter("error")
ignore_numpy_warning()
ignore_igraph_warning()
ignore_joblib_warning()


def ignore_numpy_warning():
Expand All @@ -34,6 +35,13 @@ def ignore_igraph_warning():
"ConfigParser directly instead")


def ignore_joblib_warning():
warnings.filterwarnings(
"ignore", category=DeprecationWarning,
message="check_pickle is deprecated in joblib 0.12 and will be removed"
" in 0.13")


reset_warnings()

global digits
Expand Down
37 changes: 34 additions & 3 deletions test/test_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from __future__ import print_function
from load_tests import (
nose2,
data,
build_graph,
raises,
warns,
)
import warnings

import igraph
import numpy as np
Expand All @@ -21,6 +19,19 @@ def test_from_igraph():
e = np.random.choice(n, 2, replace=False)
K[e[0], e[1]] = K[e[1], e[0]] = 1
g = igraph.Graph.Adjacency(K.tolist())
G = graphtools.from_igraph(g, attribute=None)
G2 = graphtools.Graph(K, precomputed='adjacency')
assert np.all(G.K == G2.K)


def test_from_igraph_weighted():
n = 100
m = 500
K = np.zeros((n, n))
for _ in range(m):
e = np.random.choice(n, 2, replace=False)
K[e[0], e[1]] = K[e[1], e[0]] = np.random.uniform(0, 1)
g = igraph.Graph.Weighted_Adjacency(K.tolist())
G = graphtools.from_igraph(g)
G2 = graphtools.Graph(K, precomputed='adjacency')
assert np.all(G.K == G2.K)
Expand All @@ -35,7 +46,19 @@ def test_from_igraph_invalid_precomputed():
e = np.random.choice(n, 2, replace=False)
K[e[0], e[1]] = K[e[1], e[0]] = 1
g = igraph.Graph.Adjacency(K.tolist())
G = graphtools.from_igraph(g, precomputed='affinity')
G = graphtools.from_igraph(g, attribute=None, precomputed='affinity')


@warns(UserWarning)
def test_from_igraph_invalid_attribute():
n = 100
m = 500
K = np.zeros((n, n))
for _ in range(m):
e = np.random.choice(n, 2, replace=False)
K[e[0], e[1]] = K[e[1], e[0]] = 1
g = igraph.Graph.Adjacency(K.tolist())
G = graphtools.from_igraph(g, attribute="invalid")


def test_to_pygsp():
Expand All @@ -45,6 +68,14 @@ def test_to_pygsp():
assert np.all(G2.K == G.K)


def test_to_igraph():
G = build_graph(data, use_pygsp=True)
G2 = G.to_igraph()
assert isinstance(G2, igraph.Graph)
assert np.all(np.array(G2.get_adjacency(
attribute="weight").data) == G.W)


@warns(UserWarning)
def test_to_pygsp_invalid_precomputed():
G = build_graph(data)
Expand Down
Loading

0 comments on commit de4a123

Please sign in to comment.