From a720f24f47dfa3a67c873f76a6b1ffd0ec143696 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Thu, 22 Nov 2018 16:29:42 -0800 Subject: [PATCH 1/8] remove accidental double conda explanation --- README.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 7e3dd0b..5baf4db 100644 --- a/README.rst +++ b/README.rst @@ -39,11 +39,7 @@ Alternatively, graphtools can be installed using `Conda 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 ------------- From ef25775170c2f26a59042c68e952ee4a1f7b38d0 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Mon, 26 Nov 2018 21:24:51 -0500 Subject: [PATCH 2/8] fix conda badge --- README.rst | 4 ++-- doc/source/index.rst | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5baf4db..2859790 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/doc/source/index.rst b/doc/source/index.rst index 16a6ab7..0cddedf 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -6,6 +6,10 @@ graphtools Latest PyPi version +.. raw:: html + + Latest Conda version + .. raw:: html Travis CI Build From fec703537d4e0707965a86e06ff789a0a48b4021 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Mon, 26 Nov 2018 21:25:04 -0500 Subject: [PATCH 3/8] bump sklearn requirement --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 08e1515..f584815 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index f67b380..f995b52 100644 --- a/setup.py +++ b/setup.py @@ -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', ] From 88c5e289a0a6c6ec0d06a71fdae3843107617880 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Mon, 26 Nov 2018 21:25:20 -0500 Subject: [PATCH 4/8] document allowing callable distance --- graphtools/graphs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphtools/graphs.py b/graphtools/graphs.py index a0810da..e67ef5d 100644 --- a/graphtools/graphs.py +++ b/graphtools/graphs.py @@ -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`) From 874cc772eb1e6f53a5a8510cdc1a59229ed6b819 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Wed, 28 Nov 2018 12:39:12 -0500 Subject: [PATCH 5/8] fix joblib error --- test/load_tests/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/load_tests/__init__.py b/test/load_tests/__init__.py index f018957..ce12fee 100644 --- a/test/load_tests/__init__.py +++ b/test/load_tests/__init__.py @@ -17,6 +17,7 @@ def reset_warnings(): warnings.simplefilter("error") ignore_numpy_warning() ignore_igraph_warning() + ignore_joblib_warning() def ignore_numpy_warning(): @@ -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 From d720f8189ac62e08bd37850aaa9cc4a0c6f04b74 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Thu, 29 Nov 2018 15:53:18 -0500 Subject: [PATCH 6/8] allow weighted igraph --- graphtools/api.py | 15 +++++++++++++-- test/test_api.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/graphtools/api.py b/graphtools/api.py index 9e5d31b..ef5c0c8 100644 --- a/graphtools/api.py +++ b/graphtools/api.py @@ -230,7 +230,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 @@ -240,6 +240,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 @@ -254,5 +257,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) diff --git a/test/test_api.py b/test/test_api.py index 49d2126..e8134af 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -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 @@ -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) @@ -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(): @@ -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) From e54a7cd90b67614d4caf9afc8763a45b2efd48cc Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Thu, 29 Nov 2018 16:00:45 -0500 Subject: [PATCH 7/8] add anisotropy feature and fix up to_igraph --- graphtools/api.py | 5 ++++ graphtools/base.py | 68 ++++++++++++++++++++++++++++++++++++++----- graphtools/utils.py | 6 ++++ test/test_exact.py | 46 +++++++++++++++++++++++++++++ test/test_knn.py | 41 +++++++++++++++++++++++++- test/test_landmark.py | 1 + test/test_mnn.py | 1 + 7 files changed, 159 insertions(+), 9 deletions(-) diff --git a/graphtools/api.py b/graphtools/api.py index ef5c0c8..16c2e8b 100644 --- a/graphtools/api.py +++ b/graphtools/api.py @@ -15,6 +15,7 @@ def Graph(data, knn=5, decay=10, bandwidth=None, + anisotropy=0, distance='euclidean', thresh=1e-4, kernel_symm='+', @@ -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. diff --git a/graphtools/base.py b/graphtools/base.py index 5f1a2ab..c084a55 100644 --- a/graphtools/base.py +++ b/graphtools/base.py @@ -23,9 +23,7 @@ # anndata not installed pass -from .utils import (elementwise_minimum, - elementwise_maximum, - set_diagonal) +from . import utils class Base(object): @@ -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. @@ -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: @@ -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...") @@ -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): @@ -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 @@ -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 @@ -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( @@ -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. @@ -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 diff --git a/graphtools/utils.py b/graphtools/utils.py index c7fbabd..e2d9290 100644 --- a/graphtools/utils.py +++ b/graphtools/utils.py @@ -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 diff --git a/test/test_exact.py b/test/test_exact.py index 80d84f0..1a0ceb3 100644 --- a/test/test_exact.py +++ b/test/test_exact.py @@ -348,6 +348,51 @@ def test_exact_graph_fixed_bandwidth(): assert((G.W != G2.W).nnz == 0) +##################################################### +# Check anisotropy +##################################################### + +def test_exact_graph_anisotropy(): + k = 3 + a = 13 + n_pca = 20 + anisotropy = 0.9 + data_small = data[np.random.choice( + len(data), len(data) // 2, replace=False)] + pca = PCA(n_pca, svd_solver='randomized', random_state=42).fit(data_small) + data_small_nu = pca.transform(data_small) + pdx = squareform(pdist(data_small_nu, metric='euclidean')) + knn_dist = np.partition(pdx, k, axis=1)[:, :k] + epsilon = np.max(knn_dist, axis=1) + weighted_pdx = (pdx.T / epsilon).T + K = np.exp(-1 * weighted_pdx**a) + K = K + K.T + K = np.divide(K, 2) + d = K.sum(1) + W = K / (np.outer(d, d) ** anisotropy) + np.fill_diagonal(W, 0) + G = pygsp.graphs.Graph(W) + G2 = build_graph(data_small, thresh=0, n_pca=n_pca, + decay=a, knn=k, random_state=42, + use_pygsp=True, anisotropy=anisotropy) + assert(isinstance(G2, graphtools.graphs.TraditionalGraph)) + assert(G.N == G2.N) + assert(np.all(G.d == G2.d)) + assert((G2.W != G.W).sum() == 0) + assert((G.W != G2.W).nnz == 0) + assert_raises(ValueError, build_graph, + data_small, thresh=0, n_pca=n_pca, + decay=a, knn=k, random_state=42, + use_pygsp=True, anisotropy=-1) + assert_raises(ValueError, build_graph, + data_small, thresh=0, n_pca=n_pca, + decay=a, knn=k, random_state=42, + use_pygsp=True, anisotropy=2) + assert_raises(ValueError, build_graph, + data_small, thresh=0, n_pca=n_pca, + decay=a, knn=k, random_state=42, + use_pygsp=True, anisotropy='invalid') + ##################################################### # Check interpolation ##################################################### @@ -409,6 +454,7 @@ def test_set_params(): 'kernel_symm': '+', 'theta': None, 'knn': 3, + 'anisotropy': 0, 'decay': 10, 'bandwidth': None, 'distance': 'euclidean', diff --git a/test/test_knn.py b/test/test_knn.py index 7d15b0d..f552bec 100644 --- a/test/test_knn.py +++ b/test/test_knn.py @@ -1,4 +1,4 @@ -from __future__ import print_function +from __future__ import print_function, division from load_tests import ( graphtools, np, @@ -196,6 +196,42 @@ def test_knn_graph_sparse_no_pca(): random_state=42, use_pygsp=True) +##################################################### +# Check anisotropy +##################################################### + +def test_knn_graph_anisotropy(): + k = 3 + a = 13 + n_pca = 20 + anisotropy = 0.9 + thresh = 1e-4 + data_small = data[np.random.choice( + len(data), len(data) // 2, replace=False)] + pca = PCA(n_pca, svd_solver='randomized', random_state=42).fit(data_small) + data_small_nu = pca.transform(data_small) + pdx = squareform(pdist(data_small_nu, metric='euclidean')) + knn_dist = np.partition(pdx, k, axis=1)[:, :k] + epsilon = np.max(knn_dist, axis=1) + weighted_pdx = (pdx.T / epsilon).T + K = np.exp(-1 * weighted_pdx**a) + K[K < thresh] = 0 + K = K + K.T + K = np.divide(K, 2) + d = K.sum(1) + W = K / (np.outer(d, d) ** anisotropy) + np.fill_diagonal(W, 0) + G = pygsp.graphs.Graph(W) + G2 = build_graph(data_small, n_pca=n_pca, + thresh=thresh, + decay=a, knn=k, random_state=42, + use_pygsp=True, anisotropy=anisotropy) + assert(isinstance(G2, graphtools.graphs.kNNGraph)) + assert(G.N == G2.N) + assert(np.all(G.d == G2.d)) + np.testing.assert_allclose((G2.W - G.W).data, 0, atol=1e-14, rtol=1e-14) + + ##################################################### # Check interpolation ##################################################### @@ -250,6 +286,7 @@ def test_set_params(): 'random_state': 42, 'kernel_symm': '+', 'theta': None, + 'anisotropy': 0, 'knn': 3, 'decay': None, 'bandwidth': None, @@ -272,10 +309,12 @@ def test_set_params(): assert_raises(ValueError, G.set_params, thresh=1e-3) assert_raises(ValueError, G.set_params, theta=0.99) assert_raises(ValueError, G.set_params, kernel_symm='*') + assert_raises(ValueError, G.set_params, anisotropy=0.7) assert_raises(ValueError, G.set_params, bandwidth=5) G.set_params(knn=G.knn, decay=G.decay, thresh=G.thresh, distance=G.distance, theta=G.theta, + anisotropy=G.anisotropy, kernel_symm=G.kernel_symm) diff --git a/test/test_landmark.py b/test/test_landmark.py index da0fbbc..7333207 100644 --- a/test/test_landmark.py +++ b/test/test_landmark.py @@ -150,6 +150,7 @@ def test_set_params(): 'kernel_symm': '+', 'theta': None, 'n_landmark': 500, + 'anisotropy': 0, 'knn': 3, 'decay': None, 'bandwidth': None, diff --git a/test/test_mnn.py b/test/test_mnn.py index be78437..217913d 100644 --- a/test/test_mnn.py +++ b/test/test_mnn.py @@ -328,6 +328,7 @@ def test_set_params(): 'random_state': 42, 'kernel_symm': 'theta', 'theta': 0.5, + 'anisotropy': 0, 'beta': 1, 'adaptive_k': None, 'knn': 3, From b6889bb9597d900535953be2e74da01afe9141c2 Mon Sep 17 00:00:00 2001 From: Scott Gigante Date: Thu, 29 Nov 2018 23:14:40 -0500 Subject: [PATCH 8/8] bump version --- graphtools/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphtools/version.py b/graphtools/version.py index d3ec452..3ced358 100644 --- a/graphtools/version.py +++ b/graphtools/version.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1"