diff --git a/networkcommons/data/__init__.py b/networkcommons/data/__init__.py index 1153ac8..2251f98 100644 --- a/networkcommons/data/__init__.py +++ b/networkcommons/data/__init__.py @@ -14,5 +14,5 @@ # from ._builtin import _module_data, urls -from .omics import * -from .network import * +from . import omics +from . import network diff --git a/networkcommons/data/network/lianaplus.py b/networkcommons/data/network/_liana.py similarity index 86% rename from networkcommons/data/network/lianaplus.py rename to networkcommons/data/network/_liana.py index 16a6c9c..73c9499 100644 --- a/networkcommons/data/network/lianaplus.py +++ b/networkcommons/data/network/_liana.py @@ -13,6 +13,16 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +Prior knowledge network used by LIANA+. +""" + +__all__ = ['get_lianaplus'] + +import lazy_import + +liana = lazy_import.lazy_module('liana') + def get_lianaplus(resource='Consensus'): """ @@ -27,8 +37,6 @@ def get_lianaplus(resource='Consensus'): pandas.DataFrame: Liana+ network with source, target, and sign columns. """ - import liana - network = liana.resource.select_resource(resource).drop_duplicates() network.columns = ['source', 'target'] network['sign'] = 1 diff --git a/networkcommons/data/network/moon.py b/networkcommons/data/network/_moon.py similarity index 86% rename from networkcommons/data/network/moon.py rename to networkcommons/data/network/_moon.py index c99ced2..9b49a6f 100644 --- a/networkcommons/data/network/moon.py +++ b/networkcommons/data/network/_moon.py @@ -13,26 +13,34 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +Prior knowledge network used by MOON. +""" + +__all__ = ['build_moon_regulons'] + +import lazy_import import numpy as np import pandas as pd +dc = lazy_import.lazy_module('decoupler') + from networkcommons import _utils +from . import _omnipath +from . import _liana def build_moon_regulons(include_liana=False): - import decoupler as dc - from . import omnipath - dorothea_df = dc.get_collectri() TFs = np.unique(dorothea_df['source']) - full_pkn = omnipath.get_omnipath(genesymbols=True, directed_signed=True) + full_pkn = _omnipath.get_omnipath(genesymbols=True, directed_signed=True) if include_liana: - from . import lianaplus - ligrec_resource = lianaplus.get_lianaplus() + + ligrec_resource = _liana.get_lianaplus() full_pkn = pd.concat([full_pkn, ligrec_resource]) full_pkn['edgeID'] = full_pkn['source'] + '_' + full_pkn['target'] diff --git a/networkcommons/data/network/omnipath.py b/networkcommons/data/network/_omnipath.py similarity index 88% rename from networkcommons/data/network/omnipath.py rename to networkcommons/data/network/_omnipath.py index 10ad2a3..279dbcc 100644 --- a/networkcommons/data/network/omnipath.py +++ b/networkcommons/data/network/_omnipath.py @@ -13,12 +13,19 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # -import omnipath as op +""" +Access to the OmniPath database. +""" + +__all__ = ['get_omnipath'] + +import lazy_import import numpy as np +op = lazy_import.lazy_module('omnipath') + -def get_omnipath(genesymbols=True, - directed_signed=True): +def get_omnipath(genesymbols: bool = True, directed_signed: bool = True): """ Retrieves the Omnipath network with directed interactions and specific criteria. diff --git a/networkcommons/data/omics/__init__.py b/networkcommons/data/omics/__init__.py index 2ff6c6a..b180689 100644 --- a/networkcommons/data/omics/__init__.py +++ b/networkcommons/data/omics/__init__.py @@ -13,8 +13,12 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # -from .decryptm import * -from .decryptm_ebi import get_decryptm as _get_decryptm -from .deseq2 import * -from .panacea import * -from .scperturb import * +""" +Access to public omics datasets. +""" + +from ._common import * +from ._decryptm import * +from ._deseq2 import * +from ._panacea import * +from ._scperturb import * diff --git a/networkcommons/data/omics/common.py b/networkcommons/data/omics/_common.py similarity index 98% rename from networkcommons/data/omics/common.py rename to networkcommons/data/omics/_common.py index f0f57ec..44c7469 100644 --- a/networkcommons/data/omics/common.py +++ b/networkcommons/data/omics/_common.py @@ -13,27 +13,28 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +General procedures for downloading omics datasets. +""" + from __future__ import annotations -from typing import Any, IO +__all__ = ['datasets'] + +from typing import IO import zipfile import os -import re -import glob import hashlib import contextlib import urllib.parse -import shutil import requests import bs4 import pandas as pd -from .._builtin import _module_data from networkcommons import _conf from networkcommons._session import _log - -__all__ = ['datasets'] +from .._builtin import _module_data def _datasets() -> dict[str, dict]: diff --git a/networkcommons/data/omics/decryptm.py b/networkcommons/data/omics/_decryptm.py similarity index 86% rename from networkcommons/data/omics/decryptm.py rename to networkcommons/data/omics/_decryptm.py index 64ea11f..fa210dd 100644 --- a/networkcommons/data/omics/decryptm.py +++ b/networkcommons/data/omics/_decryptm.py @@ -13,18 +13,22 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +DecryptM proteomics and phosphoproteomics data. +""" + from __future__ import annotations +__all__ = ['decryptm_datasets', 'decryptm_table', 'decryptm_experiment'] + import os import urllib.parse import pandas as pd -from . import common +from . import _common from networkcommons import _conf -__all__ = ['decryptm_datasets', 'decryptm_table', 'decryptm_experiment'] - def decryptm_datasets(update: bool = False) -> pd.DataFrame: """ @@ -43,7 +47,7 @@ def decryptm_datasets(update: bool = False) -> pd.DataFrame: if update or not os.path.exists(path): - baseurl = urllib.parse.urljoin(common._baseurl(), 'decryptm') + baseurl = urllib.parse.urljoin(_common._baseurl(), 'decryptm') datasets = pd.DataFrame( [ @@ -52,9 +56,9 @@ def decryptm_datasets(update: bool = False) -> pd.DataFrame: data_type, fname, ) - for experiment in common._ls(baseurl) - for data_type in common._ls(f'{baseurl}/{experiment}') - for fname in common._ls(f'{baseurl}/{experiment}/{data_type}') + for experiment in _common._ls(baseurl) + for data_type in _common._ls(f'{baseurl}/{experiment}') + for fname in _common._ls(f'{baseurl}/{experiment}/{data_type}') if fname.startswith('curve') ], columns = ['experiment', 'data_type', 'fname'] @@ -86,8 +90,8 @@ def decryptm_table(experiment: str, data_type: str, fname: str) -> pd.DataFrame: The table as a pandas data frame. """ - return common._open( - common._commons_url('decryptm', **locals()), + return _common._open( + _common._commons_url('decryptm', **locals()), df = {'sep': '\t'}, ) diff --git a/networkcommons/data/omics/decryptm_ebi.py b/networkcommons/data/omics/_decryptm_ebi.py similarity index 84% rename from networkcommons/data/omics/decryptm_ebi.py rename to networkcommons/data/omics/_decryptm_ebi.py index ed81290..6f2c2e8 100644 --- a/networkcommons/data/omics/decryptm_ebi.py +++ b/networkcommons/data/omics/_decryptm_ebi.py @@ -15,16 +15,18 @@ from __future__ import annotations +__all__ = [] + import os import shutil import glob import urllib.parse -from . import common +from . import _common from networkcommons._session import _log -def get_decryptm(path: str): +def _get_decryptm(path: str): """ Download DecryptM from EBI. @@ -35,13 +37,13 @@ def get_decryptm(path: str): os.makedirs(path, exist_ok = True) url = 'https://ftp.pride.ebi.ac.uk/pride/data/archive/2023/03/PXD037285/' - files = [f for f in common._ls(url) if f.endswith('Curves.zip')] + files = [f for f in _common._ls(url) if f.endswith('Curves.zip')] for fname in files: zip_url = urllib.parse.urljoin(url, fname) - with common._open(zip_url) as zip_file: + with _common._open(zip_url) as zip_file: _log(f'Extracting zip `{zip_file.filename}` to `{path}`.') zip_file.extractall(path) diff --git a/networkcommons/data/omics/deseq2.py b/networkcommons/data/omics/_deseq2.py similarity index 89% rename from networkcommons/data/omics/deseq2.py rename to networkcommons/data/omics/_deseq2.py index bdb2d8d..c2aa1d4 100644 --- a/networkcommons/data/omics/deseq2.py +++ b/networkcommons/data/omics/_deseq2.py @@ -13,8 +13,14 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +Differential expression analysis with DESeq2. +""" + from __future__ import annotations +__all__ = ['deseq2'] + from typing import TYPE_CHECKING import multiprocessing import importlib @@ -23,17 +29,16 @@ import pandas as pd +import lazy_import from pypath_common import _misc as _ppcommon -import pydeseq2 as _deseq2 for _mod in ('default_inference', 'dds', 'ds'): - importlib.import_module(f'pydeseq2.{_mod}') + + globals()[f'_deseq2_{_mod}'] = lazy_import.lazy_module(f'pydeseq2.{_mod}') from networkcommons import _conf from networkcommons._session import _log -__all__ = ['deseq2'] - def deseq2( counts: pd.DataFrame, @@ -70,9 +75,9 @@ def deseq2( ref_group = ref_group.replace('_', '-') n_cpus = _conf.get('cpu_count', multiprocessing.cpu_count()) - inference = _deseq2.default_inference.DefaultInference(n_cpus = n_cpus) + inference = _deseq2_default_inference.DefaultInference(n_cpus = n_cpus) - dds = _deseq2.dds.DeseqDataSet( + dds = _deseq2_dds.DeseqDataSet( counts=counts.T, metadata=metadata, design_factors=design_factors, @@ -82,7 +87,7 @@ def deseq2( dds.deseq2() - results = _deseq2.ds.DeseqStats( + results = _deseq2_ds.DeseqStats( dds, contrast=['group', test_group, ref_group], inference=inference diff --git a/networkcommons/data/omics/_moon.py b/networkcommons/data/omics/_moon.py index 733d043..e985525 100644 --- a/networkcommons/data/omics/_moon.py +++ b/networkcommons/data/omics/_moon.py @@ -19,11 +19,11 @@ from __future__ import annotations -import pandas as pd +__all__ = ['moon'] -from . import common +import pandas as pd -__all__ = ['moon'] +from . import _common def moon() -> dict[str, pd.DataFrame]: diff --git a/networkcommons/data/omics/panacea.py b/networkcommons/data/omics/_panacea.py similarity index 77% rename from networkcommons/data/omics/panacea.py rename to networkcommons/data/omics/_panacea.py index d07cc2d..10b1e1c 100644 --- a/networkcommons/data/omics/panacea.py +++ b/networkcommons/data/omics/_panacea.py @@ -13,13 +13,18 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +RNA-Seq data from the 'Pancancer Analysis of Chemical Entity Activity' +resource. +""" + from __future__ import annotations -import pandas as pd +__all__ = ['panacea'] -from . import common +import pandas as pd -__all__ = ['panacea'] +from . import _common def panacea() -> tuple[pd.DataFrame]: @@ -31,8 +36,8 @@ def panacea() -> tuple[pd.DataFrame]: """ return tuple( - common._open( - common._commons_url('panacea', table = table), + _common._open( + _common._commons_url('panacea', table = table), df = {'sep': '\t'}, ) for table in ('count', 'meta') diff --git a/networkcommons/data/omics/scperturb.py b/networkcommons/data/omics/_scperturb.py similarity index 87% rename from networkcommons/data/omics/scperturb.py rename to networkcommons/data/omics/_scperturb.py index 6ff27fb..8c27478 100644 --- a/networkcommons/data/omics/scperturb.py +++ b/networkcommons/data/omics/_scperturb.py @@ -13,15 +13,21 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +Single-cell RNA-Seq data from the 'scPerturb' resource. +""" + from __future__ import annotations +__all__ = ['scperturb', 'scperturb_metadata', 'scperturb_datasets'] + from typing import Any import json import bs4 import anndata as ad -from . import common +from . import _common _URL = 'https://zenodo.org/record/10044268' @@ -50,7 +56,7 @@ def scperturb_metadata() -> dict[str, Any]: record is https://zenodo.org/records/10044268. """ - soup = common._open(_URL, ftype = 'html') + soup = _common._open(_URL, ftype = 'html') data = soup.find(id = 'recordCitation').attrs['data-record'] return json.loads(data) @@ -72,6 +78,6 @@ def scperturb(dataset: str) -> ad.AnnData: """ urls = scperturb_datasets() - path = common._maybe_download(urls[dataset]) + path = _common._maybe_download(urls[dataset]) return ad.read_h5ad(path) diff --git a/networkcommons/eval/__init__.py b/networkcommons/eval/__init__.py index a01b238..553fff4 100644 --- a/networkcommons/eval/__init__.py +++ b/networkcommons/eval/__init__.py @@ -17,4 +17,4 @@ Network metrics and analysis methods. """ -from . import metrics as metrics +from .metrics import * diff --git a/networkcommons/eval/metrics.py b/networkcommons/eval/_metrics.py similarity index 93% rename from networkcommons/eval/metrics.py rename to networkcommons/eval/_metrics.py index 195d249..765a278 100644 --- a/networkcommons/eval/metrics.py +++ b/networkcommons/eval/_metrics.py @@ -17,9 +17,7 @@ Basic graph metrics. """ -import pandas as pd -import networkx as nx -import numpy as np +from __future__ import annotations __all__ = [ 'get_number_nodes', @@ -31,8 +29,12 @@ 'get_graph_metrics', ] +import pandas as pd +import networkx as nx +import numpy as np + -def get_number_nodes(network): +def get_number_nodes(network: nx.Graph) -> int: """ Get the number of nodes in the network. @@ -46,7 +48,7 @@ def get_number_nodes(network): return network.number_of_nodes() -def get_number_edges(network): +def get_number_edges(network: nx.Graph) -> int: """ Get the number of edges in the network. @@ -60,7 +62,7 @@ def get_number_edges(network): return network.number_of_edges() -def get_mean_degree(network): +def get_mean_degree(network: nx.Graph) -> float: """ Get the mean degree centrality of the network. @@ -74,7 +76,7 @@ def get_mean_degree(network): return np.mean(list(dict(nx.degree(network)).values())) -def get_mean_betweenness(network): +def get_mean_betweenness(network: nx.Graph) -> float: """ Get the mean betweenness centrality of the network. @@ -88,7 +90,7 @@ def get_mean_betweenness(network): return np.mean(list(nx.betweenness_centrality(network).values())) -def get_mean_closeness(network): +def get_mean_closeness(network: nx.Graph) -> float: """ Get the mean closeness centrality of the network. diff --git a/networkcommons/methods/__init__.py b/networkcommons/methods/__init__.py index 475fc33..4497219 100644 --- a/networkcommons/methods/__init__.py +++ b/networkcommons/methods/__init__.py @@ -17,6 +17,6 @@ Network based inference methods and network operations. """ -from networkcommons._methods.graph import * -from networkcommons._methods.causal import * -from networkcommons._methods.moon import * \ No newline at end of file +from ._graph import * +from ._causal import * +from ._moon import * diff --git a/networkcommons/methods/causal.py b/networkcommons/methods/_causal.py similarity index 81% rename from networkcommons/methods/causal.py rename to networkcommons/methods/_causal.py index de8594b..a3fff3e 100644 --- a/networkcommons/methods/causal.py +++ b/networkcommons/methods/_causal.py @@ -17,19 +17,17 @@ Causal network inference methods. """ -import networkx as nx -import corneto as cn -from corneto.contrib.networkx import ( - networkx_to_corneto_graph, - corneto_graph_to_networkx -) -from corneto.methods.carnival import get_selected_edges +from __future__ import annotations __all__ = [ 'convert_cornetograph', 'run_corneto_carnival', ] +import lazy_import +import networkx as nx +cn = lazy_import.lazy_import('corneto') + def convert_cornetograph(graph): """ @@ -44,7 +42,7 @@ def convert_cornetograph(graph): if isinstance(graph, cn._graph.Graph): corneto_graph = graph elif isinstance(graph, (nx.Graph, nx.DiGraph)): - corneto_graph = networkx_to_corneto_graph(graph) + corneto_graph = cn.contrib.networkx.networkx_to_corneto_graph(graph) return corneto_graph @@ -80,10 +78,14 @@ def run_corneto_carnival(network, verbose=verbose ) - network_sol = graph.edge_subgraph(get_selected_edges(problem, graph)) + network_sol = graph.edge_subgraph( + cn.methods.carnival.get_selected_edges(problem, graph), + ) - network_nx = corneto_graph_to_networkx(network_sol, - skip_unsupported_edges=True) + network_nx = cn.contrib.networkx.corneto_graph_to_networkx( + network_sol, + skip_unsupported_edges=True, + ) network_nx.remove_nodes_from(['_s', '_pert_c0', '_meas_c0']) diff --git a/networkcommons/methods/graph.py b/networkcommons/methods/_graph.py similarity index 99% rename from networkcommons/methods/graph.py rename to networkcommons/methods/_graph.py index ff06d49..e10f50c 100644 --- a/networkcommons/methods/graph.py +++ b/networkcommons/methods/_graph.py @@ -13,16 +13,12 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +from __future__ import annotations + """ Graph based methods. """ -import networkx as nx -import numpy as np - -from networkcommons import _utils -from networkcommons._session import session as _session - __all__ = [ 'run_shortest_paths', 'run_sign_consistency', @@ -33,6 +29,12 @@ 'compute_ppr_overlap', ] +import networkx as nx +import numpy as np + +from networkcommons import _utils +from networkcommons._session import session as _session + def run_shortest_paths(network, source_dict, target_dict, verbose=False): """ diff --git a/networkcommons/methods/moon.py b/networkcommons/methods/_moon.py similarity index 99% rename from networkcommons/methods/moon.py rename to networkcommons/methods/_moon.py index 133209a..bc21120 100644 --- a/networkcommons/methods/moon.py +++ b/networkcommons/methods/_moon.py @@ -17,16 +17,7 @@ Moon: multi-omics?? """ -from typing import Counter -import re - -import networkx as nx -import pandas as pd -import decoupler as dc -import numpy as np - -from networkcommons._methods import graph as _graph -from networkcommons._session import _log +from __future__ import annotations __all__ = [ 'meta_network_cleanup', @@ -44,6 +35,18 @@ 'translate_res', ] +import collections +import re + +import lazy_import +import networkx as nx +import pandas as pd +dc = lazy_import.lazy_module('decoupler') +import numpy as np + +from . import _graph +from networkcommons._session import _log + def meta_network_cleanup(graph): """ @@ -297,7 +300,7 @@ def compress_same_children(uncompressed_graph, sig_input, metab_input): if parent not in sig_input and parent not in metab_input } - signature_counts = Counter(filtered_signatures.values()) + signature_counts = collections.Counter(filtered_signatures.values()) # Identify duplicated signatures that are not in metab_input or sig_input duplicated_parents = { diff --git a/networkcommons/network/__init__.py b/networkcommons/network/__init__.py index e69de29..e910329 100644 --- a/networkcommons/network/__init__.py +++ b/networkcommons/network/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Representation of molecular interaction networks. + +The `Network` object represents a graph of molecular interactions with custom +node and edge attributes and metadata. It is a central piece of the +`networkcommons` package, and integrates virtually all the functionalities +provided. +""" + +from ._network import * diff --git a/networkcommons/network/_network.py b/networkcommons/network/_network.py index 4425b75..94cf3b6 100644 --- a/networkcommons/network/_network.py +++ b/networkcommons/network/_network.py @@ -1,24 +1,63 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Representation of molecular interaction networks. + +The `Network` object represents a graph of molecular interactions with custom +node and edge attributes and metadata. It is a central piece of the +`networkcommons` package, and integrates virtually all the functionalities +provided. +""" + from __future__ import annotations +__all__ = ['Network'] + +import lazy_import import pandas as pd -import corneto +cn = lazy_import.lazy_import('corneto') -from networkcommons._data import _network as _universe -from networkcommons._noi._noi import Noi +from networkcommons.data import _network as _universe +from networkcommons.noi._noi import Noi class Network: + """ + A molecular interaction network. + """ - _co: corneto.Graph - _edges: pd.DataFrame - _nodes: pd.DataFrame def __init__( self, universe: str | None = "omnipath", noi: Noi | list[str] | list[list[str]] | dict[str, list[str]] = None, ): + """ + Args: + universe: + The prior knowledge universe: a complete set of interactions + that the instance uses to extract subnetworks from and that + will be queried in operations applied on the instance. + noi: + Nodes of interest. + """ + self._co: cn.Graph = None + self._edges: pd.DataFrame = None + self._nodes: pd.DataFrame = None self.universe = universe self.noi = noi @@ -43,7 +82,7 @@ def as_nx(self, attrs: str | list[str]) -> nx.DiGraph: pass - def as_corneto(self, attrs: str | list[str]) -> corneto.Graph: + def as_corneto(self, attrs: str | list[str]) -> cn.Graph: """ Return the graph as an igraph object with the desired attributes. """ diff --git a/networkcommons/noi/__init__.py b/networkcommons/noi/__init__.py index 31bfd8a..6843acb 100644 --- a/networkcommons/noi/__init__.py +++ b/networkcommons/noi/__init__.py @@ -13,5 +13,11 @@ # https://www.gnu.org/licenses/gpl-3.0.txt # +""" +Handling nodes of interest for network lookup queries. +""" + +__all__ = ['Node', 'Noi'] + from ._node import Node from ._noi import Noi diff --git a/networkcommons/noi/_node.py b/networkcommons/noi/_node.py index c2399b4..b4e89f0 100644 --- a/networkcommons/noi/_node.py +++ b/networkcommons/noi/_node.py @@ -19,18 +19,19 @@ from __future__ import annotations +__all__ = ['Node'] + from typing import Generator, Literal, Sequence import re import inspect -import pypath_common._misc as _common -import pypath.utils.taxonomy as _taxonomy -import pypath.utils.mapping as _mapping -import pypath.utils.orthology as _orthology -import pypath.inputs.uniprot as _uniprot -import pypath.inputs.uniprot_db as _uniprot_db - -__all__ = ['Node'] +import lazy_import +_common = lazy_import.lazy_module('pypath_common._misc') +_taxonomy = lazy_import.lazy_module('pypath.utils.taxonomy') +_mapping = lazy_import.lazy_module('pypath.utils.mapping') +_orthology = lazy_import.lazy_module('pypath.utils.orthology') +_uniprot = lazy_import.lazy_module('pypath.inputs.uniprot') +_uniprot_db = lazy_import.lazy_module('pypath.inputs.uniprot_db') _REENS = re.compile(r'ENS[A-Z]*?([A-Z])[0-9]+') diff --git a/networkcommons/noi/_noi.py b/networkcommons/noi/_noi.py index bbfe39a..2cb864e 100644 --- a/networkcommons/noi/_noi.py +++ b/networkcommons/noi/_noi.py @@ -19,6 +19,8 @@ from __future__ import annotations +__all__ = ['Noi'] + from typing import Any, Callable, Hashable import collections import itertools @@ -30,7 +32,6 @@ from . import _node -__all__ = ['Noi'] class Noi(collections.abc.Mapping): diff --git a/networkcommons/visual/__init__.py b/networkcommons/visual/__init__.py index 122425f..756899b 100644 --- a/networkcommons/visual/__init__.py +++ b/networkcommons/visual/__init__.py @@ -17,9 +17,9 @@ Visualization methods for networks and analyses. """ -from . import vis_networkx as vis_networkx -from . import vis_yfiles as vis_yfiles -from . import styles as vis_styles -from . import yfiles_styles as vis_yfiles_styles -from . import rnaseq as vis_rnaseq -from . import network_stats as vis_network_stat +from ._vis_networkx import * +from ._vis_yfiles import * +from ._styles import * +from ._yfiles_styles import * +from ._rnaseq import * +from ._network_stats import * diff --git a/networkcommons/visual/_aux.py b/networkcommons/visual/_aux.py index b15d4bf..1a05d89 100644 --- a/networkcommons/visual/_aux.py +++ b/networkcommons/visual/_aux.py @@ -1,3 +1,18 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + def wrap_node_name(node_name): if ":" in node_name: @@ -7,5 +22,3 @@ def wrap_node_name(node_name): return node_name[8:] else: return node_name - - diff --git a/networkcommons/visual/network_stats.py b/networkcommons/visual/_network_stats.py similarity index 87% rename from networkcommons/visual/network_stats.py rename to networkcommons/visual/_network_stats.py index 22a8c57..3359332 100644 --- a/networkcommons/visual/network_stats.py +++ b/networkcommons/visual/_network_stats.py @@ -1,7 +1,32 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Plot network (graph) metrics. +""" + +from __future__ import annotations + +__all__ = ['plot_n_nodes_edges'] + +from typing import List, Dict + +import lazy_import import pandas as pd -import matplotlib.pyplot as plt +plt = lazy_import.lazy_import('matplotlib.pyplot') import networkx as nx -from typing import List, Dict def plot_n_nodes_edges( @@ -97,4 +122,4 @@ def plot_n_nodes_edges( plt.savefig(filepath) if render: - plt.show() \ No newline at end of file + plt.show() diff --git a/networkcommons/visual/rnaseq.py b/networkcommons/visual/_rnaseq.py similarity index 85% rename from networkcommons/visual/rnaseq.py rename to networkcommons/visual/_rnaseq.py index 3921d66..d0e359d 100644 --- a/networkcommons/visual/rnaseq.py +++ b/networkcommons/visual/_rnaseq.py @@ -1,9 +1,37 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Plots for omics data exploration. +""" + +from __future__ import annotations + +__all__ = [ + 'build_volcano_plot', + 'build_ma_plot', + 'build_pca_plot', + 'build_heatmap_with_tree', +] + +import lazy_import import pandas as pd import numpy as np -import matplotlib.pyplot as plt -import seaborn as sns -from sklearn.decomposition import PCA -from typing import List +plt = lazy_import.lazy_import('matplotlib.pyplot') +sns = lazy_import.lazy_import('seaborn') +sklearn_decomp = lazy_import.lazy_module('sklearn.decomposition') def build_volcano_plot( @@ -135,7 +163,7 @@ def build_pca_plot( save: bool = False, output_dir: str = "." ): - pca = PCA(n_components=2) + pca = sklearn_decomp.PCA(n_components=2) principal_components = pca.fit_transform(data) pca_df = pd.DataFrame(data=principal_components, columns=['PC1', 'PC2']) @@ -162,7 +190,7 @@ def build_heatmap_with_tree( data: pd.DataFrame, top_n: int = 50, value_column: str = 'log2FoldChange_condition_1', - conditions: List[str] = None, + conditions: list[str] = None, title: str = "Heatmap of Top Differentially Expressed Genes", save: bool = False, output_dir: str = "." @@ -174,7 +202,7 @@ def build_heatmap_with_tree( data (pd.DataFrame): DataFrame containing RNA-seq results. top_n (int): Number of top differentially expressed genes to include in the heatmap. value_column (str): Column name for the values to rank and select the top genes. - conditions (List[str]): List of condition columns to include in the heatmap. + conditions (list[str]): List of condition columns to include in the heatmap. title (str): Title of the plot. save (bool): Whether to save the plot. Default is False. output_dir (str): Directory to save the plot. Default is ".". @@ -196,4 +224,4 @@ def build_heatmap_with_tree( if save: plt.savefig(f"{output_dir}/heatmap_with_tree.png") - plt.show() \ No newline at end of file + plt.show() diff --git a/networkcommons/visual/styles.py b/networkcommons/visual/_styles.py similarity index 91% rename from networkcommons/visual/styles.py rename to networkcommons/visual/_styles.py index 54e7838..04872a0 100644 --- a/networkcommons/visual/styles.py +++ b/networkcommons/visual/_styles.py @@ -1,3 +1,26 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Style definitions for network visualizations. +""" + +from __future__ import annotations + +__all__ = ['get_styles', 'merge_styles', 'set_style_attributes'] + from networkcommons._session import _log @@ -156,4 +179,3 @@ def merge_styles(default_style, custom_style, path=""): _log(f"Missing key '{path}.{key}' in custom style. Using default value.") return merged_style - diff --git a/networkcommons/visual/vis_networkx.py b/networkcommons/visual/_vis_networkx.py similarity index 82% rename from networkcommons/visual/vis_networkx.py rename to networkcommons/visual/_vis_networkx.py index 7dae8f0..88b7e48 100644 --- a/networkcommons/visual/vis_networkx.py +++ b/networkcommons/visual/_vis_networkx.py @@ -14,19 +14,28 @@ # """ -This module contains functions to visualize networks. -The styles for different types of networks are defined in the get_styles() function. -The set_style_attributes() function is used to set attributes for nodes and edges based on the given styles. -The visualize_network_default() function visualizes the graph with default style. -The visualize_network_sign_consistent() function visualizes the graph considering sign consistency. -The visualize_network() function is the main function to visualize the graph based on the network type. +Visualize networks. + +The styles for different types of networks are defined in the `get_styles()` +function. The `set_style_attributes()` function is used to set attributes for +nodes and edges based on the given styles. The `visualize_network_default()` +function visualizes the graph with default style. The +`visualize_network_sign_consistent()` function visualizes the graph considering +sign consistency. The `visualize_network()` function is the main function to +visualize the graph based on the network type. """ +from __future__ import annotations + +__all__ = ['NetworkXVisualizer'] + +import lazy_import import networkx as nx -import matplotlib.pyplot as plt -from networkcommons._visual._aux import wrap_node_name +plt = lazy_import.lazy_import('matplotlib.pyplot') + +from . import _aux +from . import _styles from networkcommons._session import _log -from networkcommons._visual.styles import (get_styles, set_style_attributes, merge_styles) class NetworkXVisualizer: @@ -34,13 +43,13 @@ class NetworkXVisualizer: def __init__(self, network, color_by="effect"): self.network = network.copy() self.color_by = color_by - self.edge_colors = get_styles()['default']['edges'] + self.edge_colors = _styles.get_styles()['default']['edges'] def set_custom_edge_colors(self, custom_edge_colors): self.edge_colors.update(custom_edge_colors) def color_nodes(self): - default_node_colors = get_styles()['default']['nodes']['default'] + default_node_colors = _styles.get_styles()['default']['nodes']['default'] source_node_color = default_node_colors['sources'] target_node_color = default_node_colors['targets'] nodes = self.network.nodes @@ -54,7 +63,7 @@ def color_nodes(self): nodes[node]['color'] = target_node_color def color_edges(self): - edge_colors = get_styles()['default']['edges'] + edge_colors = _styles.get_styles()['default']['edges'] for edge in self.network.edges: u, v = edge edge_data = self.network.get_edge_data(u, v) @@ -80,8 +89,8 @@ def visualize_network_default(self, prog (str, optional): The layout program to use. Defaults to 'dot'. custom_style (dict, optional): The custom style to apply. If None, the default style is used. """ - default_style = get_styles()['default'] - style = merge_styles(default_style, custom_style) + default_style = _styles.get_styles()['default'] + style = _styles.merge_styles(default_style, custom_style) A = nx.nx_agraph.to_agraph(network) A.graph_attr['ratio'] = '1.2' @@ -100,11 +109,11 @@ def visualize_network_default(self, else: base_style = style['nodes']['other'] - set_style_attributes(node, base_style) + _styles.set_style_attributes(node, base_style) for edge in A.edges(): edge_style = style['edges']['neutral'] - set_style_attributes(edge, edge_style) + _styles.set_style_attributes(edge, edge_style) A.layout(prog=prog) return A @@ -125,8 +134,8 @@ def visualize_network_sign_consistent(self, prog (str, optional): The layout program to use. Defaults to 'dot'. custom_style (dict, optional): The custom style to apply. Defaults to None. """ - default_style = get_styles()['sign_consistent'] - style = merge_styles(default_style, custom_style) + default_style = _styles.get_styles()['sign_consistent'] + style = _styles.merge_styles(default_style, custom_style) # Call the core visualization function A = self.visualize_network_default(network, source_dict, target_dict, prog, style) @@ -153,7 +162,7 @@ def visualize_network_sign_consistent(self, if condition_style: # Apply condition style without overwriting base style - set_style_attributes(node, {}, condition_style) + _styles.set_style_attributes(node, {}, condition_style) for edge in A.edges(): u, v = edge @@ -168,7 +177,7 @@ def visualize_network_sign_consistent(self, else: edge_style = style['edges']['neutral'] - set_style_attributes(edge, edge_style) + s_styles.et_style_attributes(edge, edge_style) return A @@ -193,7 +202,7 @@ def visualize_network(self, if network_type == 'sign_consistent': return self.visualize_network_sign_consistent(network, source_dict, target_dict, prog, custom_style) else: - default_style = get_styles().get(network_type, get_styles()['default']) + default_style = _styles.get_styles().get(network_type, get_styles()['default']) return self.visualize_network_default(network, source_dict, target_dict, prog, custom_style) def visualize(self, output_file='network.png', render=False, highlight_nodes=None, style=None): @@ -211,7 +220,7 @@ def visualize(self, output_file='network.png', render=False, highlight_nodes=Non highlight_color = style['highlight_color'] else: highlight_color = style['nodes']['default']['fillcolor'] - highlight_nodes = [wrap_node_name(node) for node in highlight_nodes] + highlight_nodes = [_aux.wrap_node_name(node) for node in highlight_nodes] nx.draw_networkx_nodes(self.network, pos, nodelist=highlight_nodes, node_color=highlight_color) if render: diff --git a/networkcommons/visual/vis_yfiles.py b/networkcommons/visual/_vis_yfiles.py similarity index 64% rename from networkcommons/visual/vis_yfiles.py rename to networkcommons/visual/_vis_yfiles.py index f1cf616..3a7fa9a 100644 --- a/networkcommons/visual/vis_yfiles.py +++ b/networkcommons/visual/_vis_yfiles.py @@ -1,36 +1,58 @@ -from yfiles_jupyter_graphs import GraphWidget -from typing import Dict -from IPython.display import display +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Interactive network visualization using the yFiles HTML widget. +""" + +from __future__ import annotations + +__all__ = ['YFilesVisualizer'] + +import lazy_import +yfiles = lazy_import.lazy_module('yfiles_jupyter_graphs') +ipydisplay = lazy_import.lazy_module('IPython.display') + +from . import _yfiles_styles from networkcommons._session import _log -from networkcommons._visual.yfiles_styles import (get_styles, - apply_node_style, - apply_edge_style, - update_node_property, - update_edge_property, - get_edge_color, - get_comparison_color) - class YFilesVisualizer: + def __init__(self, network): + self.network = network.copy() - self.styles = get_styles() + self.styles = _yfiles_styles.get_styles() + def visualize(self, graph_layout="Organic", directed=True): + available_layouts = ["Circular", "Hierarchic", "Organic", "Orthogonal", "Radial", "Tree"] if graph_layout not in available_layouts: graph_layout = "Organic" _log.warning(f"Graph layout not available. Using default layout: {graph_layout}") # creating empty object for visualization - w = GraphWidget() + w = yfiles.GraphWidget() # filling w with nodes node_objects = [] for node in self.network.nodes: + styled_node = self.styles['default']['nodes'] node_objects.append({ "id": node, @@ -45,8 +67,9 @@ def visualize(self, graph_layout="Organic", directed=True): edge_objects = [] for edge in self.network.edges(data=True): + styled_edge = self.styles['default']['edges'] - styled_edge = {"color": get_edge_color(edge[2]['sign'], self.styles)} + styled_edge = {"color": _yfiles_styles.get_edge_color(edge[2]['sign'], self.styles)} edge_objects.append({ "id": (edge[0], edge[1]), "start": edge[0], @@ -64,18 +87,21 @@ def visualize(self, graph_layout="Organic", directed=True): w.directed = directed w.graph_layout = graph_layout - display(w) + ipydisplay.display(w) def vis_comparison(self, int_comparison, node_comparison, graph_layout, directed): # creating empty object for visualization - w = GraphWidget() + w = yfiles.GraphWidget() objects = [] for idx, item in node_comparison.iterrows(): node_props = {"label": item["node"], "comparison": item["comparison"]} - node = apply_node_style(node_props, self.styles['default']['nodes']) - node = update_node_property(node, type="color", - value=get_comparison_color(item["comparison"], self.styles)) + node = _yfiles_styles.apply_node_style(node_props, self.styles['default']['nodes']) + node = _yfiles_styles.update_node_property( + node, + type="color", + value=_yfiles_styles.get_comparison_color(item["comparison"], self.styles), + ) objects.append({ "id": item["node"], "properties": node, @@ -87,9 +113,12 @@ def vis_comparison(self, int_comparison, node_comparison, graph_layout, directed objects = [] for index, row in int_comparison.iterrows(): edge_props = {"comparison": row["comparison"]} - edge = apply_edge_style(edge_props, self.styles['default']['edges']) - edge = update_edge_property(edge, type="color", - value=get_comparison_color(row["comparison"], self.styles)) + edge = _yfiles_styles.apply_edge_style(edge_props, self.styles['default']['edges']) + edge = _yfiles_styles.update_edge_property( + edge, + type="color", + value=_yfiles_styles.get_comparison_color(row["comparison"], self.styles), + ) objects.append({ "id": row["comparison"], "start": row["source"], @@ -109,19 +138,19 @@ def vis_comparison(self, int_comparison, node_comparison, graph_layout, directed display(w) @staticmethod - def custom_edge_color_mapping(edge: Dict): + def custom_edge_color_mapping(edge: dict): return edge["properties"]["color"] @staticmethod - def custom_node_color_mapping(node: Dict): + def custom_node_color_mapping(node: dict): return {"color": node["color"]} @staticmethod - def custom_factor_mapping(node: Dict): + def custom_factor_mapping(node: dict): return 1 @staticmethod - def custom_label_styles_mapping(node: Dict): - label_style = get_styles()['default']['labels'].copy() + def custom_label_styles_mapping(node: dict): + label_style = _yfiles_styles.get_styles()['default']['labels'].copy() label_style['text'] = node["properties"]["label"] return label_style diff --git a/networkcommons/visual/yfiles_styles.py b/networkcommons/visual/_yfiles_styles.py similarity index 86% rename from networkcommons/visual/yfiles_styles.py rename to networkcommons/visual/_yfiles_styles.py index 9cc4844..03829a2 100644 --- a/networkcommons/visual/yfiles_styles.py +++ b/networkcommons/visual/_yfiles_styles.py @@ -1,3 +1,35 @@ +#!/usr/bin/env python + +# +# This file is part of the `networkcommons` Python module +# +# Copyright 2024 +# Heidelberg University Hospital +# +# File author(s): Saez Lab (omnipathdb@gmail.com) +# +# Distributed under the GPLv3 license +# See the file `LICENSE` or read a copy at +# https://www.gnu.org/licenses/gpl-3.0.txt +# + +""" +Interactive network visualization using the yFiles HTML widget. +""" + +from __future__ import annotations + +__all__ = [ + 'get_styles', + 'get_comparison_color', + 'get_edge_color', + 'update_node_property', + 'update_edge_property', + 'apply_node_style', + 'apply_edge_style', +] + + def get_styles(): """ Return a dictionary containing styles for different types of networks specific to yFiles visualizations.