Skip to content

Commit de4a123

Browse files
authored
Merge pull request #30 from KrishnaswamyLab/dev
graphtools v0.2.1
2 parents 9480f9a + 4e261d7 commit de4a123

File tree

15 files changed

+226
-25
lines changed

15 files changed

+226
-25
lines changed

README.rst

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ graphtools
55
.. image:: https://img.shields.io/pypi/v/graphtools.svg
66
:target: https://pypi.org/project/graphtools/
77
:alt: Latest PyPi version
8-
.. image:: https://anaconda.org/conda-forge/tasklogger/badges/version.svg
9-
:target: https://anaconda.org/conda-forge/tasklogger/
8+
.. image:: https://anaconda.org/conda-forge/graphtools/badges/version.svg
9+
:target: https://anaconda.org/conda-forge/graphtools/
1010
:alt: Latest Conda version
1111
.. image:: https://api.travis-ci.com/KrishnaswamyLab/graphtools.svg?branch=master
1212
:target: https://travis-ci.com/KrishnaswamyLab/graphtools
@@ -39,11 +39,7 @@ Alternatively, graphtools can be installed using `Conda <https://conda.io/docs/>
3939

4040
Or, to install the latest version from github::
4141

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

4844
Usage example
4945
-------------

doc/source/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ graphtools
66

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

9+
.. raw:: html
10+
11+
<a href="https://anaconda.org/conda-forge/graphtools/"><img src="https://anaconda.org/conda-forge/graphtools/badges/version.svg" alt="Latest Conda version"></a>
12+
913
.. raw:: html
1014

1115
<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>

graphtools/api.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def Graph(data,
1515
knn=5,
1616
decay=10,
1717
bandwidth=None,
18+
anisotropy=0,
1819
distance='euclidean',
1920
thresh=1e-4,
2021
kernel_symm='+',
@@ -68,6 +69,10 @@ def Graph(data,
6869
bandwidth or a list-like (shape=[n_samples]) of bandwidths for each
6970
sample.
7071
72+
anisotropy : float, optional (default: 0)
73+
Level of anisotropy between 0 and 1
74+
(alpha in Coifman & Lafon, 2006)
75+
7176
distance : `str`, optional (default: `'euclidean'`)
7277
Any metric from `scipy.spatial.distance` can be used
7378
distance metric for building kNN graph.
@@ -230,7 +235,7 @@ def Graph(data,
230235
return Graph(**params)
231236

232237

233-
def from_igraph(G, **kwargs):
238+
def from_igraph(G, attribute="weight", **kwargs):
234239
"""Convert an igraph.Graph to a graphtools.Graph
235240
236241
Creates a graphtools.graphs.TraditionalGraph with a
@@ -240,6 +245,9 @@ def from_igraph(G, **kwargs):
240245
----------
241246
G : igraph.Graph
242247
Graph to be converted
248+
attribute : str, optional (default: "weight")
249+
attribute containing edge weights, if any.
250+
If None, unweighted graph is built
243251
kwargs
244252
keyword arguments for graphtools.Graph
245253
@@ -254,5 +262,13 @@ def from_igraph(G, **kwargs):
254262
"Use 'adjacency' instead.".format(kwargs['precomputed']),
255263
UserWarning)
256264
del kwargs['precomputed']
257-
return Graph(sparse.coo_matrix(G.get_adjacency().data),
265+
try:
266+
K = G.get_adjacency(attribute=attribute).data
267+
except ValueError as e:
268+
if str(e) == "Attribute does not exist":
269+
warnings.warn("Edge attribute {} not found. "
270+
"Returning unweighted graph".format(attribute),
271+
UserWarning)
272+
K = G.get_adjacency(attribute=None).data
273+
return Graph(sparse.coo_matrix(K),
258274
precomputed='adjacency', **kwargs)

graphtools/base.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
# anndata not installed
2424
pass
2525

26-
from .utils import (elementwise_minimum,
27-
elementwise_maximum,
28-
set_diagonal)
26+
from . import utils
2927

3028

3129
class Base(object):
@@ -318,6 +316,10 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
318316
Min-max symmetrization constant.
319317
K = `theta * min(K, K.T) + (1 - theta) * max(K, K.T)`
320318
319+
anisotropy : float, optional (default: 0)
320+
Level of anisotropy between 0 and 1
321+
(alpha in Coifman & Lafon, 2006)
322+
321323
initialize : `bool`, optional (default : `True`)
322324
if false, don't create the kernel matrix.
323325
@@ -336,8 +338,10 @@ class BaseGraph(with_metaclass(abc.ABCMeta, Base)):
336338
diff_op : synonym for `P`
337339
"""
338340

339-
def __init__(self, kernel_symm='+',
341+
def __init__(self,
342+
kernel_symm='+',
340343
theta=None,
344+
anisotropy=0,
341345
gamma=None,
342346
initialize=True, **kwargs):
343347
if gamma is not None:
@@ -351,6 +355,10 @@ def __init__(self, kernel_symm='+',
351355
self.kernel_symm = kernel_symm
352356
self.theta = theta
353357
self._check_symmetrization(kernel_symm, theta)
358+
if not (isinstance(anisotropy, numbers.Real) and 0 <= anisotropy <= 1):
359+
raise ValueError("Expected 0 <= anisotropy <= 1. "
360+
"Got {}".format(anisotropy))
361+
self.anisotropy = anisotropy
354362

355363
if initialize:
356364
tasklogger.log_debug("Initializing kernel...")
@@ -395,6 +403,7 @@ def _build_kernel(self):
395403
"""
396404
kernel = self.build_kernel()
397405
kernel = self.symmetrize_kernel(kernel)
406+
kernel = self.apply_anisotropy(kernel)
398407
if (kernel - kernel.T).max() > 1e-5:
399408
warnings.warn("K should be symmetric", RuntimeWarning)
400409
if np.any(kernel.diagonal == 0):
@@ -412,8 +421,8 @@ def symmetrize_kernel(self, K):
412421
elif self.kernel_symm == 'theta':
413422
tasklogger.log_debug(
414423
"Using theta symmetrization (theta = {}).".format(self.theta))
415-
K = self.theta * elementwise_minimum(K, K.T) + \
416-
(1 - self.theta) * elementwise_maximum(K, K.T)
424+
K = self.theta * utils.elementwise_minimum(K, K.T) + \
425+
(1 - self.theta) * utils.elementwise_maximum(K, K.T)
417426
elif self.kernel_symm is None:
418427
tasklogger.log_debug("Using no symmetrization.")
419428
pass
@@ -424,11 +433,27 @@ def symmetrize_kernel(self, K):
424433
"Got {}".format(self.theta))
425434
return K
426435

436+
def apply_anisotropy(self, K):
437+
if self.anisotropy == 0:
438+
# do nothing
439+
return K
440+
else:
441+
if sparse.issparse(K):
442+
d = np.array(K.sum(1)).flatten()
443+
K = K.tocoo()
444+
K.data = K.data / ((d[K.row] * d[K.col]) ** self.anisotropy)
445+
K = K.tocsr()
446+
else:
447+
d = K.sum(1)
448+
K = K / (np.outer(d, d) ** self.anisotropy)
449+
return K
450+
427451
def get_params(self):
428452
"""Get parameters from this object
429453
"""
430454
return {'kernel_symm': self.kernel_symm,
431-
'theta': self.theta}
455+
'theta': self.theta,
456+
'anisotropy': self.anisotropy}
432457

433458
def set_params(self, **params):
434459
"""Set parameters on this object
@@ -450,6 +475,9 @@ def set_params(self, **params):
450475
"""
451476
if 'theta' in params and params['theta'] != self.theta:
452477
raise ValueError("Cannot update theta. Please create a new graph")
478+
if 'anisotropy' in params and params['anisotropy'] != self.anisotropy:
479+
raise ValueError(
480+
"Cannot update anisotropy. Please create a new graph")
453481
if 'kernel_symm' in params and \
454482
params['kernel_symm'] != self.kernel_symm:
455483
raise ValueError(
@@ -580,6 +608,30 @@ def to_pygsp(self, **kwargs):
580608
precomputed="affinity", use_pygsp=True,
581609
**kwargs)
582610

611+
def to_igraph(self, attribute="weight", **kwargs):
612+
"""Convert to an igraph Graph
613+
614+
Uses the igraph.Graph.Weighted_Adjacency constructor
615+
616+
Parameters
617+
----------
618+
attribute : str, optional (default: "weight")
619+
kwargs : additional arguments for igraph.Graph.Weighted_Adjacency
620+
"""
621+
try:
622+
import igraph as ig
623+
except ImportError:
624+
raise ImportError("Please install igraph with "
625+
"`pip install --user python-igraph`.")
626+
try:
627+
W = self.W
628+
except AttributeError:
629+
# not a pygsp graph
630+
W = self.K.copy()
631+
W = utils.set_diagonal(W, 0)
632+
return ig.Graph.Weighted_Adjacency(utils.to_dense(W).tolist(),
633+
attr=attribute, **kwargs)
634+
583635

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

635687
weight = kernel.copy()
636688
self._diagonal = weight.diagonal().copy()
637-
weight = set_diagonal(weight, 0)
689+
weight = utils.set_diagonal(weight, 0)
638690
return weight
639691

640692

graphtools/graphs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class kNNGraph(DataGraph):
4444
4545
distance : `str`, optional (default: `'euclidean'`)
4646
Any metric from `scipy.spatial.distance` can be used
47-
distance metric for building kNN graph.
47+
distance metric for building kNN graph. Custom distance
48+
functions of form `f(x, y) = d` are also accepted.
4849
TODO: actually sklearn.neighbors has even more choices
4950
5051
thresh : `float`, optional (default: `1e-4`)

graphtools/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ def set_diagonal(X, diag):
4444
def set_submatrix(X, i, j, values):
4545
X[np.ix_(i, j)] = values
4646
return X
47+
48+
49+
def to_dense(X):
50+
if sparse.issparse(X):
51+
X = X.toarray()
52+
return X

graphtools/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.0"
1+
__version__ = "0.2.1"

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
numpy>=1.14.0
22
scipy>=1.1.0
33
pygsp>=>=0.5.1
4-
scikit-learn>=0.19.1
4+
scikit-learn>=0.20.0
55
future
66
tasklogger>=0.4.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'numpy>=1.14.0',
77
'scipy>=1.1.0',
88
'pygsp>=0.5.1',
9-
'scikit-learn>=0.19.1',
9+
'scikit-learn>=0.20.0',
1010
'future',
1111
'tasklogger>=0.4.0',
1212
]

test/load_tests/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def reset_warnings():
1717
warnings.simplefilter("error")
1818
ignore_numpy_warning()
1919
ignore_igraph_warning()
20+
ignore_joblib_warning()
2021

2122

2223
def ignore_numpy_warning():
@@ -34,6 +35,13 @@ def ignore_igraph_warning():
3435
"ConfigParser directly instead")
3536

3637

38+
def ignore_joblib_warning():
39+
warnings.filterwarnings(
40+
"ignore", category=DeprecationWarning,
41+
message="check_pickle is deprecated in joblib 0.12 and will be removed"
42+
" in 0.13")
43+
44+
3745
reset_warnings()
3846

3947
global digits

0 commit comments

Comments
 (0)