diff --git a/README.md b/README.md index d79ca0b..19fd6a9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ what is planned. | DESeq2 | `fit_deseq2`, `deseq2_wald_test`, `deseq2_likelihood_ratio_test` and `deseq2_dispersions`: DESeq2's analysis with its defaults, from gene-wise dispersions (Cox-Reid adjusted profile likelihood), the parametric trend and the maximum a posteriori dispersions to its fits of every gene, Wald tests of coefficients and contrasts, and likelihood ratio tests of a reduced design (all the levels of a factor at once, a time course), with the columns of `results()`, its independent filtering and its Cook's distance flags | DESeq2, PyDESeq2 | | Empirical Bayes moderation | Moderated t and F statistics, B-statistics, TREAT, with constant, trend and robust priors, estimated as limma estimates them by default: by the method of moments, or, when the residual degrees of freedom differ between genes, by weighted moments and profile likelihood (limma's `fitFDistUnequalDF1`); either can be chosen | limma | | Gene-set tests | `camera` (competitive, allowing for the correlation between the genes of a set, given or estimated per set; parametric or rank-based) and `fry` (self-contained, directional and mixed p-values), for one contrast of a linear model, with voom or other precision weights and trend or robust moderation; the rotation tests `roast` and `mroast` (self-contained, with the mean, floor-mean, mean-50 and mean-square set statistics, contribution weights per gene or per set, and the proportions of active genes) and `romer` (competitive, by the ranks of the genes), with rotations from a seeded generator; gene sets named by gene id or position. Gene sets read from and written to GMT files (MSigDB, Reactome, WikiPathways, the Gene Ontology), with the source, version and licence of the collection, matched to an analysis and filtered by size; over-representation analysis of the genes up, down or either way in a differential-expression result, a table or a list of genes (limma's `kegga` and `goana`), with its dot plot; gene set enrichment analysis (`gsea`) of genes ranked by a statistic, as fgsea computes it: enrichment scores, leading edges, normalized scores, and p-values by fgsea's multilevel method or by permutations, from seeds that a numpy generator draws, with its enrichment plot and table; scores of gene sets in every sample, as the GSVA package computes them: `gsva` (with the Gaussian, Poisson or no kernel, chosen as GSVA chooses it, and its three scores), `ssgsea`, `combined_z_scores` and `plage`, from a matrix or a cell type of a cube, with their heatmap, ready for linear models | limma, R, decoupler, fgsea, GSVA | +| Activities of regulators and pathways | `ulm`, `mlm` and `weighted_aggregate`, as decoupler computes them: the activity of every source of a network (transcription factors, pathways) in every sample, cell type of a cube or group and contrast of a differential-expression result, by a linear model of each source's weights, one model of all of them, or the weighted mean or sum of its targets with permutations; the networks CollecTRI (`collectri`, at a licence level for commercial or academic use) and PROGENy (`progeny`, human and mouse), downloaded from their releases on first use and checked against pinned SHA-256 digests, or any network given as a table; their heatmap and bar plot | decoupler, progeny | | Multiple testing | Bonferroni, Holm, Hochberg, Hommel, Benjamini-Hochberg, Benjamini-Yekutieli | R | | Differential expression | `limma_voom` and `limma_trend`: limma-voom and limma-trend on every group (cell type) of a cube, and `dream_voom`: dream, with the design's random intercepts, such as a donor's, in every group's fits, dream's choice of Kenward-Roger's or Satterthwaite's degrees of freedom by the number of samples in each group, and F-tests of several contrasts, with the design matched to the cube by sample id, samples with too few cells left out, rank-deficient groups fitted on their column space with the contrasts they can estimate, and a provenance record for every result. Options for pseudobulk data, off by default: sample quality weights, prior weights from the number of cells, a minimum total count per sample, and p-values adjusted across cell types | limma, edgeR, variancePartition; on simulated data and on published single-cell data | | Variance partitioning | `variance_fractions`: the fraction of every gene's variance that each variable of a design explains, as variancePartition's `fitExtractVarPartModel`: a mixed model of every gene by maximum likelihood (or REML), with the categorical variables as random intercepts, the continuous ones as fixed effects and precision weights such as voom's, or a linear model without random effects; `variance_partitioning`: the same on every cell type of a cube, with dream's precision weights, the samples each fit used and a provenance record | variancePartition, lme4 | @@ -51,7 +52,6 @@ These are not implemented yet: variance within subsets of the samples. dream with random intercepts is implemented (see the table above); `fit_linear_model` refuses a design whose random effects it would drop, and its message points to `fit_mixed_model`. -- Pathway or transcription-factor activity from regulatory networks (ULM, MLM, VIPER) - Composition tests with precision weights or random effects (crumblr) - Ligand-receptor scoring and differential interactions - Dose-response fits, benchmark doses and synergy @@ -322,6 +322,12 @@ These are not implemented yet: `smooth.spline` keeps some. `differential_expression_replication` reads limma's two-sided p-values with the signs of the log2 fold changes, so it needs a contrast, not an F-test. +- `mlm` builds the design of the genes and sources of each set of profiles that observe + the same genes, and its QR factor: 16 x genes x sources bytes, 190 MB for 15,000 + genes and 800 CollecTRI sources. `collectri` and `progeny` download 4.3 MB and + 10.6 MB once, into `~/.cache/cytepool/networks`; PROGENy names genes by the symbols + of its 2020 release, so data named by current symbols miss the genes renamed since, + which decoupler's `op.progeny` renames through OmniPath. - numpy masked arrays, and array-likes that convert to one such as netCDF4 variables, are refused with a `TypeError` that names the argument, because numpy drops the mask when it converts one and the masked entries would be read as data. Where an diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b56a36a..cc5679a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -108,6 +108,65 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +### decoupler + +`cytepool/activity.py` follows decoupler 2.2.0 (https://github.com/scverse/decoupler) in +how it scores the sources of a network, so that given the same inputs it gives +decoupler's activities: from `mt/_ulm.py`, the regression of each profile over all of +its genes on a source's weights, zero for the genes that are not its targets, and the +t-statistic of the slope from their correlation; from `mt/_mlm.py`, the one model of +all the sources with an intercept and its t-statistics on n - m - 1 degrees of +freedom; from `mt/_waggr.py`, the weighted mean (`_wmean`) and sum (`_wsum`) and the +normalized score of `_perm`, the score less the mean of the permuted scores over their +standard deviation with n - 1; from `mt/_gsea.py`, the permutations of `_ridx`, a +tiled range of the genes shuffled row by row by numpy's generator from the seed; from +`pp/net.py`, the sources left out by `prune` with fewer than `tmin` targets among the +genes; from `pp/data.py`, the features left out by `_validate_mat` that are zero in +every observation; and from `mt/_run.py`, the Benjamini-Hochberg adjustment over the +sources of each observation. `cytepool/networks.py` follows `op/_collectri.py` in the +address of CollecTRI's file and in how it writes the resources and references of each +interaction, and `op/_progeny.py` in keeping PROGENy's genes with a p-value below 0.05. +`plot_top_activities` in `cytepool/plotting.py` chooses and orders its bars as +`pl/_barplot.py` does. Nothing is taken from `mt/_viper.py`. decoupler is described in +Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S, +Taus P, Dugourd A, Holland CH, Ramirez Flores RO, Saez-Rodriguez J (2022). decoupleR: +ensemble of computational methods to infer biological activities from omics data. +Bioinformatics Advances 2, vbac016. doi:10.1093/bioadv/vbac016. It is distributed +under the BSD 3-Clause License: + +```text +BSD 3-Clause License + +Copyright (c) 2025, scverse® +Copyright (c) 2025, Pau Badia i Mompel, Saez lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + ### numba `cytepool/_jit.py` places the cache of its compiled routines with subclasses of numba's @@ -195,3 +254,43 @@ Inc., Massachusetts Institute of Technology, and Regents of the University of California, as its page states. MSigDB versions v6.0 to v7.5.1, and v2022.1 and above, are distributed under the Creative Commons Attribution 4.0 International License (https://www.gsea-msigdb.org/gsea/msigdb_license_terms.jsp). + +### CollecTRI (Mueller-Dott et al. 2023) + +`collectri()` in `cytepool/networks.py` and `validation/activity.py` download +`CollecTRI_regulons.csv`, version 2.0 of Zenodo record 8192729 +(https://zenodo.org/records/8192729, doi:10.5281/zenodo.8192729), into a cache outside +the repository. No row of it is in the repository: `tests/data/activity` holds rows in +its format whose data are simulated. The record is distributed under the Creative +Commons Attribution 4.0 International License; the resources that CollecTRI gathers +keep their own terms, which OmniPath lists (https://omnipathdb.org/resources, read on +26 October 2026): Creative Commons Attribution 4.0 for ExTRI, NTNU Curated, TFactS, +SIGNOR, IntAct, CytReg, GOA and Pavlidis2021, Creative Commons +Attribution-ShareAlike 4.0 for TRRUST, the GNU Lesser General Public License 3.0 for +HTRIdb, an informal licence for DoRothEA's level A, and the Academic Free License 3.0 +for GEREDB, which OmniPath lists for academic use. `collectri(licence="commercial")`, +the default, leaves out the interactions that GEREDB alone supports. + +Mueller-Dott S, Tsirvouli E, Vazquez M, Ramirez Flores RO, Badia-i-Mompel P, Fallegger +R, Tuerei D, Laegreid A, Saez-Rodriguez J (2023). Expanding the coverage of regulons +from high-confidence prior knowledge for accurate estimation of transcription factor +activities. Nucleic Acids Research 51, 10934-10949. doi:10.1093/nar/gkad841 + +### PROGENy (Schubert et al. 2018; Holland, Szalai and Saez-Rodriguez 2020) + +`progeny()` in `cytepool/networks.py` and `validation/activity.py` download the R +package progeny 1.28.0 from Bioconductor 3.20 +(https://bioconductor.org/packages/3.20/bioc/src/contrib/progeny_1.28.0.tar.gz) into a +cache outside the repository and read its data frames `model_human_full` and +`model_mouse_full`. No value of them is in the repository: `tests/data/activity` holds +a package in its layout whose data are simulated. progeny, its data included, is +distributed under the Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0). + +Schubert M, Klinger B, Kluenemann M, Sieber A, Uhlitz F, Sauer S, Garnett MJ, +Bluethgen N, Saez-Rodriguez J (2018). Perturbation-response genes reveal signaling +footprints in cancer gene expression. Nature Communications 9, 20. +doi:10.1038/s41467-017-02391-6 + +Holland CH, Szalai B, Saez-Rodriguez J (2020). Transfer of regulatory knowledge from +human to mouse for functional genomics analysis. Biochimica et Biophysica Acta (BBA) - +Gene Regulatory Mechanisms 1863, 194431. doi:10.1016/j.bbagrm.2019.194431 diff --git a/cytepool/_expression_values.py b/cytepool/_expression_values.py new file mode 100644 index 0000000..5545328 --- /dev/null +++ b/cytepool/_expression_values.py @@ -0,0 +1,298 @@ +"""The expression values that per-sample scores and activities read: a samples x genes +array with its gene and sample ids, or a group of a cube as log-CPM or counts. + +A cube's group is read as limma_voom reads it: the samples with at least ``min_cells`` +cells and some counts, over the genes that edgeR's ``filterByExpr`` keeps +(:func:`~cytepool.normalization.filter_by_expression`), as log2 counts per million +after TMM normalization with a prior count of 2, edgeR's ``cpm(log = TRUE)`` (Robinson +and Oshlack 2010; Chen, Lun and Smyth 2016), or as counts. + +References: + Robinson MD, Oshlack A (2010). A scaling normalization method for differential + expression analysis of RNA-seq data. Genome Biology 11, R25. + doi:10.1186/gb-2010-11-3-r25 + + Chen Y, Lun ATL, Smyth GK (2016). From reads to genes to pathways: differential + expression analysis of RNA-Seq experiments using Rsubread and the edgeR + quasi-likelihood pipeline. F1000Research 5, 1438. doi:10.12688/f1000research.8987.2 +""" + +from typing import Any, NamedTuple + +import numpy as np +import numpy.typing as npt + +from ._columns import text_column +from ._gene_ids import is_missing +from ._masked_arrays import Missing, real_array, reject_masked, unmasked_array +from ._messages import quoted +from ._options import checked_choice, checked_flag, integral_number +from .cube import Cube +from .normalization import filter_by_expression, log_cpm, tmm_factors +from .table import Table + +FloatArray = npt.NDArray[np.float64] + +VALUES = ("log_cpm", "counts") +# As limma_voom leaves samples out, and edgeR's cpm(log = TRUE) shifts counts. +MIN_CELLS = 10 +PRIOR_COUNT = 2.0 + + +class ExpressionValues(NamedTuple): + """The values read, with their samples and genes.""" + + values: FloatArray # samples x genes, NaN where missing + integer: bool # whether the values were stored as integers + samples: Table + gene_ids: list[str | None] # one per column; None for a gene without an id + settings: dict[str, Any] # how a cube's values were taken + + +def expression_values( + data: Any, + gene_ids: Any, + sample_ids: Any, + group: Any, + values: Any, + min_cells: Any, + expression_filter: Any, + *, + missing: Missing = "NaN", + matched_to: str = "the gene sets", + other_forms: str = "", +) -> ExpressionValues: + """The values to score, samples x genes, with their samples and gene ids. + + Args: + data: A 2-D array of values, samples x genes, or a Cube. + gene_ids: For an array, one id per column; for a cube, the name of a column + of its features table (``"feature_id"`` by default) or one id per feature. + sample_ids: For an array, one id per row; by default "1", "2", ... + group: The group of a cube; None for its only one. + values: For a cube, ``"log_cpm"`` or ``"counts"``. + min_cells: For a cube, the fewest cells a sample needs in the group. + expression_filter: For a cube, whether to keep only the genes that + filter_by_expression keeps. + missing: How the values mark missing ones, for messages. + matched_to: What the gene ids are matched to, for messages, such as "the + gene sets". + other_forms: The other forms that ``data`` may take, for messages, such as + ", a DifferentialExpressionResult". + """ + reject_masked(gene_ids, "gene_ids", missing="None") + reject_masked(sample_ids, "sample_ids") + if isinstance(data, Cube): + if sample_ids is not None: + raise ValueError( + "sample_ids applies to an array; a cube's samples have their ids" + ) + return _cube_values( + data, gene_ids, group, values, min_cells, expression_filter, matched_to + ) + defaults = ( + ("group", group is None), + ("values", isinstance(values, str) and values == "log_cpm"), + ( + "min_cells", + isinstance(min_cells, int) + and not isinstance(min_cells, bool) + and min_cells == MIN_CELLS, + ), + ("expression_filter", expression_filter is True), + ) + for name, default in defaults: + if not default: + raise ValueError( + f"{name} applies to a cube, and data is a {type(data).__name__}" + ) + array = unmasked_array(data, "data", missing=missing) + integer = array.dtype.kind in "iu" + try: + matrix = real_array(array, "data", boolean=False, objects=False) + except TypeError: + raise TypeError( + f"data must hold real numbers, samples x genes, or be a Cube{other_forms}; " + f"got dtype {array.dtype}" + ) from None + if matrix.ndim != 2: + raise ValueError( + "data must be a 2-D array of shape (n_samples, n_genes), or a " + f"Cube{other_forms}; got shape {matrix.shape}" + ) + n_samples, n_genes = matrix.shape + if n_samples == 0 or n_genes == 0: + raise ValueError( + f"data must have at least one sample and one gene, got shape {matrix.shape}" + ) + if np.isinf(matrix).any(): + raise ValueError("data holds infinite values; mark missing values with NaN") + if gene_ids is None: + raise ValueError( + "gene_ids is needed with an array: one id per column, by which " + f"{matched_to} name the genes" + ) + if isinstance(gene_ids, str): + raise TypeError( + "gene_ids names a column of a cube's features table; with an array, give " + "one id per column" + ) + ids = _gene_id_list(gene_ids, n_genes, "gene_ids") + _check_unique(ids, "gene_ids", matched_to) + if sample_ids is None: + names = [str(i + 1) for i in range(n_samples)] + else: + names = _sample_id_list(sample_ids, n_samples) + return ExpressionValues( + np.ascontiguousarray(matrix, dtype=np.float64), + integer, + Table({"sample_id": text_column(names)}), + ids, + {}, + ) + + +def _gene_id_list(values: Any, n: int, name: str) -> list[str | None]: + """One gene id per gene, None where missing. + + Raises: + TypeError: If ``values`` is not a sequence, or an id is neither a string nor + missing. + ValueError: If there is not one id per gene. + """ + if isinstance(values, np.ndarray): + found = values.tolist() + else: + try: + found = list(values) + except TypeError: + raise TypeError( + f"{name} must be a sequence of gene ids, got {type(values).__name__}" + ) from None + if len(found) != n: + raise ValueError(f"{name} must hold one id per gene ({n}), got {len(found)}") + ids: list[str | None] = [] + for value in found: + if isinstance(value, str) and value: + ids.append(str(value)) + elif is_missing(value): + ids.append(None) + else: + raise TypeError( + f"{name} must hold gene ids as strings, got {type(value).__name__} " + f"{value!r}; convert numeric ids, such as Entrez ids, to text first" + ) + return ids + + +def _cube_values( + cube: Cube, + gene_ids: Any, + group: Any, + values: Any, + min_cells: Any, + expression_filter: Any, + matched_to: str, +) -> ExpressionValues: + """A cube group's log-CPM or counts, of its samples with enough cells and counts, + over the genes kept by filterByExpr.""" + kind = checked_choice(values, "values", VALUES) + fewest = integral_number(min_cells, "min_cells") + if fewest < 0: + raise ValueError(f"min_cells must be at least 0, got {fewest}") + filtered = checked_flag(expression_filter, "expression_filter") + if group is None: + if cube.n_groups != 1: + raise ValueError( + f"the cube has {cube.n_groups} groups; name the one to score with " + f"group, one of {quoted(cube.groups['group_id'].tolist())}" + ) + group = cube.groups["group_id"][0] + if not isinstance(group, str): + reject_masked(group, "group") + raise TypeError(f"group must be a group id (str), got {type(group).__name__}") + counts = cube.layer("counts", group) + used = np.asarray(counts.sum(axis=1, dtype=np.float64) > 0.0) + if cube.n_cells is not None: + position = cube.groups["group_id"].tolist().index(group) + used &= np.asarray(cube.n_cells)[:, position] >= fewest + rows = np.flatnonzero(used) + if rows.size == 0: + raise ValueError( + f"no sample of group {group!r} has counts and at least {fewest} cells" + ) + kept = counts[rows] + genes = np.ones(kept.shape[1], dtype=bool) + if filtered: + genes = filter_by_expression(kept, group=np.zeros(rows.size)) + if not genes.any(): + raise ValueError( + f"no gene of group {group!r} passes filter_by_expression; pass " + "expression_filter=False to score every gene" + ) + kept = kept[:, genes] + if kind == "log_cpm": + matrix = log_cpm(kept, norm_factors=tmm_factors(kept), prior_count=PRIOR_COUNT) + integer = False + else: + matrix = np.asarray(kept, dtype=np.float64) + integer = kept.dtype.kind in "iu" + if gene_ids is None: + gene_ids = "feature_id" + if isinstance(gene_ids, str): + features = cube.features + if gene_ids not in features: + raise KeyError( + f"the cube's features table has no column {gene_ids!r}; its columns " + f"are {quoted(list(features.columns))}" + ) + label = f"the features column {gene_ids!r}" + ids = _gene_id_list(features[gene_ids], cube.n_features, label) + else: + label = "gene_ids" + ids = _gene_id_list(gene_ids, cube.n_features, label) + ids = [ids[j] for j in np.flatnonzero(genes).tolist()] + _check_unique(ids, label, matched_to) + settings = { + "group": group, + "values": kind, + "min_cells": fewest, + "expression_filter": filtered, + } + return ExpressionValues( + np.ascontiguousarray(matrix, dtype=np.float64), + integer, + cube.samples.take(rows), + ids, + settings, + ) + + +def _check_unique(ids: list[str | None], name: str, matched_to: str) -> None: + """Refuse an id that names two of the genes scored.""" + seen: set[str] = set() + for gene in ids: + if gene is not None: + if gene in seen: + raise ValueError( + f"{name} names two of the genes scored {gene!r}; each gene needs " + f"an id of its own to be matched to {matched_to}: keep one gene " + f"per id, or match {matched_to} by other ids" + ) + seen.add(gene) + + +def _sample_id_list(values: Any, n: int) -> list[str]: + array = unmasked_array(values, "sample_ids") + if array.ndim != 1 or array.size != n: + raise ValueError( + f"sample_ids must hold one id per sample ({n}), got shape {array.shape}" + ) + names = [] + for value in array.tolist(): + if is_missing(value): + raise ValueError("sample_ids has a missing id; give every sample one") + names.append(str(value)) + if len(set(names)) != n: + raise ValueError("sample_ids must be unique") + return names diff --git a/cytepool/activity.py b/cytepool/activity.py new file mode 100644 index 0000000..4b72a2a --- /dev/null +++ b/cytepool/activity.py @@ -0,0 +1,1372 @@ +"""Activities of transcription factors and pathways, inferred from the expression of +their target genes, as decoupler computes them: the univariate and multivariate +linear models (ULM, MLM) and the weighted aggregate. + +A network (:class:`~cytepool.networks.Network`), such as CollecTRI's regulons or +PROGENy's pathway signatures (:mod:`cytepool.networks`), gives each source, a +transcription factor or a pathway, a signed weight on each of its target genes. A +source is active in a profile, the expression of a sample or the statistics of a +contrast, when its targets are high where their weights are positive and low where +they are negative. Each function scores every source in every profile, as decoupler +2.2.0's ``mt.ulm``, ``mt.mlm`` and ``mt.waggr`` do (Badia-i-Mompel et al. 2022), and +returns an :class:`Activities`, profiles x sources, which the linear models take as +they take pathway scores, and which :func:`~cytepool.plotting.plot_activities` draws +as a heatmap and :func:`~cytepool.plotting.plot_top_activities` as bars. + +The profiles: + - A samples x genes array, one profile per row, with one gene id per column. + - A group (cell type) of a :class:`~cytepool.cube.Cube`: its samples with at + least ``min_cells`` cells and some counts, over the genes that edgeR's + ``filterByExpr`` keeps, as log2 counts per million after TMM normalization, + the values that :func:`~cytepool.pathway_scores.gsva` scores. + - The statistics of a + :class:`~cytepool.differential_expression.DifferentialExpressionResult`: one + profile for every group and contrast tested, by default the moderated + t-statistics, as decoupler's guides score the statistics of a contrast; or of a + table of results, a pandas Series or a mapping of gene id to statistic, one + profile. + + Each profile's genes are those with an id and a value: NaN marks a missing value, + and a gene that a group did not test has none in that group's profiles. Genes + whose values are zero in every profile are left out, as decoupler leaves them + out: in sparse data they are genes that were not detected. + +The network's targets: + Targets are matched to the genes' ids exactly. An edge whose weight is zero is no + edge. A source with fewer than ``min_targets`` targets among a profile's genes + has no score there (NaN), and a source with fewer in every profile is left out, + as decoupler's ``tmin`` leaves it out; ``n_targets`` gives each source's targets + among each profile's genes. + +ULM (:func:`ulm`): + For each source and profile, a linear model of the profile's n genes on the + source's weights, zero for the genes that are not its targets: + + y_g = b_0 + b_1 x_g + e_g, + + fitted by least squares. The activity score is the t-statistic of the slope, + t = b_1 / se(b_1) = r sqrt((n - 2) / (1 - r^2)), with r the correlation of y and + x, and its p-value is two-sided, from Student's t distribution on n - 2 degrees + of freedom. ``estimates`` are the slopes b_1. + +MLM (:func:`mlm`): + One linear model of each profile's genes on the weights of all its sources at + once, with an intercept, y_g = b_0 + sum_s b_s x_gs + e_g, so that each source's + score, the t-statistic of b_s on n - m - 1 degrees of freedom for m sources, is + that of its targets beyond what the other sources explain. The model is fitted by + a QR decomposition. Sources whose weights over the genes are linear combinations + of the weights of other sources, such as two sources with the same targets and + weights, cannot be told apart, and are refused: the message names them. + +Weighted aggregate (:func:`weighted_aggregate`): + The weighted mean of the values of a source's targets, sum_g w_g y_g / + sum_g |w_g|, or their weighted sum, sum_g w_g y_g, the score of the original + PROGENy method (Schubert et al. 2018). With ``n_permutations`` random + permutations of the genes, each permutation giving every gene the value of + another, as decoupler draws them (its ``seed`` is ``rng``), the score is + normalized by the scores of the permutations, (ES - mean) / sd, the standard + deviation with n - 1 in its denominator, and the p-value doubles the smaller + of the two one-sided permutation p-values, (1 + b) / (1 + n_permutations), with b + the permutations whose score is at least, or at most, the observed one (Phipson + and Smyth 2010). ``estimates`` keep the weighted means or sums. + +Adjusted p-values: + ``adjusted_p_values`` are Benjamini and Hochberg's (1995) over the sources of each + profile, as decoupler adjusts them. + +Deliberate differences from decoupler 2.2.0: + - decoupler reports MLM's p-values without adjusting them, under the name of its + adjusted p-values; here they are ``p_values``, and ``adjusted_p_values`` are + adjusted as for the other methods. + - decoupler keeps adjusted p-values in single precision, and computes MLM's + p-values as one minus the distribution function, which gives 0 below about + 1e-16; here both are in double precision, from the upper tail. + - decoupler counts the permutations of the weighted aggregate whose absolute + score exceeds the absolute observed score, and then folds that share as it + would fold a one-sided one, so that a score near zero, which most permutations + exceed, gets a p-value near 2 / n_permutations; here the p-value doubles the + smaller one-sided p-value. When every permutation scores alike, decoupler's + normalized score is infinite, or 0 for a score of 0; here it is NaN. + - decoupler adds 2.2e-16 to 1 - r and 1 + r in ULM's t-statistic; here it is + exact, which changes the score only for |r| within about 1e-6 of 1. + - decoupler refuses missing values and leaves out samples whose values are all + zero; here each profile uses its observed genes, and a profile without genes + has NaN scores. Where MLM cannot separate sources, decoupler stops with a + singular matrix; here the message names them. + +References: + Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S, + Taus P, Dugourd A, Holland CH, Ramirez Flores RO, Saez-Rodriguez J (2022). + decoupleR: ensemble of computational methods to infer biological activities from + omics data. Bioinformatics Advances 2, vbac016. doi:10.1093/bioadv/vbac016 + + Schubert M, Klinger B, Kluenemann M, Sieber A, Uhlitz F, Sauer S, Garnett MJ, + Bluethgen N, Saez-Rodriguez J (2018). Perturbation-response genes reveal + signaling footprints in cancer gene expression. Nature Communications 9, 20. + doi:10.1038/s41467-017-02391-6 + + Phipson B, Smyth GK (2010). Permutation p-values should never be zero: calculating + exact p-values when permutations are randomly drawn. Statistical Applications in + Genetics and Molecular Biology 9, article 39. doi:10.2202/1544-6115.1585 + + Benjamini Y, Hochberg Y (1995). Controlling the false discovery rate: a practical + and powerful approach to multiple testing. Journal of the Royal Statistical + Society B 57, 289-300. doi:10.1111/j.2517-6161.1995.tb02031.x + + Welford BP (1962). Note on a method for calculating corrected sums of squares and + products. Technometrics 4, 419-420. doi:10.1080/00401706.1962.10490022 +""" + +import math +from collections.abc import Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any, NamedTuple + +import numpy as np +import numpy.typing as npt +from numba import prange + +from ._columns import is_data_frame, text_column +from ._expression_values import MIN_CELLS, expression_values +from ._jit import by_chunks, jit, thread_limit +from ._linear_algebra import RANK_TOLERANCE +from ._masked_arrays import read_only, reject_masked +from ._messages import listing, quoted +from ._options import ( + checked_choice, + integral_number, + positive_integer, + random_generator, +) +from ._ranked_genes import ranked_genes +from .differential_expression import DifferentialExpressionResult +from .networks import Network +from .stats.distributions import t_sf +from .stats.multiple_testing import adjust_p_values +from .table import Table + +__all__ = ["Activities", "mlm", "ulm", "weighted_aggregate"] + +FloatArray = npt.NDArray[np.float64] +IntArray = npt.NDArray[np.int64] + +_MIN_TARGETS = 5 # decoupler's tmin +_N_PERMUTATIONS = 1000 # decoupler's times +_STATISTICS = ("mean", "sum") +_BLOCK = 128 # permutations drawn and scored at a time +_PROFILE_ID = "profile_id" + + +@dataclass(frozen=True, eq=False, repr=False) +class Activities: + """Activities of the sources of a network in every profile. + + Rows of the arrays are profiles, in the order of ``profiles``, and columns are + sources, in the order of ``network.sources``, so that + :func:`~cytepool.stats.linear_models.fit_linear_model` takes ``scores`` as its + values, with a design built from ``profiles``. The arrays are read-only. + + Attributes: + scores: ``(n_profiles, n_sources)`` activity scores: the t-statistics of ULM + and MLM, and of the weighted aggregate the scores normalized by + permutations, or, without them, the weighted means or sums. NaN where a + source has fewer than ``min_targets`` targets among a profile's genes. + estimates: The slopes (ULM), coefficients (MLM) or weighted means or sums + (weighted aggregate). + p_values: Two-sided p-values: of the t-tests (ULM, MLM), or of the + permutations (weighted aggregate; NaN without them). + adjusted_p_values: Benjamini and Hochberg's, over the sources of each + profile. + n_targets: ``(n_profiles, n_sources)`` int64: the targets of each source + among each profile's genes. + profiles: One row per profile: ``profile_id`` and, for a cube's group, the + columns of the cube's sample table, or, for a differential-expression + result, ``group_id`` and ``contrast``. + network: The sources scored, with their targets among the genes and the + record of the network given. + method: ``"ulm"``, ``"mlm"`` or ``"weighted_aggregate"``. + settings: The options of the method and how the profiles were read. + """ + + scores: FloatArray + estimates: FloatArray + p_values: FloatArray + adjusted_p_values: FloatArray + n_targets: IntArray + profiles: Table + network: Network + method: str + settings: Mapping[str, Any] + + @property + def sources(self) -> tuple[str, ...]: + """The sources scored, in the order of the columns.""" + return self.network.sources + + def table(self) -> Table: + """Return the activities as a table with one row per source and profile. + + Returns: + A table with ``source``, ``profile_id``, ``n_targets``, ``score``, + ``estimate``, ``p_value`` and ``adjusted_p_value``, the rows of each + source together, in the order of ``sources``, and its profiles in the + order of ``profiles``. + """ + n_profiles, n_sources = self.scores.shape + ids = self.profiles[_PROFILE_ID].tolist() + return Table( + { + "source": text_column([source for source in self.sources for _ in ids]), + "profile_id": text_column(ids * n_sources), + "n_targets": self.n_targets.T.ravel().copy(), + "score": self.scores.T.ravel().copy(), + "estimate": self.estimates.T.ravel().copy(), + "p_value": self.p_values.T.ravel().copy(), + "adjusted_p_value": self.adjusted_p_values.T.ravel().copy(), + } + ) + + def __repr__(self) -> str: + n_profiles, n_sources = self.scores.shape + return ( + f"Activities(method={self.method!r}, n_profiles={n_profiles}, " + f"n_sources={n_sources})" + ) + + +class _Profiles(NamedTuple): + """The values scored, profiles x genes, with their genes and profiles.""" + + values: FloatArray # NaN where a profile has no value of a gene + gene_ids: list[str | None] + profiles: Table + settings: dict[str, Any] + + +class _Edges(NamedTuple): + """The network's edges among the genes kept: those of each source after another, + by the position of their gene among the genes kept.""" + + starts: IntArray # where each source's edges start, and where the last ends + genes: IntArray + weights: FloatArray + rows: IntArray # the rows of the network's table + + +def ulm( + data: Any, + network: Any, + *, + gene_ids: Any = None, + sample_ids: Any = None, + group: str | None = None, + contrast: str | None = None, + min_cells: int = MIN_CELLS, + expression_filter: bool = True, + id_column: str = "feature_id", + statistic_column: str = "t", + min_targets: int = _MIN_TARGETS, + n_threads: int | None = None, +) -> Activities: + """Score the sources of a network in every profile by univariate linear models + (ULM). + + As decoupler's ``mt.ulm`` (Badia-i-Mompel et al. 2022): for each source, the + values of a profile's genes are regressed on the source's weights, zero for the + genes that are not its targets, and the score is the t-statistic of the slope. + The module docstring gives the profiles, the matching of targets and the + deliberate differences from decoupler. + + Args: + data: ``(n_profiles, n_genes)`` values, such as log-expression values of + samples or statistics of contrasts, NaN where missing; a + :class:`~cytepool.cube.Cube`, whose ``group`` is scored; a + :class:`~cytepool.differential_expression.DifferentialExpressionResult`, + whose statistics of every group and contrast are scored; or a table of + results (a ``Table`` or pandas DataFrame), a pandas Series indexed by + gene id or a mapping of gene id to statistic, one profile. A samples x + genes DataFrame, as decoupler takes it, is given as + ``frame.to_numpy()`` with ``gene_ids`` and ``sample_ids``. + network: A :class:`~cytepool.networks.Network`, such as + :func:`~cytepool.networks.collectri` or + :func:`~cytepool.networks.progeny` returns, or what ``Network`` takes: a + table with the columns ``source``, ``target`` and ``weight``, or a + mapping of source to targets. + gene_ids: For an array, the genes' ids, one per column (None, NaN or an + empty string for a gene without one); for a cube, the name of a column of + its features table, ``"feature_id"`` by default or ``"symbol"``, or one + id per feature. + sample_ids: For an array, one id per row; by default ``"1"``, ``"2"``, ... + group: For a cube, the group (cell type) to score; it may be left out when + the cube has one. For a differential-expression result, the group whose + statistics are scored; by default every group. + contrast: For a differential-expression result, the contrast whose + statistics are scored; by default every contrast. + min_cells: For a cube, the fewest cells a sample needs in the group to be + scored, at least 0, as for + :func:`~cytepool.differential_expression.limma_voom`. + expression_filter: For a cube, whether to score only the genes that + :func:`~cytepool.normalization.filter_by_expression` keeps. + id_column: For a differential-expression result or a table, the column of + gene ids, such as ``"feature_id"`` or ``"symbol"``. + statistic_column: For a differential-expression result or a table, the + column of statistics, such as ``"t"`` or ``"log2_fold_change"``. + min_targets: The fewest targets among a profile's genes for a source to be + scored, at least 1 (decoupler's ``tmin``). + n_threads: The most threads to use; None uses numba's current setting. The + result does not depend on it. + + Returns: + The activities, with the t-statistics as ``scores`` and the slopes as + ``estimates``. + + Raises: + TypeError: If an argument has the wrong type, values are not real numbers, + gene ids are not strings, or an argument is or holds a numpy masked array. + ValueError: If the values are not 2-D or hold an infinite value, the ids do + not match the values, an id names two genes, an option is out of range or + applies to another kind of data, a table lacks a column, or no source has + ``min_targets`` targets among the genes. + KeyError: If the cube has no such group or features column, or the result + has no such group or contrast. + + Example: + >>> import numpy as np + >>> from cytepool.networks import Network + >>> rng = np.random.default_rng(1) + >>> genes = [f"g{j}" for j in range(40)] + >>> y = rng.normal(size=(2, 40)) + >>> y[1, :6] += 3.0 # the targets of A are up in the second sample + >>> network = Network({"A": genes[:6], "B": genes[10:16]}) + >>> ulm(y, network, gene_ids=genes).scores.round(2).tolist() + [[0.71, -0.1], [7.94, -0.85]] + """ + profiles, graph, edges, smallest = _prepared( + data, + network, + gene_ids, + sample_ids, + group, + contrast, + min_cells, + expression_filter, + id_column, + statistic_column, + min_targets, + ) + values = profiles.values + n_profiles = values.shape[0] + n_sources = edges.starts.size - 1 + slopes = np.full((n_profiles, n_sources), np.nan) + t_values = np.full((n_profiles, n_sources), np.nan) + counts = np.zeros((n_profiles, n_sources), dtype=np.int64) + with thread_limit(n_threads): + by_chunks( + _ulm_parallel, + _ulm_serial, + (values, edges.starts, edges.genes, edges.weights, smallest, slopes, + t_values, counts), + n_profiles, + ) # fmt: skip + df = np.sum(~np.isnan(values), axis=1).astype(np.float64) - 2.0 + return _activities( + "ulm", + t_values, + slopes, + _two_sided(t_values, df), + counts, + profiles, + graph, + edges, + {"min_targets": smallest}, + ) + + +def mlm( + data: Any, + network: Any, + *, + gene_ids: Any = None, + sample_ids: Any = None, + group: str | None = None, + contrast: str | None = None, + min_cells: int = MIN_CELLS, + expression_filter: bool = True, + id_column: str = "feature_id", + statistic_column: str = "t", + min_targets: int = _MIN_TARGETS, +) -> Activities: + """Score the sources of a network in every profile by one multivariate linear + model (MLM). + + As decoupler's ``mt.mlm`` (Badia-i-Mompel et al. 2022): the values of a + profile's genes are regressed on the weights of all the sources at once, with an + intercept, and each source's score is the t-statistic of its coefficient, which + measures its targets beyond what the other sources explain. The module docstring + gives the profiles, the matching of targets and the deliberate differences from + decoupler. + + Args: + data: The profiles, as for :func:`ulm`. + network: The network, as for :func:`ulm`. + gene_ids: For an array or a cube, as for :func:`ulm`. + sample_ids: For an array, as for :func:`ulm`. + group: For a cube or a differential-expression result, as for :func:`ulm`. + contrast: For a differential-expression result, as for :func:`ulm`. + min_cells: For a cube, as for :func:`ulm`. + expression_filter: For a cube, as for :func:`ulm`. + id_column: For a differential-expression result or a table, as for + :func:`ulm`. + statistic_column: For a differential-expression result or a table, as for + :func:`ulm`. + min_targets: The fewest targets among a profile's genes for a source to be + in its model, at least 1 (decoupler's ``tmin``). + + Returns: + The activities, with the t-statistics as ``scores`` and the coefficients as + ``estimates``. + + Raises: + TypeError: If an argument has the wrong type, values are not real numbers, + gene ids are not strings, or an argument is or holds a numpy masked array. + ValueError: If the values are not 2-D or hold an infinite value, the ids do + not match the values, an id names two genes, an option is out of range or + applies to another kind of data, a table lacks a column, no source has + ``min_targets`` targets among the genes, a profile has no more genes than + sources, or the weights of some sources are linear combinations of + those of others. + KeyError: If the cube has no such group or features column, or the result + has no such group or contrast. + """ + profiles, graph, edges, smallest = _prepared( + data, + network, + gene_ids, + sample_ids, + group, + contrast, + min_cells, + expression_filter, + id_column, + statistic_column, + min_targets, + ) + values = profiles.values + n_profiles = values.shape[0] + n_sources = edges.starts.size - 1 + coefficients = np.full((n_profiles, n_sources), np.nan) + t_values = np.full((n_profiles, n_sources), np.nan) + p_values = np.full((n_profiles, n_sources), np.nan) + counts = np.zeros((n_profiles, n_sources), dtype=np.int64) + observed = ~np.isnan(values) + patterns: dict[bytes, list[int]] = {} + for row in range(n_profiles): + patterns.setdefault(np.packbits(observed[row]).tobytes(), []).append(row) + names = list(graph.sources) + for rows in patterns.values(): + _fit_pattern( + values, observed, rows, edges, smallest, names, profiles.profiles, + coefficients, t_values, p_values, counts, + ) # fmt: skip + return _activities( + "mlm", + t_values, + coefficients, + p_values, + counts, + profiles, + graph, + edges, + {"min_targets": smallest}, + ) + + +def weighted_aggregate( + data: Any, + network: Any, + *, + statistic: str = "mean", + n_permutations: int = _N_PERMUTATIONS, + rng: np.random.Generator | int | None = None, + gene_ids: Any = None, + sample_ids: Any = None, + group: str | None = None, + contrast: str | None = None, + min_cells: int = MIN_CELLS, + expression_filter: bool = True, + id_column: str = "feature_id", + statistic_column: str = "t", + min_targets: int = _MIN_TARGETS, + n_threads: int | None = None, +) -> Activities: + """Score the sources of a network in every profile by the weighted mean or sum of + their targets' values. + + As decoupler's ``mt.waggr`` (Badia-i-Mompel et al. 2022): the weighted mean of + the values of a source's targets, sum w y / sum |w|, or their weighted sum, + sum w y, which is how PROGENy scores its pathways (Schubert et al. 2018); with + permutations of the genes, normalized by the scores of the permutations, with a + permutation p-value. The module docstring gives the permutations, the p-values + and the deliberate differences from decoupler. + + Args: + data: The profiles, as for :func:`ulm`. + network: The network, as for :func:`ulm`. + statistic: ``"mean"``, the weighted mean (decoupler's ``"wmean"``), or + ``"sum"``, the weighted sum (``"wsum"``). + n_permutations: The random permutations of the genes, 0 for none or at least + 2 (decoupler's ``times``). + rng: A ``numpy.random.Generator`` to draw the permutations from, an integer + seed for one (decoupler's ``seed``, 42 by default there), or None for a + fresh, unseeded generator. + gene_ids: For an array or a cube, as for :func:`ulm`. + sample_ids: For an array, as for :func:`ulm`. + group: For a cube or a differential-expression result, as for :func:`ulm`. + contrast: For a differential-expression result, as for :func:`ulm`. + min_cells: For a cube, as for :func:`ulm`. + expression_filter: For a cube, as for :func:`ulm`. + id_column: For a differential-expression result or a table, as for + :func:`ulm`. + statistic_column: For a differential-expression result or a table, as for + :func:`ulm`. + min_targets: The fewest targets among a profile's genes for a source to be + scored, at least 1 (decoupler's ``tmin``). + n_threads: The most threads to use; None uses numba's current setting. The + result does not depend on it. + + Returns: + The activities, with the normalized scores as ``scores`` (the weighted means + or sums without permutations) and the weighted means or sums as + ``estimates``. + + Raises: + TypeError: If an argument has the wrong type, values are not real numbers, + gene ids are not strings, or an argument is or holds a numpy masked array. + ValueError: If the values are not 2-D or hold an infinite value, the ids do + not match the values, an id names two genes, an option is out of range or + applies to another kind of data, a table lacks a column, or no source has + ``min_targets`` targets among the genes. + KeyError: If the cube has no such group or features column, or the result + has no such group or contrast. + """ + kind = checked_choice(statistic, "statistic", _STATISTICS) + reject_masked(n_permutations, "n_permutations") + count = integral_number(n_permutations, "n_permutations") + if count < 0 or count == 1: + raise ValueError( + f"n_permutations must be 0, for no permutations, or at least 2, got {count}" + ) + generator = random_generator(rng) + profiles, graph, edges, smallest = _prepared( + data, + network, + gene_ids, + sample_ids, + group, + contrast, + min_cells, + expression_filter, + id_column, + statistic_column, + min_targets, + ) + values = profiles.values + n_profiles, n_genes = values.shape + n_sources = edges.starts.size - 1 + mean = kind == "mean" + estimates = np.full((n_profiles, n_sources), np.nan) + counts = np.zeros((n_profiles, n_sources), dtype=np.int64) + with thread_limit(n_threads): + identity = np.arange(n_genes, dtype=np.int64)[np.newaxis, :] + _aggregate(values, edges, identity, mean, smallest, estimates, counts) + scores = estimates.copy() + p_values = np.full((n_profiles, n_sources), np.nan) + if count: + scores, p_values = _permuted( + values, edges, mean, smallest, estimates, count, generator + ) + settings = { + "statistic": kind, + "n_permutations": count, + "min_targets": smallest, + } + return _activities( + "weighted_aggregate", + scores, + estimates, + p_values, + counts, + profiles, + graph, + edges, + settings, + ) + + +# The profiles and the network. + + +def _prepared( + data: Any, + network: Any, + gene_ids: Any, + sample_ids: Any, + group: Any, + contrast: Any, + min_cells: Any, + expression_filter: Any, + id_column: Any, + statistic_column: Any, + min_targets: Any, +) -> tuple[_Profiles, Network, _Edges, int]: + """The profiles over the genes kept, the network, its edges among those genes, + and the fewest targets a source needs.""" + reject_masked(min_targets, "min_targets") + smallest = positive_integer(min_targets, "min_targets") + graph = network if isinstance(network, Network) else _network(network) + profiles = _profiles( + data, + gene_ids, + sample_ids, + group, + contrast, + min_cells, + expression_filter, + id_column, + statistic_column, + ) + values = profiles.values + observed = ~np.isnan(values) + named = np.array([gene is not None for gene in profiles.gene_ids], dtype=bool) + kept = np.flatnonzero(named & np.any(observed & (values != 0.0), axis=0)) + ids = [profiles.gene_ids[j] for j in kept.tolist()] + edges = _matched_edges(graph, ids, profiles.gene_ids) + kept_profiles = _Profiles( + np.ascontiguousarray(values[:, kept]), + list(ids), + profiles.profiles, + profiles.settings, + ) + return kept_profiles, graph, edges, smallest + + +def _network(network: Any) -> Network: + """A network given as a table or a mapping, which Network checks.""" + if isinstance(network, Table | Mapping) or is_data_frame(network): + return Network(network) + reject_masked(network, "network") + raise TypeError( + "network must be a Network, such as collectri() or progeny() returns, a " + "table with the columns 'source', 'target' and 'weight', or a mapping of " + f"source to targets; got {type(network).__name__}" + ) + + +def _matched_edges( + network: Network, ids: list[str | None], all_ids: list[str | None] +) -> _Edges: + """The edges of every source of the network whose target is among the genes kept + and whose weight is not zero, by source in the network's order.""" + position = {gene: j for j, gene in enumerate(ids)} + edges = network.edges + sources = edges["source"].tolist() + targets = edges["target"].tolist() + weights = np.asarray(edges["weight"], dtype=np.float64) + by_source: dict[str, list[int]] = {source: [] for source in network.sources} + for row, (source, target) in enumerate(zip(sources, targets, strict=True)): + if target in position and weights[row] != 0.0: + by_source[source].append(row) + rows = [row for source in network.sources for row in by_source[source]] + if not rows: + examples = list(dict.fromkeys(targets))[:3] + genes = [gene for gene in all_ids if gene is not None][:3] + raise ValueError( + "no target of the network is among the genes; the network and the data " + "must name genes the same way (for example both by symbol, or both by " + f"Ensembl id): the network names genes such as {quoted(examples)}, the " + f"data genes such as {quoted(genes)}" + ) + starts = np.zeros(len(network.sources) + 1, dtype=np.int64) + starts[1:] = np.cumsum([len(by_source[source]) for source in network.sources]) + taken = np.array(rows, dtype=np.int64) + return _Edges( + starts, + np.array([position[targets[row]] for row in rows], dtype=np.int64), + np.ascontiguousarray(weights[taken]), + taken, + ) + + +def _profiles( + data: Any, + gene_ids: Any, + sample_ids: Any, + group: Any, + contrast: Any, + min_cells: Any, + expression_filter: Any, + id_column: Any, + statistic_column: Any, +) -> _Profiles: + """The profiles of an array, a cube's group or a set of statistics.""" + statistics = ( + isinstance(data, DifferentialExpressionResult | Table | Mapping) + or is_data_frame(data) + or _is_series(data) + ) + if statistics: + for name, value, default in ( + ("gene_ids", gene_ids, gene_ids is None), + ("sample_ids", sample_ids, sample_ids is None), + ("min_cells", min_cells, _is_default_cells(min_cells)), + ("expression_filter", expression_filter, expression_filter is True), + ): + if not default: + reject_masked(value, name) + hint = "" + if name == "gene_ids": + hint = "; name the column of gene ids with id_column" + raise ValueError( + f"{name} applies to an array or a cube, and data is a " + f"{type(data).__name__}{hint}" + ) + return _statistics(data, group, contrast, id_column, statistic_column) + for name, given, expected in ( + ("contrast", contrast, None), + ("id_column", id_column, "feature_id"), + ("statistic_column", statistic_column, "t"), + ): + if expected is None: + same = given is None + else: + same = isinstance(given, str) and given == expected + if not same: + reject_masked(given, name) + raise ValueError( + f"{name} applies to the statistics of a differential-expression " + f"result or a table, and data is a {type(data).__name__}" + ) + expression = expression_values( + data, + gene_ids, + sample_ids, + group, + "log_cpm", + min_cells, + expression_filter, + matched_to="the network's targets", + other_forms=( + ", a DifferentialExpressionResult, a table of statistics or a mapping of " + "gene id to statistic" + ), + ) + samples = expression.samples + if _PROFILE_ID in samples: + raise ValueError( + f"the sample table has a column {_PROFILE_ID!r}, the name the activities " + "give the ids of their profiles; rename it" + ) + ids = samples["sample_id"] + table = Table({_PROFILE_ID: ids, **samples.to_dict()}) + return _Profiles( + expression.values, expression.gene_ids, table, dict(expression.settings) + ) + + +def _statistics( + data: Any, group: Any, contrast: Any, id_column: Any, statistic_column: Any +) -> _Profiles: + """The statistics of every group and contrast of a result, or of a table or + mapping, one profile each, over the union of their genes.""" + ranked = [] + if isinstance(data, DifferentialExpressionResult): + groups = list(data.groups) + contrasts = list(data.contrasts.names) + for value, name, options in ( + (group, "group", groups), + (contrast, "contrast", contrasts), + ): + if value is not None: + if not isinstance(value, str): + reject_masked(value, name) + raise TypeError( + f"{name} must be a string, got {type(value).__name__}" + ) + if value not in options: + raise KeyError( + f"the result has no {name} {value!r}; its {name}s are " + f"{quoted(options)}" + ) + pairs = [ + (g, c) + for g in groups + for c in contrasts + if (g, c) in data.tables + and (group is None or g == group) + and (contrast is None or c == contrast) + ] + if not pairs: + raise ValueError( + "the result has no table of " + + (f"group {group!r}" if group is not None else "any group") + + " and " + + (f"contrast {contrast!r}" if contrast is not None else "any contrast") + ) + for g, c in pairs: + ranked.append( + ranked_genes( + data, + group=g, + contrast=c, + id_column=id_column, + statistic_column=statistic_column, + ) + ) + if len(contrasts) == 1 or contrast is not None: + ids = [g for g, _ in pairs] + elif len(groups) == 1 or group is not None: + ids = [c for _, c in pairs] + else: + ids = [f"{g}: {c}" for g, c in pairs] + table = Table( + { + _PROFILE_ID: text_column(ids), + "group_id": text_column([g for g, _ in pairs]), + "contrast": text_column([c for _, c in pairs]), + } + ) + else: + if (isinstance(data, Table) or is_data_frame(data)) and not ( + _has_column(data, id_column) or _has_column(data, statistic_column) + ): + raise ValueError( + f"data is read as a table of results, with a column of gene ids " + f"({id_column!r}) and one of statistics ({statistic_column!r}), and " + "it has neither. To score a samples x genes table, give its values " + "with their ids: data=frame.to_numpy(), gene_ids=list(frame.columns), " + "sample_ids=list(frame.index)" + ) + ranked.append( + ranked_genes( + data, + group=group, + contrast=contrast, + id_column=id_column, + statistic_column=statistic_column, + ) + ) + if isinstance(data, Table) or is_data_frame(data): + label = str(statistic_column) + elif _is_series(data) and data.name is not None: + label = str(data.name) + else: + label = "statistic" + table = Table({_PROFILE_ID: text_column([label])}) + genes = list(dict.fromkeys(gene for found in ranked for gene in found.ids)) + position = {gene: j for j, gene in enumerate(genes)} + values = np.full((len(ranked), len(genes)), np.nan) + for row, found in enumerate(ranked): + columns = np.array([position[gene] for gene in found.ids], dtype=np.intp) + values[row, columns] = found.values + settings = {"id_column": id_column, "statistic_column": statistic_column} + return _Profiles(values, list(genes), table, settings) + + +def _has_column(data: Any, name: Any) -> bool: + """Whether a Table or DataFrame has a column, or a DataFrame an index, of this + name.""" + if not isinstance(name, str): + return False + if name in list(data.columns): + return True + return is_data_frame(data) and name in list(data.index.names) + + +def _is_default_cells(value: Any) -> bool: + return isinstance(value, int) and not isinstance(value, bool) and value == MIN_CELLS + + +def _is_series(value: object) -> bool: + """Whether ``value`` is a pandas Series, recognised without importing pandas.""" + return all( + hasattr(value, name) for name in ("index", "to_numpy", "name", "dtype") + ) and not hasattr(value, "columns") + + +# The result. + + +def _activities( + method: str, + scores: FloatArray, + estimates: FloatArray, + p_values: FloatArray, + counts: IntArray, + profiles: _Profiles, + network: Network, + edges: _Edges, + options: dict[str, Any], +) -> Activities: + """The record of the sources scored in at least one profile.""" + scored = np.flatnonzero(np.any(counts >= options["min_targets"], axis=0)) + if scored.size == 0: + most = int(counts.max()) if counts.size else 0 + raise ValueError( + f"no source of the network has {options['min_targets']} targets among " + f"the genes of a profile (the most is {most}); lower min_targets, or " + "check that the network and the data name genes the same way" + ) + rows = np.concatenate( + [edges.rows[edges.starts[s] : edges.starts[s + 1]] for s in scored.tolist()] + ) + kept = Network( + network.edges.take(rows), + name=network.name, + version=network.version, + url=network.url, + licence=network.licence, + citation=network.citation, + file=network.file, + sha256=network.sha256, + ) + p = p_values[:, scored] + adjusted = adjust_p_values(p, axis=1) if p.size else p.copy() + settings = {**options, **profiles.settings} + return Activities( + read_only(np.ascontiguousarray(scores[:, scored])), + read_only(np.ascontiguousarray(estimates[:, scored])), + read_only(np.ascontiguousarray(p)), + read_only(np.ascontiguousarray(adjusted)), + read_only(np.ascontiguousarray(counts[:, scored])), + profiles.profiles, + kept, + method, + MappingProxyType(settings), + ) + + +def _two_sided(t_values: FloatArray, df: FloatArray) -> FloatArray: + """Two-sided p-values of t-statistics on ``df`` degrees of freedom per row.""" + df_full = np.broadcast_to(df[:, np.newaxis], t_values.shape) + usable = ~np.isnan(t_values) & (df_full > 0.0) + p = np.full(t_values.shape, np.nan) + if usable.any(): + tails = np.asarray(t_sf(np.abs(t_values[usable]), df_full[usable])) + p[usable] = np.minimum(2.0 * tails, 1.0) + return p + + +# ULM. + + +@jit(parallel=True) +def _ulm_parallel( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + min_targets: int, + slopes: FloatArray, + t_values: FloatArray, + counts: IntArray, + n_runs: int, +) -> None: + n_profiles = values.shape[0] + for run in prange(n_runs): + for p in range(run * n_profiles // n_runs, (run + 1) * n_profiles // n_runs): + _ulm_profile( + values[p], starts, genes, weights, min_targets, slopes[p], + t_values[p], counts[p], + ) # fmt: skip + + +@jit +def _ulm_serial( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + min_targets: int, + slopes: FloatArray, + t_values: FloatArray, + counts: IntArray, +) -> None: + for p in range(values.shape[0]): + _ulm_profile( + values[p], starts, genes, weights, min_targets, slopes[p], t_values[p], + counts[p], + ) # fmt: skip + + +@jit(error_model="numpy") +def _ulm_profile( + y: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + min_targets: int, + slopes: FloatArray, + t_values: FloatArray, + counts: IntArray, +) -> None: + """The slope and its t-statistic of every source in one profile. + + Over the n observed genes, with x the source's weights (zero off its targets): + Sxx = sum (x - xbar)^2, from the targets and the n - k other genes, and + Sxy = sum (x - xbar)(y - ybar) = sum_targets w (y - ybar) - xbar sum (y - ybar), + whose last sum is zero but for rounding. + """ + n = 0 + total = 0.0 + for g in range(y.size): + if not math.isnan(y[g]): + n += 1 + total += y[g] + if n == 0: + for s in range(starts.size - 1): + counts[s] = 0 + return + mean = total / n + syy = 0.0 + residual = 0.0 + for g in range(y.size): + if not math.isnan(y[g]): + deviation = y[g] - mean + syy += deviation * deviation + residual += deviation + df = n - 2 + for s in range(starts.size - 1): + k = 0 + weight_sum = 0.0 + for e in range(starts[s], starts[s + 1]): + if not math.isnan(y[genes[e]]): + k += 1 + weight_sum += weights[e] + counts[s] = k + if k < min_targets or df < 1: + continue + x_mean = weight_sum / n + sxx = (n - k) * x_mean * x_mean + sxy = -x_mean * residual + for e in range(starts[s], starts[s + 1]): + value = y[genes[e]] + if not math.isnan(value): + deviation = weights[e] - x_mean + sxx += deviation * deviation + sxy += weights[e] * (value - mean) + if not sxx > 0.0: + continue + slope = sxy / sxx + slopes[s] = slope + rss = max(syy - slope * sxy, 0.0) + if rss > 0.0: + t_values[s] = slope / math.sqrt(rss / (df * sxx)) + elif slope != 0.0: + t_values[s] = math.copysign(math.inf, slope) + + +# MLM. + + +def _fit_pattern( + values: FloatArray, + observed: npt.NDArray[np.bool_], + rows: list[int], + edges: _Edges, + min_targets: int, + names: list[str], + profiles: Table, + coefficients: FloatArray, + t_values: FloatArray, + p_values: FloatArray, + counts: IntArray, +) -> None: + """Fit MLM to the profiles that observe the same genes.""" + genes = np.flatnonzero(observed[rows[0]]) + inside = np.concatenate(([0], np.cumsum(observed[rows[0]][edges.genes]))) + per_source = inside[edges.starts[1:]] - inside[edges.starts[:-1]] + counts[rows] = per_source + scored = np.flatnonzero(per_source >= min_targets) + n = genes.size + if scored.size == 0 or n == 0: + return + m = scored.size + df = n - m - 1 + ids = [str(profiles[_PROFILE_ID][row]) for row in rows] + if df < 1: + raise ValueError( + f"MLM fits {m} sources and an intercept to the {n} genes of profile(s) " + f"{quoted(ids)}, which leaves no residual degrees of freedom; it needs " + "more genes than sources: use ulm, or a network with fewer sources" + ) + position = np.full(observed.shape[1], -1, dtype=np.int64) + position[genes] = np.arange(n) + design = np.zeros((n, m + 1)) + design[:, 0] = 1.0 + for column, s in enumerate(scored.tolist(), start=1): + span = slice(edges.starts[s], edges.starts[s + 1]) + places = position[edges.genes[span]] + present = places >= 0 + design[places[present], column] = edges.weights[span][present] + q, r = np.linalg.qr(design) + norms = np.linalg.norm(design, axis=0) + aliased = np.flatnonzero(np.abs(np.diag(r)) <= RANK_TOLERANCE * norms) + if aliased.size: + listed = [names[scored[j - 1]] for j in aliased.tolist() if j > 0] + raise ValueError( + f"MLM cannot tell the sources {listing([repr(s) for s in listed])} apart " + "from the other sources: over the genes of profile(s) " + f"{quoted(ids)}, their weights are linear combinations of the weights of " + "sources before them in the network. Leave them out of the network " + "(Network.select), or use ulm, which scores each source alone" + ) + y = values[np.ix_(rows, genes)].T + effects = q.T @ y + estimates = np.linalg.solve(r, effects) + residuals = y - q @ effects + sigma2 = np.einsum("ij,ij->j", residuals, residuals) / df + inverse = np.linalg.solve(r, np.eye(m + 1)) + unscaled = np.einsum("ij,ij->i", inverse, inverse) + with np.errstate(divide="ignore", invalid="ignore"): + t = estimates / np.sqrt(np.outer(unscaled, sigma2)) + coefficients[np.ix_(rows, scored)] = estimates[1:].T + t_values[np.ix_(rows, scored)] = t[1:].T + p_values[np.ix_(rows, scored)] = _two_sided( + np.ascontiguousarray(t[1:].T), np.full(len(rows), float(df)) + ) + + +# The weighted aggregate. + + +def _aggregate( + values: FloatArray, + edges: _Edges, + order: IntArray, + mean: bool, + min_targets: int, + out: FloatArray, + counts: IntArray, +) -> None: + """The weighted means or sums of the targets' values, the genes taking the + values of ``order[0]``, the identity for the observed scores.""" + by_chunks( + _aggregate_parallel, + _aggregate_serial, + (values, edges.starts, edges.genes, edges.weights, order[0], mean, + min_targets, out, counts), + values.shape[0], + ) # fmt: skip + + +@jit(parallel=True) +def _aggregate_parallel( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + order: IntArray, + mean: bool, + min_targets: int, + out: FloatArray, + counts: IntArray, + n_runs: int, +) -> None: + n_profiles = values.shape[0] + for run in prange(n_runs): + for p in range(run * n_profiles // n_runs, (run + 1) * n_profiles // n_runs): + _aggregate_profile( + values[p], starts, genes, weights, order, mean, min_targets, out[p], + counts[p], + ) # fmt: skip + + +@jit +def _aggregate_serial( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + order: IntArray, + mean: bool, + min_targets: int, + out: FloatArray, + counts: IntArray, +) -> None: + for p in range(values.shape[0]): + _aggregate_profile( + values[p], starts, genes, weights, order, mean, min_targets, out[p], + counts[p], + ) # fmt: skip + + +@jit +def _aggregate_profile( + y: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + order: IntArray, + mean: bool, + min_targets: int, + out: FloatArray, + counts: IntArray, +) -> None: + for s in range(starts.size - 1): + k = 0 + total = 0.0 + size = 0.0 + for e in range(starts[s], starts[s + 1]): + if not math.isnan(y[genes[e]]): + k += 1 + value = y[order[genes[e]]] + if not math.isnan(value): + total += weights[e] * value + size += abs(weights[e]) + counts[s] = k + if k >= min_targets and size > 0.0: + out[s] = total / size if mean else total + + +def _permuted( + values: FloatArray, + edges: _Edges, + mean: bool, + min_targets: int, + estimates: FloatArray, + n_permutations: int, + generator: np.random.Generator, +) -> tuple[FloatArray, FloatArray]: + """The scores normalized by those of random permutations of the genes, and the + permutation p-values. + + Each permutation shuffles the positions of the genes, as decoupler's ``_ridx`` + shuffles each row of a tiled range with numpy's ``Generator.shuffle``, so that the + same seed gives decoupler's permutations. + """ + n_profiles, n_genes = values.shape + n_sources = edges.starts.size - 1 + # For each profile and source: permutations counted, their running mean and sum + # of squared deviations (Welford 1962), and those at least and at most as high. + seen = np.zeros((n_profiles, n_sources)) + running = np.zeros((n_profiles, n_sources)) + squares = np.zeros((n_profiles, n_sources)) + higher = np.zeros((n_profiles, n_sources)) + lower = np.zeros((n_profiles, n_sources)) + done = 0 + while done < n_permutations: + size = min(_BLOCK, n_permutations - done) + block = np.tile(np.arange(n_genes, dtype=np.int64), (size, 1)) + for row in block: + generator.shuffle(row) + by_chunks( + _permutations_parallel, + _permutations_serial, + (values, edges.starts, edges.genes, edges.weights, block, mean, + min_targets, estimates, seen, running, squares, higher, lower), + n_profiles, + ) # fmt: skip + done += size + with np.errstate(divide="ignore", invalid="ignore"): + sd = np.sqrt(squares / (seen - 1.0)) + scores = np.where(sd > 0.0, (estimates - running) / sd, np.nan) + p_values = np.minimum( + 1.0, 2.0 * (1.0 + np.minimum(higher, lower)) / (1.0 + seen) + ) + p_values[np.isnan(estimates)] = np.nan + scores[np.isnan(estimates)] = np.nan + return scores, p_values + + +@jit(parallel=True) +def _permutations_parallel( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + block: IntArray, + mean: bool, + min_targets: int, + estimates: FloatArray, + seen: FloatArray, + running: FloatArray, + squares: FloatArray, + higher: FloatArray, + lower: FloatArray, + n_runs: int, +) -> None: + n_profiles = values.shape[0] + for run in prange(n_runs): + for p in range(run * n_profiles // n_runs, (run + 1) * n_profiles // n_runs): + _permutations_profile( + values[p], starts, genes, weights, block, mean, min_targets, + estimates[p], seen[p], running[p], squares[p], higher[p], lower[p], + ) # fmt: skip + + +@jit +def _permutations_serial( + values: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + block: IntArray, + mean: bool, + min_targets: int, + estimates: FloatArray, + seen: FloatArray, + running: FloatArray, + squares: FloatArray, + higher: FloatArray, + lower: FloatArray, +) -> None: + for p in range(values.shape[0]): + _permutations_profile( + values[p], starts, genes, weights, block, mean, min_targets, + estimates[p], seen[p], running[p], squares[p], higher[p], lower[p], + ) # fmt: skip + + +@jit +def _permutations_profile( + y: FloatArray, + starts: IntArray, + genes: IntArray, + weights: FloatArray, + block: IntArray, + mean: bool, + min_targets: int, + estimates: FloatArray, + seen: FloatArray, + running: FloatArray, + squares: FloatArray, + higher: FloatArray, + lower: FloatArray, +) -> None: + """Add a block of permutations to the counts of one profile's sources.""" + for b in range(block.shape[0]): + order = block[b] + for s in range(starts.size - 1): + observed = estimates[s] + if math.isnan(observed): + continue + total = 0.0 + size = 0.0 + for e in range(starts[s], starts[s + 1]): + if not math.isnan(y[genes[e]]): + value = y[order[genes[e]]] + if not math.isnan(value): + total += weights[e] * value + size += abs(weights[e]) + if not size > 0.0: + continue + score = total / size if mean else total + seen[s] += 1.0 + delta = score - running[s] + running[s] += delta / seen[s] + squares[s] += delta * (score - running[s]) + if score >= observed: + higher[s] += 1.0 + if score <= observed: + lower[s] += 1.0 diff --git a/cytepool/networks.py b/cytepool/networks.py new file mode 100644 index 0000000..ea42422 --- /dev/null +++ b/cytepool/networks.py @@ -0,0 +1,1098 @@ +"""Networks of regulators and pathways and the genes they act on, with signed weights: +CollecTRI's transcription factors and PROGENy's pathways, downloaded from their +releases, and networks given as tables. + +A :class:`Network` maps each source, a transcription factor or a pathway, to its +targets, the genes it regulates or that respond to it, each with a weight whose sign +says whether the source raises the gene (positive) or lowers it (negative). The +activity functions of :mod:`cytepool.activity` take one, as decoupler takes its +networks (Badia-i-Mompel et al. 2022): a table with the columns ``source``, +``target`` and ``weight``. A network records its name, version, where it comes from, +its licence and its citation, and, for a network read from a file, the name of the +file and its SHA-256 digest. + +CollecTRI (:func:`collectri`): + The signed regulons of 1,186 human transcription factors, 43,175 interactions, + that Mueller-Dott et al. (2023) assembled from twelve resources of literature + curation and text mining: version 2.0 of the file ``CollecTRI_regulons.csv`` of + Zenodo record 8192729 (doi:10.5281/zenodo.8192729), which decoupler 2.2.0 reads + too, under the Creative Commons Attribution 4.0 licence. Each interaction has a + weight of 1 (activation) or -1 (repression), the resources that list it, the + PubMed ids of its references and how its sign was decided. The complexes AP1 + and NFKB are sources of their own, as in the file. + + Licence levels. Each resource keeps its own terms, which OmniPath records + (Tuerei et al. 2021). ``licence="commercial"``, the default, keeps the + interactions that at least one resource listed for commercial use supports: + every resource but GEREDB, which OmniPath lists for academic use (under the + Academic Free License 3.0), so that the 3,279 interactions that GEREDB alone + supports are left out, 39,896 remain. ``licence="academic"`` keeps every + interaction. The terms of the other resources, as OmniPath lists them: CC BY 4.0 + (ExTRI, NTNU Curated, TFactS, SIGNOR, IntAct, CytReg, GOA and Pavlidis2021), + CC BY-SA 4.0 (TRRUST), LGPL 3.0 (HTRIdb) and an informal licence (DoRothEA's + level A). Use under any level is subject to these terms, such as attribution. + +PROGENy (:func:`progeny`): + The weights of the genes that respond to 14 signalling pathways, from the + perturbation experiments of Schubert et al. (2018), for human, and their + transfer to mouse (Holland, Szalai and Saez-Rodriguez 2020): the data frames + ``model_human_full`` (22,479 genes) and ``model_mouse_full`` (17,426 genes) of + the R package progeny 1.28.0, from its Bioconductor 3.20 release, under the + Apache License 2.0, with a weight (a z-score) and a p-value for every gene and + pathway. By default the genes with a p-value below 0.05 are kept, as decoupler's + ``op.progeny`` keeps them; ``top`` keeps the genes of each pathway with the + smallest p-values, those tied with the last one included, as progeny's + ``getModel`` does (100 genes by default there). + +Downloads and the cache: + Each network is downloaded on first use from the address it was published at, + pinned by its size and SHA-256 digest, into the cache directory: ``cache_dir``, + else ``cytepool/networks`` under ``XDG_CACHE_HOME`` or ``~/.cache``. A file + whose size or digest differs from the pinned ones is refused, whether + downloaded or found in the cache, so that a network is always the release its + version names. With the file in the cache, as when copied there from another + machine, no connection is made. + +R's data files: + progeny keeps its weights in R's data files (``.rda``), which this module reads + with a reader of R's serialization format (version 2 or 3, XDR; R Core Team + 2025) limited to what data frames of numbers, text and factors use: vectors, + pairlists of attributes, symbols and references to them, and R's compact + sequences of numbers, compressed by xz, bzip2 or gzip. + +References: + Mueller-Dott S, Tsirvouli E, Vazquez M, Ramirez Flores RO, Badia-i-Mompel P, + Fallegger R, Tuerei D, Laegreid A, Saez-Rodriguez J (2023). Expanding the + coverage of regulons from high-confidence prior knowledge for accurate estimation + of transcription factor activities. Nucleic Acids Research 51, 10934-10949. + doi:10.1093/nar/gkad841 + + Schubert M, Klinger B, Kluenemann M, Sieber A, Uhlitz F, Sauer S, Garnett MJ, + Bluethgen N, Saez-Rodriguez J (2018). Perturbation-response genes reveal + signaling footprints in cancer gene expression. Nature Communications 9, 20. + doi:10.1038/s41467-017-02391-6 + + Holland CH, Szalai B, Saez-Rodriguez J (2020). Transfer of regulatory knowledge + from human to mouse for functional genomics analysis. Biochimica et Biophysica + Acta (BBA) - Gene Regulatory Mechanisms 1863, 194431. + doi:10.1016/j.bbagrm.2019.194431 + + Tuerei D, Valdeolivas A, Gul L, Palacio-Escat N, Klein M, Ivanova O, Oelbei M, + Gabor A, Theis F, Modos D, Korcsmaros T, Saez-Rodriguez J (2021). Integrated + intra- and intercellular signaling knowledge for multicellular omics analysis. + Molecular Systems Biology 17, e9923. doi:10.15252/msb.20209923 + + Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S, + Taus P, Dugourd A, Holland CH, Ramirez Flores RO, Saez-Rodriguez J (2022). + decoupleR: ensemble of computational methods to infer biological activities from + omics data. Bioinformatics Advances 2, vbac016. doi:10.1093/bioadv/vbac016 + + R Core Team (2025). R: A Language and Environment for Statistical Computing. R + Foundation for Statistical Computing, Vienna. https://www.R-project.org/ +""" + +import bz2 +import csv +import gzip +import hashlib +import io +import lzma +import os +import struct +import tarfile +import tempfile +import urllib.error +import urllib.request +from collections.abc import Iterator, Mapping +from pathlib import Path +from types import MappingProxyType +from typing import Any, NamedTuple + +import numpy as np + +from . import __version__ +from ._columns import is_data_frame, text_column +from ._gene_ids import is_missing, listed_values +from ._masked_arrays import real_array, reject_masked +from ._messages import close_matches, quoted +from ._options import checked_choice, positive_integer, real_number +from .table import Table + +__all__ = ["Network", "collectri", "progeny"] + + +class _Download(NamedTuple): + """A file of a network: where it is published, its name in the cache, its size in + bytes and its SHA-256 digest.""" + + url: str + file: str + size: int + sha256: str + + +_DOWNLOADS = { + "collectri": _Download( + "https://zenodo.org/records/8192729/files/CollecTRI_regulons.csv?download=1", + "CollecTRI_regulons.csv", + 4_345_649, + "4473c9189dd53dacc80297709ad1452dda1086a1cc2185f9a56146c261668701", + ), + "progeny": _Download( + "https://bioconductor.org/packages/3.20/bioc/src/contrib/progeny_1.28.0.tar.gz", + "progeny_1.28.0.tar.gz", + 10_642_873, + "2c41531ef6d088047e2830ccc5741aa0f28f48fb51d4c164cc8331e19b925d7b", + ), +} +_TIMEOUT = 120.0 # seconds without data before a download gives up +# Some servers, such as Bioconductor's, refuse requests that do not name their client. +_CLIENT = f"CytePool/{__version__} (+https://github.com/NygenAnalytics/CytePool)" +_BLOCK = 1 << 20 # bytes read at a time + +_COLLECTRI_COLUMNS = ( + "source", + "target", + "weight", + "resources", + "references", + "sign_decision", +) +# CollecTRI's resources that OmniPath's licence records list for academic use only; +# the others it lists for commercial use (https://omnipathdb.org/resources, read on +# 26 October 2026). +_ACADEMIC_ONLY = frozenset({"GEREDB"}) +_LICENCES = ("commercial", "academic") +_COLLECTRI_LICENCE = { + "commercial": ( + "CC BY 4.0 (Zenodo record 8192729); level 'commercial': the interactions of " + "resources that OmniPath lists for commercial use, without those that GEREDB " + "(listed for academic use) alone supports; each resource keeps its own terms" + ), + "academic": ( + "CC BY 4.0 (Zenodo record 8192729); level 'academic': every interaction, " + "including those that GEREDB, listed by OmniPath for academic use, alone " + "supports; each resource keeps its own terms" + ), +} +_COLLECTRI_CITATION = ( + "Mueller-Dott S et al. (2023). Expanding the coverage of regulons from " + "high-confidence prior knowledge for accurate estimation of transcription factor " + "activities. Nucleic Acids Research 51, 10934-10949. doi:10.1093/nar/gkad841" +) +_ORGANISMS = ("human", "mouse") +_PROGENY_CITATION = { + "human": ( + "Schubert M et al. (2018). Perturbation-response genes reveal signaling " + "footprints in cancer gene expression. Nature Communications 9, 20. " + "doi:10.1038/s41467-017-02391-6" + ), + "mouse": ( + "Holland CH, Szalai B, Saez-Rodriguez J (2020). Transfer of regulatory " + "knowledge from human to mouse for functional genomics analysis. Biochimica et " + "Biophysica Acta (BBA) - Gene Regulatory Mechanisms 1863, 194431. " + "doi:10.1016/j.bbagrm.2019.194431" + ), +} +_PROGENY_MAX_P_VALUE = 0.05 # decoupler's thr_padj +_SOURCES_SHOWN = 3 # the sources that a repr names + + +class Network(Mapping[str, Mapping[str, float]]): + """Sources, such as transcription factors or pathways, and their target genes, + with signed weights. + + A read-only mapping of each source to its targets, a mapping of gene id to weight, + so that ``network["STAT1"]["IRF1"]`` is the weight of the edge from STAT1 to IRF1. + The edges keep the order given, and the sources the order in which they first + appear. :func:`collectri` and :func:`progeny` build one from their releases. + Immutable. + + Args: + edges: A ``Table``, a pandas DataFrame or a mapping of column name to + values, with the columns ``source``, ``target`` and, optionally, + ``weight`` (1 for every edge without it), one row per edge; other + columns, such as a p-value or the resources of an interaction, are kept + with the edges. Or a mapping of source to its targets: a mapping of gene + id to weight, or a list of gene ids, each of weight 1, so that a + :class:`~cytepool.gene_sets.GeneSets` is a network of unweighted sets. + name: The network's name, such as ``"CollecTRI"``. + version: Its version or release. + url: Where it was obtained. + licence: The terms under which it is distributed, as its provider states + them. + citation: The work to cite for it. + file: The name of the file it was read from. + sha256: The SHA-256 digest of that file, 64 hexadecimal digits in lower case. + + Raises: + TypeError: If ``edges`` is none of these, a source or target is not a + string, a weight is not a real number, a text option is not a string, or + an argument is or holds a numpy masked array. + ValueError: If a table lacks the source or target column, a source or + target is missing or empty, a weight is not finite, an edge is given + twice, there are no edges, or ``sha256`` is not a SHA-256 digest. + + Example: + >>> network = Network({"STAT1": {"IRF1": 1.0, "SOCS1": 1.0, "MYC": -1.0}}) + >>> network["STAT1"]["MYC"] + -1.0 + """ + + __slots__ = ( + "_edges", + "_sources", + "_targets", + "_name", + "_version", + "_url", + "_licence", + "_citation", + "_file", + "_sha256", + ) + + _edges: Table + _sources: tuple[str, ...] + _targets: dict[str, dict[str, float]] + _name: str | None + _version: str | None + _url: str | None + _licence: str | None + _citation: str | None + _file: str | None + _sha256: str | None + + def __init__( + self, + edges: Any, + *, + name: str | None = None, + version: str | None = None, + url: str | None = None, + licence: str | None = None, + citation: str | None = None, + file: str | None = None, + sha256: str | None = None, + ) -> None: + record = { + "name": _text(name, "name"), + "version": _text(version, "version"), + "url": _text(url, "url"), + "licence": _text(licence, "licence"), + "citation": _text(citation, "citation"), + "file": _text(file, "file"), + "sha256": _digest(sha256), + } + _initialize(self, _edge_table(edges), record) + + # Immutability and pickling. + + def __setattr__(self, name: str, value: object) -> None: + raise AttributeError("Network is immutable; select() returns a new network") + + def __delattr__(self, name: str) -> None: + raise AttributeError("Network is immutable") + + def __reduce__(self) -> tuple[Any, ...]: + return (_rebuild, (self._edges, self._record())) + + # The mapping. + + def __getitem__(self, source: str) -> Mapping[str, float]: + """Return the targets of ``source``, a mapping of gene id to weight. + + Raises: + KeyError: If there is no such source; the message suggests close names. + """ + try: + return MappingProxyType(self._targets[source]) + except (KeyError, TypeError): + hint = ( + close_matches(source, list(self._sources)) + if isinstance(source, str) + else "" + ) + raise KeyError(f"no source named {source!r}{hint}") from None + + def __contains__(self, source: object) -> bool: + """Return whether a source has this name.""" + try: + return source in self._targets + except TypeError: # unhashable + return False + + def __iter__(self) -> Iterator[str]: + """Iterate over the sources, in order.""" + return iter(self._sources) + + def __len__(self) -> int: + """Return the number of sources.""" + return len(self._sources) + + # What the network records. + + @property + def edges(self) -> Table: + """The edges, one row per edge, with the columns ``source``, ``target`` and + ``weight`` and any others given.""" + return self._edges + + @property + def sources(self) -> tuple[str, ...]: + """The sources, in the order in which they first appear.""" + return self._sources + + @property + def name(self) -> str | None: + """The network's name; None when not stated.""" + return self._name + + @property + def version(self) -> str | None: + """Its version or release; None when not stated.""" + return self._version + + @property + def url(self) -> str | None: + """Where it was obtained; None when not stated.""" + return self._url + + @property + def licence(self) -> str | None: + """The terms under which it is distributed; None when not stated.""" + return self._licence + + @property + def citation(self) -> str | None: + """The work to cite for it; None when not stated.""" + return self._citation + + @property + def file(self) -> str | None: + """The name of the file it was read from; None for a network built in + Python.""" + return self._file + + @property + def sha256(self) -> str | None: + """The SHA-256 digest of that file; None for a network built in Python.""" + return self._sha256 + + # New networks. + + def select(self, sources: str | Any) -> "Network": + """Return the edges of the given sources, in that order of sources. + + Args: + sources: A source, or a sequence of them. + + Raises: + KeyError: If a name is not a source's; the message suggests close names. + TypeError: If a name is not a string, or ``sources`` is or holds a numpy + masked array. + ValueError: If a source is given twice, or none is given. + """ + reject_masked(sources, "sources") + given = [sources] if isinstance(sources, str) else _names(sources) + chosen: list[str] = [] + for source in given: + if not isinstance(source, str): + raise TypeError(f"sources are named by strings, got {source!r}") + if source not in self._targets: + hint = close_matches(source, list(self._sources)) + raise KeyError(f"no source named {source!r}{hint}") + if source in chosen: + raise ValueError(f"the source {source!r} is selected more than once") + chosen.append(str(source)) + if not chosen: + raise ValueError("select needs at least one source") + names = self._edges["source"].tolist() + rows: dict[str, list[int]] = {source: [] for source in chosen} + for row, source in enumerate(names): + if source in rows: + rows[source].append(row) + taken = [row for source in chosen for row in rows[source]] + return self._derived(np.array(taken, dtype=np.intp)) + + def __repr__(self) -> str: + n_edges = len(self._edges) + n_targets = len(set(self._edges["target"].tolist())) + lines = [ + f"Network: {len(self._sources):,} source" + f"{'' if len(self._sources) == 1 else 's'}, {n_edges:,} edge" + f"{'' if n_edges == 1 else 's'}, {n_targets:,} target gene" + f"{'' if n_targets == 1 else 's'}" + ] + stated = [ + f"{label}: {value if value is not None else 'not stated'}" + for label, value in ( + ("name", self._name), + ("version", self._version), + ("licence", self._licence), + ) + ] + lines.append(" " + "; ".join(stated)) + if self._file is not None: + digest = f", SHA-256 {self._sha256}" if self._sha256 else "" + lines.append(f" read from {self._file}{digest}") + shown = [ + f"{source} ({len(self._targets[source]):,})" + for source in self._sources[:_SOURCES_SHOWN] + ] + more = len(self._sources) - len(shown) + lines.append( + " sources: " + ", ".join(shown) + (f" and {more:,} more" if more else "") + ) + return "\n".join(lines) + + def _record(self) -> dict[str, str | None]: + return { + "name": self._name, + "version": self._version, + "url": self._url, + "licence": self._licence, + "citation": self._citation, + "file": self._file, + "sha256": self._sha256, + } + + def _derived(self, rows: np.ndarray) -> "Network": + """A network of some of these edges, with the same record.""" + derived = object.__new__(Network) + _initialize(derived, self._edges.take(rows), self._record()) + return derived + + +def collectri( + *, + licence: str = "commercial", + cache_dir: str | os.PathLike[str] | None = None, +) -> Network: + """The CollecTRI network of human transcription factors and their target genes. + + The signed regulons of Mueller-Dott et al. (2023), version 2.0 of its Zenodo + record, at the licence level asked for: the module docstring says what each + level holds and the terms of each resource. The file is downloaded on first use, + checked against its pinned size and SHA-256 digest, and kept in the cache. + + Args: + licence: ``"commercial"``, the interactions of resources that OmniPath lists + for commercial use (39,896), or ``"academic"``, every interaction + (43,175). + cache_dir: The directory that holds the downloaded file; by default + ``cytepool/networks`` under ``XDG_CACHE_HOME`` or ``~/.cache``. + + Returns: + A network with the columns ``source``, ``target``, ``weight`` (1 or -1), + ``resources`` (the resources that list the interaction, separated by + ``";"``), ``references`` (PubMed ids, separated by ``";"``) and + ``sign_decision``, in the order of the file. + + Raises: + ValueError: If ``licence`` is not one of the levels, or the file in the cache, + or the one downloaded, is not the file pinned (its size or digest + differs). + TypeError: If an argument has the wrong type. + OSError: If the file is not in the cache and cannot be downloaded. + + Example: + >>> network = collectri() # doctest: +SKIP + >>> network["STAT1"]["IRF1"] # doctest: +SKIP + 1.0 + """ + level = checked_choice(licence, "licence", _LICENCES) + download = _DOWNLOADS["collectri"] + path = _fetched(download, cache_dir) + edges = _collectri_edges(path.read_bytes(), level) + return Network( + Table(edges), + name="CollecTRI", + version="2.0 (Zenodo record 8192729)", + url=download.url, + licence=_COLLECTRI_LICENCE[level], + citation=_COLLECTRI_CITATION, + file=download.file, + sha256=download.sha256, + ) + + +def progeny( + *, + organism: str = "human", + top: int | None = None, + max_p_value: float | None = _PROGENY_MAX_P_VALUE, + cache_dir: str | os.PathLike[str] | None = None, +) -> Network: + """The PROGENy network of 14 signalling pathways and the genes that respond to + them. + + The weights of the R package progeny 1.28.0 (Schubert et al. 2018; Holland, + Szalai and Saez-Rodriguez 2020), under the Apache License 2.0: each pathway's + genes in increasing order of their p-values, those with a p-value below + ``max_p_value``, as decoupler's ``op.progeny`` keeps them, and, with ``top``, + only the ``top`` genes with the smallest p-values, those tied with the last one + included, as progeny's ``getModel`` keeps them. The package is downloaded on + first use, checked against its pinned size and SHA-256 digest, and kept in the + cache. + + Args: + organism: ``"human"`` or ``"mouse"``. + top: The most genes of each pathway, at least 1; None for no limit. + progeny's ``getModel`` and ``progeny`` take 100 by default, decoupler's + tutorials 500. + max_p_value: Keep the genes with a p-value below it, in (0, 1]; None keeps + every gene. 0.05 by default, as in decoupler. + cache_dir: The directory that holds the downloaded file; by default + ``cytepool/networks`` under ``XDG_CACHE_HOME`` or ``~/.cache``. + + Returns: + A network with the columns ``source`` (the pathway), ``target`` (the gene), + ``weight`` and ``p_value``, the pathways in alphabetical order and the genes + of each in increasing order of their p-values. + + Raises: + ValueError: If an option is out of range, or the file in the cache, or the one + downloaded, is not the file pinned (its size or digest differs). + TypeError: If an argument has the wrong type. + OSError: If the file is not in the cache and cannot be downloaded. + + Example: + >>> network = progeny(top=100) # doctest: +SKIP + >>> len(network) # doctest: +SKIP + 14 + """ + kind = checked_choice(organism, "organism", _ORGANISMS) + reject_masked(top, "top") + reject_masked(max_p_value, "max_p_value") + most = None if top is None else positive_integer(top, "top") + threshold = None + if max_p_value is not None: + threshold = real_number(max_p_value, "max_p_value", finite=True) + if not 0.0 < threshold <= 1.0: + raise ValueError(f"max_p_value must be in (0, 1], got {threshold}") + download = _DOWNLOADS["progeny"] + path = _fetched(download, cache_dir) + edges = _progeny_edges(path.read_bytes(), kind, most, threshold) + return Network( + Table(edges), + name=f"PROGENy ({kind})", + version="progeny 1.28.0 (Bioconductor 3.20)", + url=download.url, + licence="Apache License 2.0 (the R package progeny)", + citation=_PROGENY_CITATION[kind], + file=download.file, + sha256=download.sha256, + ) + + +# Building networks. + + +def _initialize(network: Network, edges: Table, record: Mapping[str, Any]) -> None: + targets: dict[str, dict[str, float]] = {} + weights = edges["weight"].tolist() + for source, target, weight in zip( + edges["source"].tolist(), edges["target"].tolist(), weights, strict=True + ): + targets.setdefault(source, {})[target] = weight + object.__setattr__(network, "_edges", edges) + object.__setattr__(network, "_sources", tuple(targets)) + object.__setattr__(network, "_targets", targets) + for key in ("name", "version", "url", "licence", "citation", "file", "sha256"): + object.__setattr__(network, f"_{key}", record[key]) + + +def _rebuild(edges: Table, record: dict[str, str | None]) -> Network: + network = object.__new__(Network) + _initialize(network, edges, record) + return network + + +def _edge_table(edges: Any) -> Table: + """The edges given, checked: source, target and weight first.""" + reject_masked(edges, "edges", missing="None or NaN") + columns = isinstance(edges, Mapping) and "source" in edges and "target" in edges + if isinstance(edges, Table) or is_data_frame(edges) or columns: + table = edges if isinstance(edges, Table) else Table(edges) + missing = [name for name in ("source", "target") if name not in table] + if missing: + raise ValueError( + f"the edges have no column {quoted(missing)}; a network's table has " + "the columns 'source', 'target' and, optionally, 'weight', one row " + f"per edge; its columns are {quoted(list(table.columns))}" + ) + sources = _names_column(table["source"], "source") + targets = _names_column(table["target"], "target") + if "weight" in table: + weights = _weights(table["weight"], "the column 'weight'") + else: + weights = np.ones(len(sources)) + others = { + name: table[name] + for name in table.columns + if name not in ("source", "target", "weight") + } + elif isinstance(edges, Mapping): + sources, targets, weight_list = [], [], [] + for source, members in edges.items(): + if not isinstance(source, str) or not source: + raise TypeError(f"sources must be non-empty strings, got {source!r}") + if isinstance(members, Mapping): + reject_masked(members, f"edges[{source!r}]", missing="NaN") + pairs = list(members.items()) + else: + genes = listed_values(members, f"the targets of {source!r}") + pairs = [(gene, 1.0) for gene in genes] + for target, weight in pairs: + if not isinstance(target, str) or not target: + raise TypeError( + f"the targets of {source!r} must be gene ids (non-empty " + f"strings), got {target!r}" + ) + sources.append(str(source)) + targets.append(str(target)) + weight_list.append(weight) + weights = _weights(weight_list, "the weights") + others = {} + else: + raise TypeError( + "edges must be a Table or pandas DataFrame with the columns 'source', " + "'target' and 'weight', or a mapping of source to targets, got " + f"{type(edges).__name__}" + ) + if not sources: + raise ValueError("the network has no edges") + seen: dict[tuple[str, str], int] = {} + for row, pair in enumerate(zip(sources, targets, strict=True)): + if pair in seen: + raise ValueError( + f"the edge from {pair[0]!r} to {pair[1]!r} is given twice (rows " + f"{seen[pair]} and {row}); give each edge once, with one weight" + ) + seen[pair] = row + return Table( + { + "source": text_column(sources), + "target": text_column(targets), + "weight": weights, + **others, + } + ) + + +def _names_column(values: np.ndarray, name: str) -> list[str]: + found = values.tolist() + names = [] + for row, value in enumerate(found): + if isinstance(value, str) and value: + names.append(str(value)) + elif is_missing(value): + raise ValueError( + f"the {name} of row {row} is missing; every edge needs one" + ) + else: + raise TypeError( + f"the column {name!r} must hold names (strings), got " + f"{type(value).__name__} {value!r} on row {row}" + ) + return names + + +def _weights(values: Any, name: str) -> np.ndarray: + try: + weights = real_array(values, name, boolean=False, objects=False) + except TypeError: + raise TypeError(f"{name} must hold real numbers") from None + weights = np.ascontiguousarray(weights, dtype=np.float64).reshape(-1) + if not np.isfinite(weights).all(): + row = int(np.flatnonzero(~np.isfinite(weights))[0]) + raise ValueError( + f"{name} must be finite; edge {row} has the weight {weights[row]}" + ) + return weights + + +def _names(values: Any) -> list[Any]: + try: + return list(values) + except TypeError: + raise TypeError( + f"sources must be a source or a list of them, got {type(values).__name__}" + ) from None + + +def _text(value: Any, name: str) -> str | None: + if value is None: + return None + reject_masked(value, name) + if not isinstance(value, str): + raise TypeError(f"{name} must be a string or None, got {type(value).__name__}") + return value + + +def _digest(value: Any) -> str | None: + digest = _text(value, "sha256") + if digest is not None and ( + len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest) + ): + raise ValueError( + "sha256 must be a SHA-256 digest, 64 hexadecimal digits in lower case, " + f"got {digest!r}" + ) + return digest + + +# Downloads and the cache. + + +def _cache_directory(cache_dir: Any) -> Path: + if cache_dir is not None: + reject_masked(cache_dir, "cache_dir") + if not isinstance(cache_dir, str | os.PathLike): + raise TypeError( + "cache_dir must be a str or os.PathLike, got " + f"{type(cache_dir).__name__}" + ) + return Path(os.fspath(cache_dir)) + base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache") + return Path(base) / "cytepool" / "networks" + + +def _fetched(download: _Download, cache_dir: Any) -> Path: + """The pinned file in the cache, downloaded when it is not there yet.""" + directory = _cache_directory(cache_dir) + path = directory / download.file + if path.exists(): + size, digest = _size_and_digest(path) + if size != download.size or digest != download.sha256: + raise ValueError( + f"{path} is not the file that CytePool pins: {size:,} bytes with " + f"SHA-256 {digest}, where {download.url} published " + f"{download.size:,} bytes with SHA-256 {download.sha256}; delete it " + "to download it again" + ) + return path + directory.mkdir(parents=True, exist_ok=True) + handle, name = tempfile.mkstemp(dir=directory, prefix=download.file, suffix=".part") + partial = Path(name) + try: + with os.fdopen(handle, "wb") as out: + try: + request = urllib.request.Request( + download.url, headers={"User-Agent": _CLIENT} + ) + with urllib.request.urlopen(request, timeout=_TIMEOUT) as answer: + while block := answer.read(_BLOCK): + out.write(block) + except (urllib.error.URLError, OSError) as error: + raise OSError( + f"could not download {download.file} from {download.url}: " + f"{error}. Without a connection, copy the file into {directory} " + "from a machine that has one, or pass cache_dir" + ) from error + size, digest = _size_and_digest(partial) + if size != download.size or digest != download.sha256: + raise ValueError( + f"the download of {download.url} is not the file that CytePool pins: " + f"{size:,} bytes with SHA-256 {digest}, not {download.size:,} bytes " + f"with SHA-256 {download.sha256}; the source may have changed it, or " + "the download was cut short" + ) + os.replace(partial, path) + finally: + partial.unlink(missing_ok=True) + return path + + +def _size_and_digest(path: Path) -> tuple[int, str]: + hasher = hashlib.sha256() + size = 0 + with path.open("rb") as file: + while block := file.read(_BLOCK): + hasher.update(block) + size += len(block) + return size, hasher.hexdigest() + + +# CollecTRI. + + +def _collectri_edges(data: bytes, level: str) -> dict[str, np.ndarray]: + """The interactions of CollecTRI's file at a licence level, with their resources + and references written as decoupler writes them.""" + rows = csv.reader(io.StringIO(data.decode("utf-8"))) + header = next(rows, None) + if header is None or tuple(header) != _COLLECTRI_COLUMNS: + raise ValueError( + f"the CollecTRI file has the columns {header}, not those of version 2.0, " + f"{list(_COLLECTRI_COLUMNS)}" + ) + columns: dict[str, list[Any]] = {name: [] for name in _COLLECTRI_COLUMNS} + for row in rows: + if not row: + continue + source, target, weight, resources, references, sign = row + # "CollecTRI;ExTRI_CollecTRI;TRRUST_CollecTRI" is "ExTRI;TRRUST", as decoupler + # writes it. + listed = sorted( + { + name.replace("_", "") + for name in resources.replace("CollecTRI", "").split(";") + if name + } + ) + if level == "commercial" and set(listed) <= _ACADEMIC_ONLY: + continue + columns["source"].append(source) + columns["target"].append(target) + columns["weight"].append(float(weight)) + columns["resources"].append(";".join(listed)) + columns["references"].append(references.replace("CollecTRI:", "")) + columns["sign_decision"].append(sign) + return { + name: np.array(values, dtype=np.float64) + if name == "weight" + else text_column(values) + for name, values in columns.items() + } + + +# PROGENy. + + +def _progeny_edges( + data: bytes, organism: str, top: int | None, max_p_value: float | None +) -> dict[str, np.ndarray]: + """The genes of each PROGENy pathway, by increasing p-value, as kept.""" + name = f"model_{organism}_full" + member = f"progeny/data/{name}.rda" + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: + try: + stored = archive.extractfile(member) + except KeyError: + stored = None + if stored is None: + raise ValueError(f"the progeny package holds no {member}") + objects = _read_r_data(stored.read()) + if name not in objects: + raise ValueError(f"{member} holds no data frame {name!r}") + frame = _data_frame(objects[name], name) + genes = text_column(frame["gene"]) + pathways = text_column(frame["pathway"]) + weights = np.asarray(frame["weight"], dtype=np.float64) + p_values = np.asarray(frame["p.value"], dtype=np.float64) + kept: list[int] = [] + for pathway in sorted(set(pathways.tolist())): + rows = np.flatnonzero(pathways == pathway) + rows = rows[np.argsort(p_values[rows], kind="stable")] + if top is not None and rows.size > top: + # Those tied with the last one kept stay, as dplyr's top_n keeps them. + rows = rows[p_values[rows] <= p_values[rows[top - 1]]] + if max_p_value is not None: + rows = rows[p_values[rows] < max_p_value] + kept.extend(rows.tolist()) + taken = np.array(kept, dtype=np.intp) + return { + "source": pathways[taken], + "target": genes[taken], + "weight": weights[taken], + "p_value": p_values[taken], + } + + +def _data_frame(value: Any, name: str) -> dict[str, Any]: + """The columns of an R data frame of numbers, text and factors, by name; factors + as their levels.""" + if not isinstance(value, _RVector) or value.kind != _LIST: + raise ValueError(f"{name} is not an R data frame") + names = value.attributes.get("names") + if not isinstance(names, _RVector) or len(names.values) != len(value.values): + raise ValueError(f"the columns of {name} have no names") + columns = {} + for column, item in zip(names.values, value.values, strict=True): + if not isinstance(item, _RVector): + raise ValueError(f"the column {column!r} of {name} is not a vector") + levels = item.attributes.get("levels") + if levels is not None: + codes = np.asarray(item.values) + if (codes < 1).any() or (codes > len(levels.values)).any(): + raise ValueError(f"the factor {column!r} of {name} has missing values") + columns[column] = [levels.values[code - 1] for code in codes.tolist()] + else: + columns[column] = item.values + return columns + + +# R's serialization format. + +_NIL = 254 +_REFERENCE = 255 +_ALTREP = 238 +_ENVIRONMENTS = (241, 242, 253) # the base, empty and global environments +_SYMBOL, _PAIRLIST, _CHARACTER_STRING = 1, 2, 9 +_LOGICAL, _INTEGER, _REAL, _STRINGS, _LIST = 10, 13, 14, 16, 19 +_HAS_ATTRIBUTES = 1 << 9 +_HAS_TAG = 1 << 10 +_LATIN1 = 1 << 14 # the LATIN1 flag of a string's levels, which start at bit 12 +_NA_INTEGER = -(2**31) +_HEADERS = (b"RDX2\n", b"RDX3\n") + + +class _RVector(NamedTuple): + """An R vector: its type, its values and its attributes.""" + + kind: int + values: Any # a numpy array of numbers, or a list of strings or items + attributes: dict[str, Any] + + +def _read_r_data(data: bytes) -> dict[str, Any]: + """The objects of an R data file (``save()``), by name. + + Numbers become numpy arrays (integers as int64, R's missing integer kept as + -2**31), text a list of str (None where missing), lists a list, each as an + ``_RVector`` with its attributes. + + Raises: + ValueError: If the data are not an R data file in the XDR format, or hold an + object this reader does not read. + """ + if data.startswith(b"\xfd7zXZ\x00"): + data = lzma.decompress(data) + elif data.startswith(b"BZh"): + data = bz2.decompress(data) + elif data.startswith(b"\x1f\x8b"): + data = gzip.decompress(data) + if data[:5] not in _HEADERS or data[5:7] != b"X\n": + raise ValueError( + "not an R data file in the XDR format (save() of R 2.4 or later)" + ) + reader = _RReader(data, 7) + version = reader.integer() + reader.integer() # the version of R that wrote it + reader.integer() # the oldest version of R that reads it + if version not in (2, 3): + raise ValueError(f"R's serialization format {version} is not read") + if version == 3: + length = reader.integer() + reader.position += length # the name of the native encoding + saved = reader.item() + if not isinstance(saved, list): + raise ValueError("an R data file holds a pairlist of named objects") + return {tag: value for tag, value in saved} + + +class _RReader: + """Reads R's serialization format, XDR, item by item.""" + + def __init__(self, data: bytes, position: int) -> None: + self.data = data + self.position = position + self.references: list[Any] = [] + + def integer(self) -> int: + if self.position + 4 > len(self.data): + raise ValueError("the R data end in the middle of an object") + value: int = struct.unpack_from(">i", self.data, self.position)[0] + self.position += 4 + return value + + def length(self) -> int: + length = self.integer() + if length == -1: # a long vector: the length in two words + upper, lower = self.integer(), self.integer() + length = (upper << 32) + (lower & 0xFFFFFFFF) + if length < 0: + raise ValueError(f"the R data hold a vector of length {length}") + return length + + def numbers(self, stored: str, size: int, length: int, kept: type) -> np.ndarray: + """``length`` numbers stored big-endian as ``stored``, as ``kept``.""" + end = self.position + size * length + if end > len(self.data): + raise ValueError("the R data end in the middle of a vector") + values = np.frombuffer(self.data, stored, length, self.position) + self.position = end + return values.astype(kept) + + def item(self) -> Any: + flags = self.integer() + kind = flags & 0xFF + if kind == _NIL or kind in _ENVIRONMENTS: + return None + if kind == _REFERENCE: + index = flags >> 8 or self.integer() + if not 1 <= index <= len(self.references): + raise ValueError(f"the R data refer to a missing object {index}") + return self.references[index - 1] + if kind == _SYMBOL: + symbol = self.item() + self.references.append(symbol) + return symbol + if kind == _PAIRLIST: + return self.pairlist(flags) + if kind == _CHARACTER_STRING: + length = self.integer() + if length == -1: + return None + text = self.data[self.position : self.position + length] + self.position += length + return text.decode("latin-1" if flags & _LATIN1 else "utf-8") + if kind == _ALTREP: + return self.altrep() + if kind in (_LOGICAL, _INTEGER): + values: Any = self.numbers(">i4", 4, self.length(), np.int64) + elif kind == _REAL: + values = self.numbers(">f8", 8, self.length(), np.float64) + elif kind in (_STRINGS, _LIST): + values = [self.item() for _ in range(self.length())] + else: + raise ValueError(f"the R data hold an object of type {kind}, not read here") + attributes = self.attributes() if flags & _HAS_ATTRIBUTES else {} + return _RVector(kind, values, attributes) + + def pairlist(self, flags: int) -> list[tuple[Any, Any]]: + """The (tag, value) pairs of a pairlist, its cells read one after another.""" + cells = [] + while True: + if flags & _HAS_ATTRIBUTES: + self.item() # the attributes of a cell, which data frames do not use + tag = self.item() if flags & _HAS_TAG else None + cells.append((tag, self.item())) + flags = self.integer() + if flags & 0xFF == _NIL: + return cells + if flags & 0xFF != _PAIRLIST: + raise ValueError("a pairlist of the R data continues with another type") + + def attributes(self) -> dict[str, Any]: + flags = self.integer() + if flags & 0xFF != _PAIRLIST: + raise ValueError("the attributes of an R vector are not a pairlist") + return {tag: value for tag, value in self.pairlist(flags)} + + def altrep(self) -> _RVector: + """A compact sequence, as R writes row names 1 to n; or a wrapped vector.""" + info = self.item() + state = self.item() + attributes = self.item() + names = [value for _, value in info] if isinstance(info, list) else [] + name = names[0] if names else None + if name in ("compact_intseq", "compact_realseq") and isinstance( + state, _RVector + ): + length, first, step = (float(v) for v in np.asarray(state.values)[:3]) + kind = _INTEGER if name == "compact_intseq" else _REAL + values = first + step * np.arange(int(length), dtype=np.float64) + if kind == _INTEGER: + values = values.astype(np.int64) + elif isinstance(name, str) and name.startswith("wrap_") and state is not None: + wrapped = state.values[0] + kind, values = wrapped.kind, wrapped.values + else: + raise ValueError( + f"the R data hold a compact vector {name!r}, not read here" + ) + found = dict(attributes) if isinstance(attributes, list) else {} + return _RVector(kind, values, found) diff --git a/cytepool/pathway_scores.py b/cytepool/pathway_scores.py index 216f4b0..1a59639 100644 --- a/cytepool/pathway_scores.py +++ b/cytepool/pathway_scores.py @@ -176,35 +176,26 @@ from dataclasses import dataclass from functools import cache from types import MappingProxyType -from typing import Any, NamedTuple +from typing import Any import numpy as np import numpy.typing as npt from numba import prange from ._columns import text_column -from ._gene_ids import is_missing +from ._expression_values import MIN_CELLS, ExpressionValues, expression_values from ._jit import by_chunks, jit, thread_limit -from ._masked_arrays import ( - Missing, - read_only, - real_array, - reject_masked, - unmasked_array, -) +from ._masked_arrays import read_only, reject_masked from ._messages import quoted from ._options import ( checked_choice, checked_flag, - integral_number, number_at_least, positive_integer, ) from ._ranked_genes import gene_set_collection from ._summation import observed_means -from .cube import Cube from .gene_sets import GeneSets -from .normalization import filter_by_expression, log_cpm, tmm_factors from .stats.distributions import norm_cdf from .stats.special import gammaincc_scalar from .table import Table @@ -216,16 +207,12 @@ _KERNELS = ("auto", "gaussian", "poisson", "none") _SCORE_TYPES = ("difference", "kuiper", "max_deviation") -_VALUES = ("log_cpm", "counts") _KERNEL_CODES = {"gaussian": 0, "poisson": 1, "none": 2} # GSVA's table of the normal distribution function: its values at multiples of # 0.001 (found by probing GSVA 2.0.0). _TABLE_STEP = 0.001 _BANDWIDTH_DIVISOR = 4.0 # the bandwidth is a quarter of the standard deviation _POISSON_OFFSET = 0.5 # the Poisson kernel's mean, x + 1/2 (Haenzelmann et al. 2013) -# As limma_voom leaves samples out, and edgeR's cpm(log = TRUE) shifts counts. -_MIN_CELLS = 10 -_PRIOR_COUNT = 2.0 # PLAGE's sign: a sum of loadings below this share of their sizes is zero. _ZERO_SUM = 1e-10 @@ -292,16 +279,6 @@ def __repr__(self) -> str: ) -class _Data(NamedTuple): - """The values scored, with their samples and genes.""" - - values: FloatArray # samples x genes, NaN where missing - integer: bool # whether the values were stored as integers - samples: Table - gene_ids: list[str | None] # one per column; None for a gene without an id - settings: dict[str, Any] # how a cube's values were taken - - def gsva( data: Any, gene_sets: Mapping[str, Any], @@ -310,7 +287,7 @@ def gsva( sample_ids: Any = None, group: str | None = None, values: str = "log_cpm", - min_cells: int = _MIN_CELLS, + min_cells: int = MIN_CELLS, expression_filter: bool = True, kernel: str = "auto", min_samples_without_kernel: int = 200, @@ -400,7 +377,7 @@ def gsva( power = number_at_least(exponent, "exponent", 0.0) score = _SCORE_TYPES.index(checked_choice(score_type, "score_type", _SCORE_TYPES)) collection = _collection(gene_sets) - expression = _expression( + expression = expression_values( data, gene_ids, sample_ids, group, values, min_cells, expression_filter ) n_samples = expression.values.shape[0] @@ -445,7 +422,7 @@ def ssgsea( sample_ids: Any = None, group: str | None = None, values: str = "log_cpm", - min_cells: int = _MIN_CELLS, + min_cells: int = MIN_CELLS, expression_filter: bool = True, exponent: float = 0.25, normalize: bool = True, @@ -502,7 +479,7 @@ def ssgsea( power = number_at_least(exponent, "exponent", 0.0) scaled = checked_flag(normalize, "normalize") collection = _collection(gene_sets) - expression = _expression( + expression = expression_values( data, gene_ids, sample_ids, group, values, min_cells, expression_filter ) kept = np.arange(expression.values.shape[1]) @@ -539,7 +516,7 @@ def combined_z_scores( sample_ids: Any = None, group: str | None = None, values: str = "log_cpm", - min_cells: int = _MIN_CELLS, + min_cells: int = MIN_CELLS, expression_filter: bool = True, min_size: int = 1, max_size: int | None = None, @@ -583,7 +560,7 @@ def combined_z_scores( """ checked = _checked_size(min_size, max_size) collection = _collection(gene_sets) - expression = _expression( + expression = expression_values( data, gene_ids, sample_ids, group, values, min_cells, expression_filter ) kept = _varying_genes(expression.values) @@ -614,7 +591,7 @@ def plage( sample_ids: Any = None, group: str | None = None, values: str = "log_cpm", - min_cells: int = _MIN_CELLS, + min_cells: int = MIN_CELLS, expression_filter: bool = True, min_size: int = 1, max_size: int | None = None, @@ -660,7 +637,7 @@ def plage( """ checked = _checked_size(min_size, max_size) collection = _collection(gene_sets) - expression = _expression( + expression = expression_values( data, gene_ids, sample_ids, @@ -695,228 +672,6 @@ def plage( # The data. -def _expression( - data: Any, - gene_ids: Any, - sample_ids: Any, - group: Any, - values: Any, - min_cells: Any, - expression_filter: Any, - *, - missing: Missing = "NaN", -) -> _Data: - """The values to score, samples x genes, with their samples and gene ids; - ``missing`` says how the values mark missing ones, for messages.""" - reject_masked(gene_ids, "gene_ids", missing="None") - reject_masked(sample_ids, "sample_ids") - if isinstance(data, Cube): - if sample_ids is not None: - raise ValueError( - "sample_ids applies to an array; a cube's samples have their ids" - ) - return _cube_values(data, gene_ids, group, values, min_cells, expression_filter) - defaults = ( - ("group", group is None), - ("values", isinstance(values, str) and values == "log_cpm"), - ( - "min_cells", - isinstance(min_cells, int) - and not isinstance(min_cells, bool) - and min_cells == _MIN_CELLS, - ), - ("expression_filter", expression_filter is True), - ) - for name, default in defaults: - if not default: - raise ValueError( - f"{name} applies to a cube, and data is a {type(data).__name__}" - ) - array = unmasked_array(data, "data", missing=missing) - integer = array.dtype.kind in "iu" - try: - matrix = real_array(array, "data", boolean=False, objects=False) - except TypeError: - raise TypeError( - "data must hold real numbers, samples x genes, or be a Cube; got dtype " - f"{array.dtype}" - ) from None - if matrix.ndim != 2: - raise ValueError( - "data must be a 2-D array of shape (n_samples, n_genes), or a Cube; got " - f"shape {matrix.shape}" - ) - n_samples, n_genes = matrix.shape - if n_samples == 0 or n_genes == 0: - raise ValueError( - f"data must have at least one sample and one gene, got shape {matrix.shape}" - ) - if np.isinf(matrix).any(): - raise ValueError("data holds infinite values; mark missing values with NaN") - if gene_ids is None: - raise ValueError( - "gene_ids is needed with an array: one id per column, which the gene " - "sets name" - ) - if isinstance(gene_ids, str): - raise TypeError( - "gene_ids names a column of a cube's features table; with an array, give " - "one id per column" - ) - ids = _gene_id_list(gene_ids, n_genes, "gene_ids") - _check_unique(ids, "gene_ids") - if sample_ids is None: - names = [str(i + 1) for i in range(n_samples)] - else: - names = _sample_id_list(sample_ids, n_samples) - return _Data( - np.ascontiguousarray(matrix, dtype=np.float64), - integer, - Table({"sample_id": text_column(names)}), - ids, - {}, - ) - - -def _cube_values( - cube: Cube, - gene_ids: Any, - group: Any, - values: Any, - min_cells: Any, - expression_filter: Any, -) -> _Data: - """A cube group's log-CPM or counts, of its samples with enough cells and counts, - over the genes kept by filterByExpr.""" - kind = checked_choice(values, "values", _VALUES) - fewest = integral_number(min_cells, "min_cells") - if fewest < 0: - raise ValueError(f"min_cells must be at least 0, got {fewest}") - filtered = checked_flag(expression_filter, "expression_filter") - if group is None: - if cube.n_groups != 1: - raise ValueError( - f"the cube has {cube.n_groups} groups; name the one to score with " - f"group, one of {quoted(cube.groups['group_id'].tolist())}" - ) - group = cube.groups["group_id"][0] - if not isinstance(group, str): - reject_masked(group, "group") - raise TypeError(f"group must be a group id (str), got {type(group).__name__}") - counts = cube.layer("counts", group) - used = np.asarray(counts.sum(axis=1, dtype=np.float64) > 0.0) - if cube.n_cells is not None: - position = cube.groups["group_id"].tolist().index(group) - used &= np.asarray(cube.n_cells)[:, position] >= fewest - rows = np.flatnonzero(used) - if rows.size == 0: - raise ValueError( - f"no sample of group {group!r} has counts and at least {fewest} cells" - ) - kept = counts[rows] - genes = np.ones(kept.shape[1], dtype=bool) - if filtered: - genes = filter_by_expression(kept, group=np.zeros(rows.size)) - if not genes.any(): - raise ValueError( - f"no gene of group {group!r} passes filter_by_expression; pass " - "expression_filter=False to score every gene" - ) - kept = kept[:, genes] - if kind == "log_cpm": - matrix = log_cpm(kept, norm_factors=tmm_factors(kept), prior_count=_PRIOR_COUNT) - integer = False - else: - matrix = np.asarray(kept, dtype=np.float64) - integer = kept.dtype.kind in "iu" - if gene_ids is None: - gene_ids = "feature_id" - if isinstance(gene_ids, str): - features = cube.features - if gene_ids not in features: - raise KeyError( - f"the cube's features table has no column {gene_ids!r}; its columns " - f"are {quoted(list(features.columns))}" - ) - label = f"the features column {gene_ids!r}" - ids = _gene_id_list(features[gene_ids], cube.n_features, label) - else: - label = "gene_ids" - ids = _gene_id_list(gene_ids, cube.n_features, label) - ids = [ids[j] for j in np.flatnonzero(genes).tolist()] - _check_unique(ids, label) - settings = { - "group": group, - "values": kind, - "min_cells": fewest, - "expression_filter": filtered, - } - return _Data( - np.ascontiguousarray(matrix, dtype=np.float64), - integer, - cube.samples.take(rows), - ids, - settings, - ) - - -def _gene_id_list(values: Any, n: int, name: str) -> list[str | None]: - """One gene id per gene, None where missing.""" - if isinstance(values, np.ndarray): - found = values.tolist() - else: - try: - found = list(values) - except TypeError: - raise TypeError( - f"{name} must be a sequence of gene ids, got {type(values).__name__}" - ) from None - if len(found) != n: - raise ValueError(f"{name} must hold one id per gene ({n}), got {len(found)}") - ids: list[str | None] = [] - for value in found: - if isinstance(value, str) and value: - ids.append(str(value)) - elif is_missing(value): - ids.append(None) - else: - raise TypeError( - f"{name} must hold gene ids as strings, got {type(value).__name__} " - f"{value!r}; convert numeric ids, such as Entrez ids, to text first" - ) - return ids - - -def _check_unique(ids: list[str | None], name: str) -> None: - """Refuse an id that names two of the genes scored.""" - seen: set[str] = set() - for gene in ids: - if gene is not None: - if gene in seen: - raise ValueError( - f"{name} names two of the genes scored {gene!r}; each gene needs " - "an id of its own to be matched to the sets: keep one gene per id, " - "or match the sets by other ids" - ) - seen.add(gene) - - -def _sample_id_list(values: Any, n: int) -> list[str]: - array = unmasked_array(values, "sample_ids") - if array.ndim != 1 or array.size != n: - raise ValueError( - f"sample_ids must hold one id per sample ({n}), got shape {array.shape}" - ) - names = [] - for value in array.tolist(): - if is_missing(value): - raise ValueError("sample_ids has a missing id; give every sample one") - names.append(str(value)) - if len(set(names)) != n: - raise ValueError("sample_ids must be unique") - return names - - def _check_counts(values: FloatArray) -> None: observed = values[~np.isnan(values)] if observed.size and ( @@ -1011,7 +766,7 @@ def _matched_sets( def _result( scores: FloatArray, - expression: _Data, + expression: ExpressionValues, collection: GeneSets, names: list[str], members: list[IntArray], diff --git a/cytepool/plotting.py b/cytepool/plotting.py index e6b40f8..2bd8b0a 100644 --- a/cytepool/plotting.py +++ b/cytepool/plotting.py @@ -1,5 +1,5 @@ """Figures of pseudobulk and bulk analyses, drawn as limma, edgeR, DESeq2, speckle, -metafor, variancePartition and fgsea draw them. +metafor, variancePartition, fgsea and decoupler draw them. Each function draws one figure from the objects that CytePool's analyses return: a cube, a voom result, a fit, the table of a differential-expression or gene-set test, @@ -39,6 +39,10 @@ (Korotkevich et al. 2021); :func:`plot_pathway_scores`, the scores of gene sets in every sample as a heatmap, as GSVA's guide draws them (Haenzelmann, Castelo and Guinney 2013). +- Activities: :func:`plot_activities`, the activities of transcription factors or + pathways in every sample or contrast as a heatmap, and :func:`plot_top_activities`, + the sources with the largest scores of one profile as bars, as decoupler's + ``pl.barplot`` draws them (Badia-i-Mompel et al. 2022). - Count models: :func:`plot_dispersions`, DESeq2's ``plotDispEsts`` (Love et al. 2014); :func:`plot_quasi_dispersions`, edgeR's ``plotQLDisp`` (Chen et al. 2016). - Meta-analysis: :func:`plot_forest`, the forest plot (Lewis and Clarke 2001) that @@ -137,6 +141,11 @@ microarray and RNA-seq data. BMC Bioinformatics 14, 7. doi:10.1186/1471-2105-14-7 + Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S, + Taus P, Dugourd A, Holland CH, Ramirez Flores RO, Saez-Rodriguez J (2022). + decoupleR: ensemble of computational methods to infer biological activities from + omics data. Bioinformatics Advances 2, vbac016. doi:10.1093/bioadv/vbac016 + Lewis S, Clarke M (2001). Forest plots: trying to see the wood and the trees. BMJ 322, 1479-1480. doi:10.1136/bmj.322.7300.1479 @@ -193,6 +202,7 @@ ranked_genes, running_sum, ) +from .activity import Activities from .composition import CompositionResult from .cube import Cube from .differential_expression import DifferentialExpressionResult @@ -218,6 +228,7 @@ from matplotlib.axes import Axes __all__ = [ + "plot_activities", "plot_barcode", "plot_cell_numbers", "plot_composition", @@ -240,6 +251,7 @@ "plot_sample_weights", "plot_significant_genes", "plot_study_agreement", + "plot_top_activities", "plot_variance_fractions", "plot_volcano", ] @@ -2826,15 +2838,7 @@ def plot_pathway_scores( groups = ([labels[i] for i in order.tolist()], hues) values = values[:, order] if scaled: - # Each row's mean and standard deviation over its observed scores; NaN for a - # row of fewer than two, or of equal scores. - observed = np.isfinite(values) - counts = observed.sum(axis=1, keepdims=True).astype(np.float64) - with np.errstate(invalid="ignore", divide="ignore"): - means = np.where(observed, values, 0.0).sum(axis=1, keepdims=True) / counts - centred = values - means - squares = np.where(observed, centred**2, 0.0).sum(axis=1, keepdims=True) - values = centred / np.sqrt(squares / (counts - 1.0)) + values = _standardized_rows(values) label = _SCORE_LABELS[scores.method] ax = _axes( ax, @@ -2931,6 +2935,245 @@ def _draw_heatmap( ax.figure.colorbar(image, ax=ax, label=colorbar_label, shrink=0.8) +def _standardized_rows(values: FloatArray) -> FloatArray: + """Each row centred on the mean of its finite values and divided by their + standard deviation, as R's ``heatmap(scale = "row")``; NaN for a row of fewer + than two values, or of equal ones.""" + observed = np.isfinite(values) + counts = observed.sum(axis=1, keepdims=True).astype(np.float64) + with np.errstate(invalid="ignore", divide="ignore"): + means = np.where(observed, values, 0.0).sum(axis=1, keepdims=True) / counts + centred = values - means + squares = np.where(observed, centred**2, 0.0).sum(axis=1, keepdims=True) + scaled: FloatArray = centred / np.sqrt(squares / (counts - 1.0)) + return scaled + + +# Activities. + + +def plot_activities( + activities: Activities, + *, + sources: Sequence[str] | None = None, + top: int | None = 25, + color_by: str | Sequence[Any] | None = None, + standardize: bool = False, + ax: "Axes | None" = None, +) -> "Axes": + """Plot the activities of the sources of a network in every profile as a + heatmap. + + A row for each source and a column for each profile, a sample or a contrast, each + cell coloured by the source's score in the profile, on the scale of + :func:`plot_pathway_scores`, from blue (low) through grey (zero) to red (high), + symmetric about zero. The sources are those named by ``sources``, top to bottom, + or else the ``top`` sources whose largest absolute score over the profiles is + largest, in the order of the network. ``color_by`` groups the profiles under a + strip of the groups' colours, and ``standardize`` centres and scales each + source's scores across the profiles, as for :func:`plot_pathway_scores`. NaN + scores, of sources with too few targets among a profile's genes, are dark grey. + + Draws the cells (gid ``"scores"``) and, with ``color_by``, the strip (gid + ``"groups"``) and the lines between groups (gid ``"group boundaries"``). + + Args: + activities: The activities of :func:`~cytepool.activity.ulm`, + :func:`~cytepool.activity.mlm` or + :func:`~cytepool.activity.weighted_aggregate`. + sources: The sources to show, top to bottom; by default the ``top`` + sources. + top: When ``sources`` is None, how many sources to show, at least 1; None + shows every source. + color_by: A column of ``activities.profiles``, such as ``"condition"`` for + the activities of a cube's group, or one label per profile, at most + eight levels. + standardize: Whether to standardize each source's scores across the + profiles. + ax: The Axes to draw into, or None for a new figure. + + Returns: + The Axes of the heatmap. + + Raises: + TypeError: If ``activities`` is not an ``Activities``, an option has the + wrong type, or an argument is or holds a numpy masked array. + ValueError: If a source is not among the activities or is named twice, an + option is out of range, the labels do not match the profiles, or + ``color_by`` has more than eight levels. + KeyError: If ``color_by`` names a column the profiles do not have. + """ + _check_axes(ax) + _check_activities(activities) + scaled = checked_flag(standardize, "standardize") + reject_masked(top, "top") + count = None if top is None else positive_integer(top, "top") + names = list(activities.sources) + if sources is not None: + chosen = _chosen_sources(sources, names) + elif count is None: + chosen = names + else: + with np.errstate(invalid="ignore"): + largest = np.nanmax(np.abs(activities.scores), axis=0, initial=-np.inf) + shown = np.argsort(-largest, kind="stable")[:count] + chosen = [names[j] for j in np.sort(shown).tolist()] + values = activities.scores[:, [names.index(name) for name in chosen]].T + table = activities.profiles + n_profiles = values.shape[1] + profiles = _Samples( + ids=[str(value) for value in table["profile_id"].tolist()], + rows=list(range(n_profiles)), + n_rows=n_profiles, + variables={name: table[name] for name in table.columns}, + source="the activities' profiles", + ) + order = np.arange(n_profiles) + groups = None + if color_by is not None: + _check_variable(color_by, "color_by") + labels, levels = profiles.labels(color_by, "color_by") + hues = _hues(levels, "color_by", "merge levels or give fewer labels") + position = {level: i for i, level in enumerate(levels)} + order = np.argsort([position[label] for label in labels], kind="stable") + groups = ([labels[i] for i in order.tolist()], hues) + values = values[:, order] + if scaled: + values = _standardized_rows(values) + ax = _axes( + ax, + (min(14.0, 3.5 + 0.22 * n_profiles), min(14.0, 1.8 + 0.28 * len(chosen))), + ) + _draw_heatmap( + ax, + values, + chosen, + [profiles.ids[i] for i in order.tolist()], + "Standardized activity" if scaled else _activity_label(activities), + groups, + ) + return ax + + +def plot_top_activities( + activities: Activities, + profile: str | None = None, + *, + top: int = 25, + ax: "Axes | None" = None, +) -> "Axes": + """Plot the sources with the largest activity scores of one profile as bars. + + decoupler's ``pl.barplot`` (Badia-i-Mompel et al. 2022): the ``top`` sources with + the largest absolute scores in the profile, in decreasing order of their scores + from top to bottom, each bar as long as the score and coloured by it on the + scale of :func:`plot_activities`, symmetric about zero. Sources without a score + in the profile (NaN) are left out. + + Draws the bars (gid ``"activities"``). + + Args: + activities: The activities of :func:`~cytepool.activity.ulm`, + :func:`~cytepool.activity.mlm` or + :func:`~cytepool.activity.weighted_aggregate`. + profile: The ``profile_id`` of the profile to show; it may be left out when + the activities have one profile. + top: How many sources to show, at least 1. + ax: The Axes to draw into, or None for a new figure. + + Returns: + The Axes. + + Raises: + TypeError: If ``activities`` is not an ``Activities``, an option has the + wrong type, or an argument is or holds a numpy masked array. + ValueError: If ``profile`` is left out of several profiles, ``top`` is below + 1, or the profile has no score. + KeyError: If no profile has the id ``profile``. + """ + _check_axes(ax) + _check_activities(activities) + count = positive_integer(top, "top") + ids = [str(value) for value in activities.profiles["profile_id"].tolist()] + if profile is None: + if len(ids) != 1: + raise ValueError( + f"the activities have {len(ids)} profiles; name the one to show with " + f"profile, one of {quoted(ids)}" + ) + row = 0 + else: + if not isinstance(profile, str): + reject_masked(profile, "profile") + raise TypeError( + f"profile must be a profile id (str), got {type(profile).__name__}" + ) + if profile not in ids: + raise KeyError( + f"no profile {profile!r} among the activities: {quoted(ids)}" + ) + row = ids.index(profile) + scores = activities.scores[row] + scored = np.flatnonzero(~np.isnan(scores)) + if scored.size == 0: + raise ValueError(f"the profile {ids[row]!r} has no score") + shown = scored[np.argsort(-np.abs(scores[scored]), kind="stable")[:count]] + shown = shown[np.argsort(-scores[shown], kind="stable")] + values = scores[shown] + names = [activities.sources[j] for j in shown.tolist()] + colors = _module("matplotlib.colors") + ramp = colors.LinearSegmentedColormap.from_list("cytepool_diverging", _DIVERGING) + limit = float(np.max(np.abs(values))) or 1.0 + shades = ramp(colors.Normalize(vmin=-limit, vmax=limit)(values)) + ax = _axes(ax, (5.0, min(14.0, 1.2 + 0.26 * values.size))) + positions = np.arange(values.size, dtype=np.float64) + ax.barh(positions, values, color=shades, height=0.8, gid="activities") + ax.axvline(0.0, color=_INK, linewidth=0.8) + ax.set_yticks(positions, names) + ax.set_ylim(values.size - 0.5, -0.5) + ax.tick_params(axis="y", length=0) + ax.spines[["top", "right"]].set_visible(False) + ax.set_xlabel(_activity_label(activities)) + ax.set_title(ids[row], loc="left") + return ax + + +def _check_activities(activities: Any) -> None: + if not isinstance(activities, Activities): + raise TypeError( + "activities must be an Activities, as ulm, mlm and weighted_aggregate " + f"return, got {type(activities).__name__}" + ) + + +def _chosen_sources(sources: Any, names: list[str]) -> list[str]: + """The sources named, checked against those of the activities.""" + reject_masked(sources, "sources") + chosen = [sources] if isinstance(sources, str) else list(sources) + for name in chosen: + if not isinstance(name, str): + raise TypeError(f"sources must hold source names (str), got {name!r}") + if name not in names: + raise ValueError( + f"no source {name!r} among the activities; they hold {quoted(names)}" + ) + if len(set(chosen)) != len(chosen): + raise ValueError("sources names a source twice") + if not chosen: + raise ValueError("sources is empty; name at least one source") + return chosen + + +def _activity_label(activities: Activities) -> str: + """What the scores of the activities are, for an axis.""" + if activities.method != "weighted_aggregate": + return "Activity (t-statistic)" + kind = "mean" if activities.settings.get("statistic") == "mean" else "sum" + if activities.settings.get("n_permutations"): + return f"Normalized weighted {kind}" + return f"Weighted {kind}" + + # Count models. diff --git a/docs/source/index.md b/docs/source/index.md index ef1412d..c3eb678 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -34,7 +34,7 @@ what CytePool offers for each today. | --- | --- | --- | | Which genes change in each cell type? | limma-voom, limma-trend and dream on every cell type of a cube; edgeR's quasi-likelihood tests and DESeq2 on one matrix at a time | [Differential expression per cell type](differential_expression.ipynb), [Bulk RNA-seq](guides/bulk_rna_seq.ipynb), [Choosing a method](statistics/choosing_a_method.md) | | Which cell types expand or contract? | propeller. Not yet: precision-weighted mixed models or count models of composition | [Composition tests](statistics/composition.md) | -| Which pathways and regulators respond? | camera, fry, roast, mroast and romer on any contrast; gene set enrichment analysis (GSEA) of ranked genes; over-representation analysis of the genes that change; scores of pathways in every sample (GSVA, ssGSEA, combined z-scores, PLAGE); gene sets read from GMT files. Not yet: activities of transcription factors and pathways from regulatory networks | [Gene-set tests](statistics/gene_set_tests.md) | +| Which pathways and regulators respond? | camera, fry, roast, mroast and romer on any contrast; gene set enrichment analysis (GSEA) of ranked genes; over-representation analysis of the genes that change; scores of pathways in every sample (GSVA, ssGSEA, combined z-scores, PLAGE); activities of transcription factors and pathways from the networks CollecTRI and PROGENy (decoupler's ULM, MLM and weighted aggregate); gene sets read from GMT files | [Gene-set tests](statistics/gene_set_tests.md) | | What drives the variation between donors, and what must be adjusted for? | Design checks for confounded factors, sample quality weights, mixed models that estimate a variance per random effect for every gene, and variance partitioning: the share of each gene's variance that each variable explains, in every cell type | [Designs, linear models and contrasts](statistics/linear_models.md), [Mixed models, REML and dream](statistics/mixed_models.md) | | Does it replicate, and how should studies be combined? | Studies aligned by sample, gene and cell type, each analysed on its own and lined up gene by gene; meta-analysis of every gene across studies, with the between-study variance, Q and I²; combined p-values, the p-value that a change replicates in at least u studies, sign agreement, pi1 between studies and the genes found in k studies. Not yet: shrinkage across studies | [Meta-analysis](statistics/meta_analysis.md), [The cube and its additive layers](statistics/cubes.md) | | Can patients be stratified from their profiles? | Not yet; cubes convert to AnnData and MuData for other tools | [Capability matrix](reference/capabilities.md) | diff --git a/docs/source/reference/api/activity.md b/docs/source/reference/api/activity.md new file mode 100644 index 0000000..515f611 --- /dev/null +++ b/docs/source/reference/api/activity.md @@ -0,0 +1,22 @@ +--- +myst: + html_meta: + description: Activities of transcription factors and pathways in CytePool, as decoupler computes them, by univariate and multivariate linear models (ULM, MLM) and the weighted aggregate, from an expression matrix, a cell type of a cube or the statistics of a differential-expression result. +--- + +# Pathway and regulator activity + +{func}`~cytepool.activity.ulm`, {func}`~cytepool.activity.mlm` and +{func}`~cytepool.activity.weighted_aggregate` score every source of a network, a +transcription factor or a pathway, in every profile: the samples of an expression +matrix or of a cell type of a cube, or the contrasts of a differential-expression +result, as decoupler's `mt.ulm`, `mt.mlm` and `mt.waggr` compute them. The activities, +an {class}`~cytepool.activity.Activities`, are profiles x sources, so that +{func}`~cytepool.stats.linear_models.fit_linear_model` tests them with a design built +from their profiles; {func}`~cytepool.plotting.plot_activities` draws them as a +heatmap and {func}`~cytepool.plotting.plot_top_activities` the top sources of a +profile as bars. The networks come from {mod}`cytepool.networks`. + +```{eval-rst} +.. automodule:: cytepool.activity +``` diff --git a/docs/source/reference/api/index.md b/docs/source/reference/api/index.md index de47926..deb65d4 100644 --- a/docs/source/reference/api/index.md +++ b/docs/source/reference/api/index.md @@ -1,7 +1,7 @@ --- myst: html_meta: - description: The CytePool Python API for pseudobulk cubes, their conversion to and from AnnData and MuData, designs, normalization, linear models, mixed models, voom, dream, sample quality weights, negative binomial models, quasi-likelihood F-tests, DESeq2's dispersions and tests, empirical Bayes moderation, gene-set tests (including rotation tests), pathway scores, multiple testing, differential expression, cell-type composition, meta-analysis, combined p-values and replication across studies and distributions. + description: The CytePool Python API for pseudobulk cubes, their conversion to and from AnnData and MuData, designs, normalization, linear models, mixed models, voom, dream, sample quality weights, negative binomial models, quasi-likelihood F-tests, DESeq2's dispersions and tests, empirical Bayes moderation, gene-set tests (including rotation tests), pathway scores, networks of regulators and pathways and their activities, multiple testing, differential expression, cell-type composition, meta-analysis, combined p-values and replication across studies and distributions. --- # CytePool API reference @@ -40,6 +40,8 @@ rotation_gene_set_tests over_representation gsea pathway_scores +networks +activity multiple_testing combined_p_values differential_expression diff --git a/docs/source/reference/api/networks.md b/docs/source/reference/api/networks.md new file mode 100644 index 0000000..6b5d12e --- /dev/null +++ b/docs/source/reference/api/networks.md @@ -0,0 +1,21 @@ +--- +myst: + html_meta: + description: Networks of transcription factors and pathways and their target genes in CytePool, with signed weights, such as CollecTRI's regulons, at a licence level for commercial or academic use, and PROGENy's pathway signatures, downloaded from their releases with pinned checksums, or any network given as a table. +--- + +# Networks + +A {class}`~cytepool.networks.Network` maps each source, a transcription factor or a +pathway, to its target genes with signed weights, and records its name, version, +licence, citation and the file it was read from. {func}`~cytepool.networks.collectri` +returns CollecTRI's regulons of human transcription factors, at the licence level you +choose, and {func}`~cytepool.networks.progeny` the pathway signatures of PROGENy for +human or mouse; both download their files on first use, check them against pinned +SHA-256 digests and keep them in a cache. The activity functions of +{mod}`cytepool.activity` take a network, or a table with the columns `source`, +`target` and `weight`. + +```{eval-rst} +.. automodule:: cytepool.networks +``` diff --git a/docs/source/reference/bibliography.md b/docs/source/reference/bibliography.md index f56c709..9a453d3 100644 --- a/docs/source/reference/bibliography.md +++ b/docs/source/reference/bibliography.md @@ -53,6 +53,10 @@ random numbers of its tests [@matsumoto1998; @durstenfeld1964]. Scores of gene sets in every sample [@haenzelmann2013; @barbie2009; @lee2008; @tomfohr2005]. +Activities of regulators and pathways, and their networks [@badiaimompel2022; +@muellerdott2023; @schubert2018; @holland2020b; @tuerei2021], and the running mean and +variance of their permutations [@welford1962]. + ## Composition [@phipson2022; @buttner2021; @mangiola2023; @hoffman2026b; @dann2022; @yi2024] diff --git a/docs/source/reference/capabilities.md b/docs/source/reference/capabilities.md index 8bcaff7..79859ad 100644 --- a/docs/source/reference/capabilities.md +++ b/docs/source/reference/capabilities.md @@ -42,6 +42,7 @@ size; its sections give every check. | Over-representation analysis (limma's `kegga` and `goana`) of the genes up, down or either way, and its dot plot | {api}`over_representation`, {api}`plot_over_representation` | limma 3.62.1, R 4.4.3 | exact | [`gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py) | | Gene set enrichment analysis of ranked genes (fgsea's `fgsea`: `fgseaMultilevel` and `fgseaSimple`), with its enrichment plot and table (`plotEnrichment`, `plotGseaTable`) | {api}`gsea`, {api}`plot_enrichment`, {api}`plot_gsea_table` | fgsea 1.32.2 | calibrated; exact given fgsea's seeds, and for enrichment scores, leading edges and figures | [`gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py) | | Scores of gene sets in every sample (GSVA's `gsva` with `gsvaParam`, `ssgseaParam`, `zscoreParam` and `plageParam`), and their heatmap | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`, {api}`plot_pathway_scores` | GSVA 2.0.0 | exact; PLAGE up to sign | [`pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py) | +| Activities of regulators and pathways (decoupler's `mt.ulm`, `mt.mlm` and `mt.waggr`) from samples, a cube's cell type or contrasts, the networks CollecTRI and PROGENy downloaded with their checksums and licences, and the bar plot of the top sources (`pl.barplot`) | {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate`, {api}`collectri`, {api}`progeny`, {api}`plot_top_activities` | decoupler 2.2.0, progeny 1.28.0 | exact; exact given decoupler's permutations for normalized scores | [`activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py) | | Adjustment for multiple testing (R's `p.adjust`) | {api}`adjust_p_values` | R 4.4.3 | exact | [`multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py) | | limma-voom and limma-trend on every cell type of a cube | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact; close with robust moderation | [`differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py) | | Options for pseudobulk data: quality weights, weights from cell numbers, a minimum count, adjustment across cell types | {api}`limma_voom`, {api}`limma_trend` | limma 3.62.1, edgeR 4.4.0 | exact; close with quality weights | [`differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py) | @@ -65,7 +66,6 @@ have no CytePool function yet: - random slopes in mixed models; - composition tests with precision weights or random effects (crumblr), and Bayesian count models of composition; -- activities of pathways and transcription factors from regulatory networks; - ligand-receptor scoring and differential cell-cell interactions; - dose-response fits, benchmark doses and drug synergy; - factorization of samples across cell types; diff --git a/docs/source/reference/citing.md b/docs/source/reference/citing.md index 163c8b8..a18f875 100644 --- a/docs/source/reference/citing.md +++ b/docs/source/reference/citing.md @@ -35,6 +35,7 @@ the credit for the methods belongs to their authors. | {api}`romer` | [@majewski2010] | | {api}`over_representation` | limma's `kegga` and `goana` [@young2010]; the hypergeometric test [@tavazoie1999] | | {api}`gsea` | fgsea [@korotkevich2021]; GSEA [@subramanian2005] | +| {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate` | decoupler [@badiaimompel2022], and the network you used: CollecTRI [@muellerdott2023] or PROGENy [@schubert2018], for mouse [@holland2020b] | | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage` | the GSVA package [@haenzelmann2013], and the paper of the method you chose: GSVA [@haenzelmann2013], ssGSEA [@barbie2009], combined z-scores [@lee2008] or PLAGE [@tomfohr2005] | | {api}`read_gmt` | the collection you read, as its provider asks; for MSigDB, [@subramanian2005; @liberzon2011] | | {api}`propeller` | [@phipson2022] | diff --git a/docs/source/reference/datasets.md b/docs/source/reference/datasets.md index 67bfd6a..f32805c 100644 --- a/docs/source/reference/datasets.md +++ b/docs/source/reference/datasets.md @@ -1,5 +1,5 @@ --- -description: The datasets the documentation's pages read, where each comes from, its licence and citation, and how to prepare them on another machine. +description: The datasets the documentation's pages read and the networks CytePool downloads, where each comes from, its licence and citation, and how to prepare them on another machine. --- # Datasets @@ -34,3 +34,27 @@ the same directory, so a machine fetches them once; its cube is the one that scr tests. A notebook page names its datasets in its metadata; `make -C docs execute` prepares them, links them into the directory the page runs in, and records the fingerprint of each file it was given, which the build checks against the registry. + +## Networks + +The activity functions of {mod}`cytepool.activity` read networks that {api}`collectri` +and {api}`progeny` download on first use into `cytepool/networks` under +`XDG_CACHE_HOME` or `~/.cache` (or the directory given as `cache_dir`), each checked +against its size and SHA-256 digest; no file of them is in the repository. + +| Network | What it holds | Source | Licence | Cite | +| --- | --- | --- | --- | --- | +| CollecTRI | 43,175 signed interactions of 1,186 human transcription factors with 6,693 target genes, gathered from twelve resources; the file `CollecTRI_regulons.csv`, 4,345,649 bytes, SHA-256 `4473c9189dd53dacc80297709ad1452dda1086a1cc2185f9a56146c261668701` | [Zenodo record 8192729](https://zenodo.org/records/8192729), version 2.0 | Creative Commons Attribution 4.0 for the record; each resource keeps its own terms, below | @muellerdott2023 | +| PROGENy | The weights (z-scores) and p-values of 22,479 human and 17,426 mouse genes in 14 signalling pathways; the R package `progeny_1.28.0.tar.gz`, 10,642,873 bytes, SHA-256 `2c41531ef6d088047e2830ccc5741aa0f28f48fb51d4c164cc8331e19b925d7b` | [progeny 1.28.0](https://bioconductor.org/packages/3.20/bioc/html/progeny.html), Bioconductor 3.20 | Apache License 2.0 | @schubert2018; mouse @holland2020b | + +CollecTRI's resources and their terms, as OmniPath's records of resources list them +[@tuerei2021]: Creative Commons Attribution 4.0 for ExTRI, NTNU Curated, TFactS, +SIGNOR, IntAct, CytReg, GOA and Pavlidis2021; Creative Commons Attribution-ShareAlike +4.0 for TRRUST; the GNU Lesser General Public License 3.0 for HTRIdb; an informal +licence for DoRothEA's level A; and the Academic Free License 3.0 for GEREDB, which +OmniPath lists for academic use. `collectri(licence="commercial")`, the default, keeps +the 39,896 interactions that at least one resource listed for commercial use supports; +`collectri(licence="academic")` keeps all 43,175, including the 3,279 that GEREDB alone +supports. Each level is subject to the terms of the resources it keeps, such as +attribution; the providers' own statements of their terms prevail over these +records. diff --git a/docs/source/reference/glossary.md b/docs/source/reference/glossary.md index b367974..c6f5611 100644 --- a/docs/source/reference/glossary.md +++ b/docs/source/reference/glossary.md @@ -4,6 +4,11 @@ description: The terms the CytePool documentation uses, each in one or two sente # Glossary +**Activity.** How strongly a transcription factor or a pathway acts in a sample or a +contrast, read from its target genes: high where the genes it raises are high and +those it lowers are low, as a network such as CollecTRI or PROGENy weights them. +[Gene-set tests](../statistics/gene_set_tests.md#activities-of-regulators-and-pathways) + **Adjusted p-value.** A p-value corrected for the number of tests, here usually by the method of Benjamini and Hochberg, which controls the false discovery rate. [Multiple testing](../statistics/multiple_testing.md) diff --git a/docs/source/reference/related_software.md b/docs/source/reference/related_software.md index 5daea07..e27398e 100644 --- a/docs/source/reference/related_software.md +++ b/docs/source/reference/related_software.md @@ -27,6 +27,7 @@ current releases below. | [lme4](https://cran.r-project.org/package=lme4) and [lmerTest](https://cran.r-project.org/package=lmerTest) | Linear mixed models; Satterthwaite's and Kenward-Roger's tests [@bates2015; @kuznetsova2017] | GPL (>= 2) | CRAN 2.1-0 and 3.2-1 | | [fgsea](https://bioconductor.org/packages/fgsea) | Fast preranked gene set enrichment analysis, with p-values by multilevel splitting [@korotkevich2021]; CytePool's `gsea` adapts its algorithms, as `THIRD_PARTY_NOTICES.md` records | MIT | Bioconductor 1.38.0 | | [GSVA](https://bioconductor.org/packages/GSVA) | Scores of gene sets in every sample: GSVA, ssGSEA, combined z-scores and PLAGE [@haenzelmann2013] | Artistic-2.0 (GPL (>= 2) up to version 2.0) | Bioconductor 2.6.14 | +| [progeny](https://bioconductor.org/packages/progeny) | PROGENy's weights of the genes that respond to 14 pathways, and its pathway scores [@schubert2018]; CytePool reads its weights | Apache-2.0 | Bioconductor 1.34.0 | | [PyDESeq2](https://github.com/scverse/PyDESeq2) | A Python implementation of DESeq2 [@muzellec2023]; CytePool also adapts some of its constants, as `THIRD_PARTY_NOTICES.md` records | MIT | PyPI 0.5.4 | Because limma, edgeR, DESeq2, variancePartition, speckle, metafor, lme4, lmerTest and @@ -43,7 +44,7 @@ cube files are Zarr v3 stores. | Tool | What it does | Licence | Current release | | --- | --- | --- | --- | | [scanpy](https://github.com/scverse/scanpy) | Single-cell analysis; `get.aggregate` sums cells into pseudobulk profiles [@wolf2018] | BSD 3-Clause | PyPI 1.12.4 | -| [decoupler](https://github.com/scverse/decoupler) | Activity scores of pathways and regulators; `pp.pseudobulk` [@badiaimompel2022] | BSD 3-Clause | PyPI 2.3.0 | +| [decoupler](https://github.com/scverse/decoupler) | Activity scores of pathways and regulators; `pp.pseudobulk` [@badiaimompel2022]; CytePool's `ulm`, `mlm` and `weighted_aggregate` compute its activities and are validated against it, as `THIRD_PARTY_NOTICES.md` records | BSD 3-Clause | PyPI 2.3.0 | | [LIANA+](https://github.com/saezlab/liana-py) | Cell-cell communication; `adata_to_views` makes a MuData of pseudobulk views [@dimitrov2024] | BSD 3-Clause | PyPI 1.10.0 | | [anndata](https://github.com/scverse/anndata) and [mudata](https://github.com/scverse/mudata) | Annotated data matrices and multimodal containers [@virshup2024; @bredikhin2022] | BSD 3-Clause | PyPI 0.13.4 and 0.4.1 | | [zarr-python](https://github.com/zarr-developers/zarr-python) | The Python implementation of Zarr; opens cube files | MIT | PyPI 3.4.1 | diff --git a/docs/source/references.bib b/docs/source/references.bib index 5e43e00..2188c74 100644 --- a/docs/source/references.bib +++ b/docs/source/references.bib @@ -866,6 +866,17 @@ @article{holland2020 doi = {10.1186/s13059-020-1949-z}, } +@article{holland2020b, + author = {Holland, Christian H. and Szalai, Bence and Saez-Rodriguez, Julio}, + title = {Transfer of regulatory knowledge from human to mouse for functional genomics analysis}, + journal = {Biochimica et Biophysica Acta (BBA) - Gene Regulatory Mechanisms}, + year = {2020}, + volume = {1863}, + number = {6}, + pages = {194431}, + doi = {10.1016/j.bbagrm.2019.194431}, +} + @article{holm1979, author = {Holm, S.}, title = {A simple sequentially rejective multiple test procedure}, @@ -1269,6 +1280,17 @@ @article{mootha2003 doi = {10.1038/ng1180}, } +@article{muellerdott2023, + author = {Müller-Dott, Sophia and Tsirvouli, Eirini and Vazquez, Miguel and Ramirez Flores, Ricardo O. and Badia-i-Mompel, Pau and Fallegger, Robin and Türei, Dénes and Lægreid, Astrid and Saez-Rodriguez, Julio}, + title = {Expanding the coverage of regulons from high-confidence prior knowledge for accurate estimation of transcription factor activities}, + journal = {Nucleic Acids Research}, + year = {2023}, + volume = {51}, + number = {20}, + pages = {10934--10949}, + doi = {10.1093/nar/gkad841}, +} + @article{murphy2022, author = {Murphy, Alan E. and Skene, Nathan G.}, title = {A balanced measure shows superior performance of pseudobulk methods in single-cell RNA-sequencing analysis}, @@ -1566,6 +1588,17 @@ @article{satterthwaite1946 doi = {10.2307/3002019}, } +@article{schubert2018, + author = {Schubert, Michael and Klinger, Bertram and Klünemann, Martina and Sieber, Anja and Uhlitz, Florian and Sauer, Sascha and Garnett, Mathew J. and Blüthgen, Nils and Saez-Rodriguez, Julio}, + title = {Perturbation-response genes reveal signaling footprints in cancer gene expression}, + journal = {Nature Communications}, + year = {2018}, + volume = {9}, + number = {1}, + pages = {20}, + doi = {10.1038/s41467-017-02391-6}, +} + @article{scott1979, author = {Scott, David W.}, title = {On optimal and data-based histograms}, @@ -1814,6 +1847,17 @@ @article{tomfohr2005 doi = {10.1186/1471-2105-6-225}, } +@article{tuerei2021, + author = {Türei, Dénes and Valdeolivas, Alberto and Gul, Lejla and Palacio-Escat, Nicolàs and Klein, Michal and Ivanova, Olga and Ölbei, Márton and Gábor, Attila and Theis, Fabian and Módos, Dezső and Korcsmáros, Tamás and Saez-Rodriguez, Julio}, + title = {Integrated intra- and intercellular signaling knowledge for multicellular omics analysis}, + journal = {Molecular Systems Biology}, + year = {2021}, + volume = {17}, + number = {3}, + pages = {e9923}, + doi = {10.15252/msb.20209923}, +} + @article{urbut2019, author = {Urbut, Sarah M. and Wang, Gao and Carbonetto, Peter and Stephens, Matthew}, title = {Flexible statistical methods for estimating and testing effects in genomic studies with multiple conditions}, @@ -1865,6 +1909,17 @@ @article{virshup2024 doi = {10.21105/joss.04371}, } +@article{welford1962, + author = {Welford, B. P.}, + title = {Note on a method for calculating corrected sums of squares and products}, + journal = {Technometrics}, + year = {1962}, + volume = {4}, + number = {3}, + pages = {419--420}, + doi = {10.1080/00401706.1962.10490022}, +} + @article{wichura1988, author = {Wichura, Michael J.}, title = {Algorithm AS 241: the percentage points of the normal distribution}, diff --git a/docs/source/statistics/choosing_a_method.md b/docs/source/statistics/choosing_a_method.md index 9856b0f..de34709 100644 --- a/docs/source/statistics/choosing_a_method.md +++ b/docs/source/statistics/choosing_a_method.md @@ -68,6 +68,7 @@ edgeR's and DESeq2's methods run on one matrix at a time, not on every group of | Do the genes of a pathway respond together? | {api}`camera`, {api}`fry`, {api}`roast`, {api}`mroast`, {api}`romer` | [Gene-set tests](gene_set_tests.md) | | Do the genes of a pathway gather at the top or the bottom of a ranking of all genes? | {api}`gsea`, with {api}`plot_enrichment` and {api}`plot_gsea_table` | [Gene-set tests](gene_set_tests.md) | | Are pathways over-represented among the genes that change? | {api}`over_representation`, with sets from {api}`read_gmt` | [Gene-set tests](gene_set_tests.md) | +| Which transcription factors and pathways are active, in each sample or contrast? | {api}`ulm`, {api}`mlm`, {api}`weighted_aggregate`, with networks from {api}`collectri` and {api}`progeny`, and {api}`plot_activities` and {api}`plot_top_activities` | [Gene-set tests](gene_set_tests.md) | | How active is a pathway in each sample, and does that differ between conditions? | {api}`gsva`, {api}`ssgsea`, {api}`combined_z_scores`, {api}`plage`, with {api}`plot_pathway_scores`; their scores in {api}`fit_linear_model` | [Gene-set tests](gene_set_tests.md) | | Does the share of a cell type change? | {api}`propeller` | [Composition tests](composition.md) | | Do studies agree, and what is their common effect? | {api}`differential_expression_meta_analysis`, {api}`meta_analysis` | [Meta-analysis](meta_analysis.md) | diff --git a/docs/source/statistics/gene_set_tests.md b/docs/source/statistics/gene_set_tests.md index c686850..c90906b 100644 --- a/docs/source/statistics/gene_set_tests.md +++ b/docs/source/statistics/gene_set_tests.md @@ -1,5 +1,5 @@ --- -description: Competitive and self-contained gene-set tests on a linear model, as limma's camera, fry, roast, mroast and romer compute them; gene set enrichment analysis of ranked genes, as fgsea computes it; over-representation analysis of the genes called significant, as limma's kegga computes it; scores of gene sets in every sample by GSVA, ssGSEA, combined z-scores and PLAGE, as the GSVA package computes them; gene sets read from GMT files; and how to choose between the tests. +description: Competitive and self-contained gene-set tests on a linear model, as limma's camera, fry, roast, mroast and romer compute them; gene set enrichment analysis of ranked genes, as fgsea computes it; over-representation analysis of the genes called significant, as limma's kegga computes it; scores of gene sets in every sample by GSVA, ssGSEA, combined z-scores and PLAGE, as the GSVA package computes them; activities of transcription factors and pathways from the networks CollecTRI and PROGENy, as decoupler computes them; gene sets read from GMT files; and how to choose between the tests. --- # Gene-set tests @@ -249,6 +249,86 @@ genes with the same low counts in different samples, can be ordered by the last R's probabilities: their ranks, and the scores of their sets, can then differ slightly from GSVA's. +## Activities of regulators and pathways + +A transcription factor raises some of its target genes and lowers others, and a +signalling pathway leaves a footprint of genes that respond to it. A network gives each +such source signed weights on its targets: 1 for a target that a transcription factor +activates and -1 for one it represses in CollecTRI's regulons [@muellerdott2023], and +in PROGENy the z-scores with which genes responded to the perturbation of each of 14 +pathways in hundreds of experiments [@schubert2018; @holland2020b]. A source is active +in a profile, the expression of a sample or the statistics of a contrast, when its +targets are high where their weights are positive and low where they are negative. +{api}`ulm`, {api}`mlm` and {api}`weighted_aggregate` score every source in every +profile as decoupler does [@badiaimompel2022], from a samples x genes matrix, a cell +type of a cube (its log-CPM, as for the pathway scores) or a differential-expression +result, one profile per group and contrast, by default of its moderated t-statistics. +They return an {api}`Activities`, profiles x sources, which +{api}`fit_linear_model` takes with a design built from its profiles, as it takes +pathway scores; {api}`Activities.table` lists the scores one per row. + +**ULM**, the univariate linear model, fits for each source a line through the values +of the profile's $n$ genes against the source's weights $x_g$, zero for the genes that +are not its targets, + +$$ +y_g = \beta_0 + \beta_1 x_g + \varepsilon_g, +$$ + +and the score is the t-statistic of the slope, +$t = \hat\beta_1 / \mathrm{se}(\hat\beta_1) = r \sqrt{(n - 2) / (1 - r^2)}$, with $r$ +the correlation of $y$ and $x$, tested on $n - 2$ degrees of freedom. Each source is +tested against all the other genes, those of other sources included. **MLM**, the +multivariate linear model, fits all the sources at once, with an intercept, +$y_g = \beta_0 + \sum_s \beta_s x_{gs} + \varepsilon_g$, so that each score, the +t-statistic of $\beta_s$ on $n - m - 1$ degrees of freedom for $m$ sources, measures a +source beyond what the others explain: where two regulators share targets, ULM +credits both with their response and MLM splits it between them. MLM needs more genes +than sources, and sources whose weights are not linear combinations of the weights of +others; it refuses those it cannot tell apart, by name. The **weighted aggregate** is +the weighted mean of the values of a source's targets, $\sum_g w_g y_g / \sum_g |w_g|$, +or their weighted sum, which is PROGENy's original score [@schubert2018]. With random +permutations of the genes (1,000 by default, as decoupler draws them), the score is +normalized by those of the permutations, $(\mathrm{ES} - \text{mean}) / \text{sd}$, and +its p-value doubles the smaller of the two one-sided permutation p-values +[@phipson2010]. + +Targets are matched to the genes' ids exactly, and genes whose values are zero in +every profile are left out, as decoupler leaves them out. A source with fewer than +`min_targets` targets among a profile's genes (5, decoupler's `tmin`) has no score +there; NaN marks a missing value, so that each profile of a differential-expression +result uses the genes its group tested. The adjusted p-values are Benjamini and +Hochberg's over the sources of each profile [@benjamini1995]. {api}`plot_activities` +draws the activities as a heatmap, and {api}`plot_top_activities` the sources with the +largest scores of one profile as bars, as decoupler's `pl.barplot` draws them. + +The networks are a {api}`Network`: each source with its targets and weights, and the +network's name, version, licence and citation. {api}`collectri` and {api}`progeny` +download their files on first use from where they were published, check them against +pinned SHA-256 digests, and keep them in a cache, so that a machine without a +connection can use a file copied there. CollecTRI gathers twelve resources, each with +its own terms, which OmniPath records [@tuerei2021]: `licence="commercial"`, the +default, keeps the 39,896 interactions that a resource listed for commercial use +supports, leaving out the 3,279 that GEREDB alone supports, which OmniPath lists for +academic use; `licence="academic"` keeps all 43,175 (the +[Datasets](../reference/datasets.md#networks) page gives the terms). PROGENy's weights +are those of the R package progeny, under the Apache License 2.0, for human and mouse; +{api}`progeny` keeps the genes with a p-value below 0.05, as decoupler does, or the +`top` genes of each pathway, as progeny's `getModel` does, 100 by default there. Its +genes are named by the symbols of its release; decoupler reads PROGENy through +OmniPath, which renames some of them to their current symbols. Any network given as a +table of `source`, `target` and `weight`, or gene sets with weights of 1, works the +same way, and {api}`Network.select` keeps some of its sources. + +The scores, estimates and p-values agree with decoupler's to rounding, and PROGENy's +weights with progeny's. CytePool differs on purpose in three places: it adjusts MLM's +p-values, which decoupler reports unadjusted as its adjusted ones; its permutation +p-values double the smaller tail, where decoupler's send a score near zero to $2/n$ for +$n$ permutations; and it gives a profile whose values are all zero NaN scores, where +decoupler leaves it out. decoupler's VIPER is not offered, +because the licence of its reference implementation allows only non-profit academic +use. + ## Choices - **Competitive or self-contained.** Rank a collection with camera; ask about one set @@ -273,3 +353,10 @@ from GSVA's. the genes' own statistics, where scores first reduce each set to one number per sample. GSVA and ssGSEA compare a set's genes with the other genes of each sample; combined z-scores and PLAGE follow each set across samples. +- **Activities or scores.** Activities use the signs and sizes of a network's weights, + so that a regulator's repressed targets count against it; pathway scores treat every + gene of a set alike. ULM is decoupler's default for regulators and pathways alike; + MLM separates sources whose targets overlap, provided the network gives it fewer + sources than genes and no two sources it cannot tell apart. For a contrast, score its + statistics; to compare samples, or to test activities with a design, score the + samples. diff --git a/docs/source/validation.md b/docs/source/validation.md index 7ed4924..1d3b245 100644 --- a/docs/source/validation.md +++ b/docs/source/validation.md @@ -17,9 +17,9 @@ exits with an error when any check falls outside its tolerance. This page summarises the tables the scripts printed when they were run against the package in this revision of the repository: on **2026-10-26** for `plotting.py`, `variance_partitioning.py`, `gene_set_files_and_ora.py`, `kenward_roger_and_f_tests.py`, -`gsea.py`, `cube_collection.py`, `pathway_scores.py` and -`combined_p_values_and_replication.py`, and for `dream.py`, which printed the same -table again after changes to the code it checks; on +`gsea.py`, `cube_collection.py`, `pathway_scores.py`, +`combined_p_values_and_replication.py` and `activity.py`, and for `dream.py`, which +printed the same table again after changes to the code it checks; on **2026-10-25** for the six scripts on cubes and their files (`cube.py`, `pool_cells.py`, `cube_files.py`, `cube_reads.py`, `cube_fingerprints.py` and `anndata_conversion.py`), for `linear_models.py`, @@ -60,6 +60,7 @@ covers and the largest difference among them. Run a script to see every check. | [`gene_set_files_and_ora.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gene_set_files_and_ora.py) | Gene-set files and over-representation | R 4.4.3, limma 3.62.1, decoupler 2.2.0 | 34 | 29 pass, 5 deliberate differences | 4.7 s | 297 MiB | | [`gsea.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/gsea.py) | Gene set enrichment analysis | R 4.4.3, fgsea 1.32.2 | 24 | 23 pass, 1 deliberate difference | 7 min 3 s | 323 MiB | | [`pathway_scores.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/pathway_scores.py) | Pathway scores | R 4.4.3, GSVA 2.0.0 | 24 | 19 pass, 3 deliberate differences, 2 information | 10.7 s | 949 MiB | +| [`activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py) | Pathway and regulator activity | decoupler 2.2.0, progeny 1.28.0, scipy 1.18.1 | 89 | 82 pass, 5 deliberate differences, 2 information | 2 min 39 s | 987 MiB | | [`multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py) | Multiple testing | R 4.4.3 | 15 | 15 pass | 2 min 56 s | 202 MiB | | [`differential_expression.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression.py) | Differential expression | limma 3.62.1, edgeR 4.4.0 | 32 | 28 pass, 4 deliberate differences | 1.8 s | 189 MiB | | [`differential_expression_options.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/differential_expression_options.py) | Options for pseudobulk differential expression | limma 3.62.1, edgeR 4.4.0 | 66 | 66 pass | 5.0 s | 182 MiB | @@ -73,7 +74,7 @@ covers and the largest difference among them. Run a script to see every check. | [`plotting.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/plotting.py) | Figures | limma 3.62.1, edgeR 4.4.0, DESeq2 1.46.0, metafor 5.0.1, speckle 1.6.0 | 26 | 23 pass, 2 information, 1 deliberate difference | 25.4 s | 810 MiB | | [`distributions.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/distributions.py) | Distributions and special functions | mpmath 1.3.0 | 46 | 46 pass | 6 min 39 s | 221 MiB | | [`smoothing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/smoothing.py) | LOWESS and spline bases | R 4.4.3 | 90 | 87 pass, 1 information, 2 deliberate differences | 1.5 s | 175 MiB | -| **All** | | | **2,267** | **1,954 pass, none failed; 174 information, 137 deliberate differences, 2 controls** | | | +| **All** | | | **2,356** | **2,036 pass, none failed; 176 information, 142 deliberate differences, 2 controls** | | | Run time is the wall time of the whole script and peak memory the largest resident set of any of its processes, reference programs included, both measured with GNU @@ -1344,6 +1345,75 @@ Deliberate differences: the enrichment scores in ssGSEA, likely due to NA values in the input expression data"); CytePool gives that set NaN and normalizes the other scores by their range. +## Pathway and regulator activity + +[`validation/activity.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/activity.py) +checks the activities of regulators and pathways, their bar plot and the networks +against decoupler 2.2.0 and progeny 1.28.0. CytePool's loaders download the networks +into its cache. CollecTRI's file and progeny's package are compared with the MD5 +digests and sizes their sources publish (Zenodo's record, Bioconductor's `PACKAGES` +and the server's `Content-Length`); `collectri(licence="academic")` with decoupler's +`op.collectri`; the commercial level with the licence purposes that OmniPath's +resources service lists, applied to the file by the script; and the weights and +p-values of `progeny` with progeny's `model_human_full` and `model_mouse_full`, and its +`top` with `getModel` (100 human genes; 500 mouse genes, with ties). The inputs of the +activities are simulated from fixed seeds: log-expression values of 6 samples over +CollecTRI's 6,693 targets and 2,000 other genes, the targets of 6 regulators shifted in +the last 3 samples, with 3 genes zero in every sample; a profile of t-statistics; the +values with 3% missing in two samples, which decoupler, refusing missing values, +scores one profile at a time over its observed genes; 6 samples over the targets of +PROGENy's top 500 genes and 1,000 others; a cube of one cell type, scored as its +log-CPM; and limma_voom's moderated t-statistics in two cell types that test different +genes. `ulm`, `mlm` and `weighted_aggregate` run as decoupler's `mt.ulm`, `mt.mlm` and +`mt.waggr` with `tmin` 5, or 3 for a network of weights of one sign. Reference: +decoupler 2.2.0 with scipy 1.18.1, and progeny 1.28.0 on R 4.4.3. + +| Check family | Reference | Largest difference | Tolerance | Outcome | +| --- | --- | --- | --- | --- | +| ULM's t-statistics and slopes, in 10 cases | `mt.ulm` | 2.3e-13 | 1e-10 | 20 pass | +| ULM's p-values, and their Benjamini-Hochberg adjustment, in 10 cases | `mt.ulm`; scipy's `false_discovery_control` of its p-values | 4.4e-11 | 1e-10 | 20 pass | +| ULM's adjusted p-values, in single precision | `mt.ulm` | 0 | 1.2e-7 | 10 pass | +| MLM's t-statistics and coefficients, in 6 cases | `mt.mlm` | 3.2e-14 | 1e-10 | 12 pass | +| MLM's p-values | scipy's `t.sf` of `mt.mlm`'s t-statistics | 3.3e-12 | 1e-10 | 6 pass | +| Weighted means and sums, and the normalized scores given decoupler's permutations (100 and 1,000) | `mt.waggr` | 1.1e-14 | 1e-10 | 4 pass | +| Weighted sums of PROGENy's top 100 genes | progeny's `progeny(scale = FALSE)` | 6.5e-16 | 1e-10 | 1 pass | +| `plot_top_activities`: the 15 bars of a sample, their sources and lengths | `pl.barplot` | 4.8e-14 | 1e-10 | 1 pass | +| CollecTRI at the academic level; at the commercial level; PROGENy's weights and p-values, human and mouse; its top 100 human and 500 mouse genes | `op.collectri`, OmniPath's resources, progeny's models and `getModel` | 0 | 0 | 6 pass | +| The files downloaded: sizes and MD5 digests | Zenodo, Bioconductor | 0 | 0 | 2 pass | +| decoupler's `op.collectri` at its levels; PROGENy through OmniPath | `op.collectri`, `op.progeny` | n/a | n/a | 2 information | +| The weighted aggregate's p-values; a sample of zeros; MLM with two sources alike; CollecTRI's interactions without a reference | `mt.waggr`, `mt.ulm`, `mt.mlm`, `op.collectri` | 0.97 | n/a | 5 deliberate differences | + +Scores and estimates are compared relative to the largest absolute value of a case, +p-values relative to each value, and NaN must stand where the reference has none; the +networks and the bars must be identical. decoupler keeps its adjusted p-values in +single precision, so CytePool's are rounded to it first. decoupler computes MLM's +p-values as one minus the distribution function, which loses their digits below about +1e-12, so CytePool's are compared with scipy's upper tail at decoupler's t-statistics. +The normalized scores are compared given decoupler's permutations: the genes in the +order in which decoupler shuffles them, and its seed, so that both draw the same +permutations. decoupler 2.2.0 accepts a licence for CollecTRI but returns the whole +file at every level. Its `op.progeny` reads PROGENy from OmniPath's annotations, which +name genes by their current symbols, leave out genes OmniPath does not map to a +protein, and merge some genes: of its 62,421 edges with a p-value below 0.05, where the +progeny package has 69,827, 59,617 are named alike, and 59,599 of them have the same +weight and p-value. + +Deliberate differences: + +- The weighted aggregate's p-values (largest difference 0.97 with 100 permutations, + 2.0e-3 with 1,000): decoupler counts the permutations whose absolute score exceeds + the absolute observed score and then folds that share as it would fold a one-sided + one, so that a score near zero gets a p-value near 2 / n for n permutations; CytePool + doubles the smaller of the two one-sided p-values (1 + b) / (1 + n). +- A sample whose values are all zero: decoupler leaves it out of its result; CytePool + gives it NaN scores. The other samples agree exactly. +- MLM with two sources of the same targets and weights: decoupler inverts a singular + matrix and gives NaN t-statistics for both (12 in 6 samples); CytePool refuses, + naming the source it cannot tell apart from the other. +- CollecTRI's 185 interactions whose field of references is empty: decoupler's loader + drops them, through pandas' `dropna`; CytePool keeps them, as the file holds them and + the paper counts them (43,175 interactions). + ## Multiple testing [`validation/multiple_testing.py`](https://github.com/NygenAnalytics/CytePool/blob/master/validation/multiple_testing.py) @@ -2321,6 +2391,8 @@ python validation/gene_set_files_and_ora.py RSCRIPT=$REF/r-gsea/bin/Rscript python validation/gsea.py RSCRIPT=$REF/r-gsea/bin/Rscript python validation/pathway_scores.py RSCRIPT=$REF/r-gsea/bin/Rscript python validation/pathway_scores.py --benchmark +# Downloads CollecTRI and progeny into ~/.cache/cytepool/networks on its first run. +RSCRIPT=$REF/r-progeny/bin/Rscript python validation/activity.py python validation/voom.py python validation/sample_quality_weights.py python validation/negative_binomial_fits.py @@ -2429,6 +2501,14 @@ conda create -p $REF/r-gsea -c conda-forge -c bioconda r-base=4.4.3 \ r-data.table=1.18.6.1 r-jsonlite=2.0.0 ``` +progeny 1.28.0 (Bioconductor 3.20), which `activity.py` runs for PROGENy's weights, has +one too, with R 4.4.3 and jsonlite 2.0.0: + +```bash +conda create -p $REF/r-progeny -c conda-forge -c bioconda r-base=4.4.3 \ + bioconductor-progeny=1.28.0 r-jsonlite=2.0.0 +``` + The Python environments use Python 3.13.12. scanpy has its own environment, and `cube_files.py` ran CytePool on Python 3.14.4 for the zstd cases: @@ -2457,6 +2537,8 @@ uv pip install --python $REF/python-liana/bin/python liana==1.10.0 decoupler==2. ``` The reference packages run only as separate programs that produce outputs to compare -against. Their source code is not read or copied: CytePool implements the methods from -their papers and documentation, and conventions that the documentation leaves open -were found by running the packages on chosen inputs. +against. CytePool implements the methods from their papers and documentation, and +conventions that the documentation leaves open were found by running the packages on +chosen inputs; the source of a package under a copyleft licence is never read, and +what CytePool adapts from permissively licensed ones, fgsea and decoupler, +`THIRD_PARTY_NOTICES.md` records. diff --git a/tests/data/activity/collectri_rows.csv b/tests/data/activity/collectri_rows.csv new file mode 100644 index 0000000..0edc26d --- /dev/null +++ b/tests/data/activity/collectri_rows.csv @@ -0,0 +1,81 @@ +source,target,weight,resources,references,sign_decision +AP1,GENE0,1.0,CollecTRI;TRRUST_CollecTRI;DoRothEA-A_CollecTRI;ExTRI_CollecTRI,,PMID +NFKB,GENE1,1.0,CollecTRI;ExTRI_CollecTRI;HTRI_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:25227886,default activation +TF0,GENE2,1.0,CollecTRI;TRRUST_CollecTRI;ExTRI_CollecTRI,CollecTRI:29648508;CollecTRI:2274839,regulon +TF1,GENE3,-1.0,CollecTRI;NTNU.Curated_CollecTRI,,PMID +TF2,GENE4,1.0,CollecTRI;SIGNOR_CollecTRI;DoRothEA-A_CollecTRI,CollecTRI:17794492,default activation +TF3,GENE5,1.0,CollecTRI;NTNU.Curated_CollecTRI;TRRUST_CollecTRI,CollecTRI:30512912;CollecTRI:38294422,regulon +TF4,GENE6,1.0,CollecTRI;TRRUST_CollecTRI;SIGNOR_CollecTRI;DoRothEA-A_CollecTRI,,PMID +TF5,GENE7,1.0,CollecTRI;ExTRI_CollecTRI;DoRothEA-A_CollecTRI;TRRUST_CollecTRI,CollecTRI:37811468,default activation +TF6,GENE8,1.0,CollecTRI;ExTRI_CollecTRI,CollecTRI:13245454;CollecTRI:2680991,regulon +TF7,GENE9,1.0,CollecTRI;GEREDB_CollecTRI,,PMID +AP1,GENE10,-1.0,CollecTRI;TRRUST_CollecTRI;ExTRI_CollecTRI;HTRI_CollecTRI,CollecTRI:15595509,default activation +NFKB,GENE11,1.0,CollecTRI;ExTRI_CollecTRI,CollecTRI:24735529;CollecTRI:26759502,regulon +TF0,GENE12,1.0,CollecTRI;HTRI_CollecTRI;DoRothEA-A_CollecTRI,,PMID +TF1,GENE13,1.0,CollecTRI;TRRUST_CollecTRI;SIGNOR_CollecTRI,CollecTRI:20005059,default activation +TF2,GENE14,1.0,CollecTRI;SIGNOR_CollecTRI;HTRI_CollecTRI;TRRUST_CollecTRI,CollecTRI:33365588;CollecTRI:33864342,regulon +TF3,GENE15,1.0,CollecTRI;DoRothEA-A_CollecTRI;ExTRI_CollecTRI,,PMID +TF4,GENE16,1.0,CollecTRI;NTNU.Curated_CollecTRI;SIGNOR_CollecTRI;HTRI_CollecTRI,CollecTRI:35260727,default activation +TF5,GENE17,-1.0,CollecTRI;ExTRI_CollecTRI;SIGNOR_CollecTRI,CollecTRI:26599339;CollecTRI:16363165,regulon +TF6,GENE18,1.0,CollecTRI;ExTRI_CollecTRI;DoRothEA-A_CollecTRI;TRRUST_CollecTRI,,PMID +TF7,GENE19,1.0,CollecTRI;GEREDB_CollecTRI,CollecTRI:21441119,default activation +AP1,GENE20,1.0,CollecTRI;NTNU.Curated_CollecTRI;TRRUST_CollecTRI,CollecTRI:24903470;CollecTRI:37331386,regulon +NFKB,GENE21,1.0,CollecTRI;DoRothEA-A_CollecTRI,,PMID +TF0,GENE22,1.0,CollecTRI;ExTRI_CollecTRI,CollecTRI:30127555,default activation +TF1,GENE23,1.0,CollecTRI;DoRothEA-A_CollecTRI,CollecTRI:13903247;CollecTRI:23112137,regulon +TF2,GENE24,-1.0,CollecTRI;TRRUST_CollecTRI;NTNU.Curated_CollecTRI;SIGNOR_CollecTRI,,PMID +TF3,GENE25,1.0,CollecTRI;ExTRI_CollecTRI;HTRI_CollecTRI,CollecTRI:38788725,default activation +TF4,GENE26,1.0,CollecTRI;DoRothEA-A_CollecTRI;SIGNOR_CollecTRI,CollecTRI:38851791;CollecTRI:24591103,regulon +TF5,GENE27,1.0,CollecTRI;HTRI_CollecTRI;SIGNOR_CollecTRI;TRRUST_CollecTRI,,PMID +TF6,GENE28,1.0,CollecTRI;HTRI_CollecTRI;ExTRI_CollecTRI,CollecTRI:8547575,default activation +TF7,GENE29,1.0,CollecTRI;GEREDB_CollecTRI,CollecTRI:3598493;CollecTRI:12573552,regulon +AP1,GENE30,1.0,CollecTRI;NTNU.Curated_CollecTRI;TRRUST_CollecTRI,,PMID +NFKB,GENE31,-1.0,CollecTRI;DoRothEA-A_CollecTRI;HTRI_CollecTRI,CollecTRI:2067820,default activation +TF0,GENE32,1.0,CollecTRI;DoRothEA-A_CollecTRI,CollecTRI:22254324;CollecTRI:37821372,regulon +TF1,GENE33,1.0,CollecTRI;SIGNOR_CollecTRI;ExTRI_CollecTRI;TRRUST_CollecTRI,,PMID +TF2,GENE34,1.0,CollecTRI;SIGNOR_CollecTRI;ExTRI_CollecTRI,CollecTRI:27111797,default activation +TF3,GENE35,1.0,CollecTRI;SIGNOR_CollecTRI;ExTRI_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:27593714;CollecTRI:8727838,regulon +TF4,GENE36,1.0,CollecTRI;NTNU.Curated_CollecTRI,,PMID +TF5,GENE37,1.0,CollecTRI;HTRI_CollecTRI;ExTRI_CollecTRI,CollecTRI:38763501,default activation +TF6,GENE38,-1.0,CollecTRI;DoRothEA-A_CollecTRI;SIGNOR_CollecTRI,CollecTRI:16352926;CollecTRI:27015928,regulon +TF7,GENE39,1.0,CollecTRI;GEREDB_CollecTRI,,PMID +AP1,GENE40,1.0,CollecTRI;TRRUST_CollecTRI;ExTRI_CollecTRI,CollecTRI:29909043,default activation +NFKB,GENE41,1.0,CollecTRI;NTNU.Curated_CollecTRI;DoRothEA-A_CollecTRI;TRRUST_CollecTRI,CollecTRI:4206728;CollecTRI:16325847,regulon +TF0,GENE42,1.0,CollecTRI;NTNU.Curated_CollecTRI,,PMID +TF1,GENE43,1.0,CollecTRI;NTNU.Curated_CollecTRI;HTRI_CollecTRI;TRRUST_CollecTRI,CollecTRI:12684997,default activation +TF2,GENE44,1.0,CollecTRI;ExTRI_CollecTRI;HTRI_CollecTRI,CollecTRI:22953960;CollecTRI:29515989,regulon +TF3,GENE45,-1.0,CollecTRI;ExTRI_CollecTRI;DoRothEA-A_CollecTRI;HTRI_CollecTRI,,PMID +TF4,GENE46,1.0,CollecTRI;ExTRI_CollecTRI;DoRothEA-A_CollecTRI;HTRI_CollecTRI,CollecTRI:10896452,default activation +TF5,GENE47,1.0,CollecTRI;HTRI_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:16967975;CollecTRI:11207598,regulon +TF6,GENE48,1.0,CollecTRI;HTRI_CollecTRI;NTNU.Curated_CollecTRI,,PMID +TF7,GENE49,1.0,CollecTRI;GEREDB_CollecTRI,CollecTRI:28553852,default activation +AP1,GENE50,1.0,CollecTRI;SIGNOR_CollecTRI,CollecTRI:18254288;CollecTRI:37318562,regulon +NFKB,GENE51,1.0,CollecTRI;TRRUST_CollecTRI;DoRothEA-A_CollecTRI,,PMID +TF0,GENE52,-1.0,CollecTRI;ExTRI_CollecTRI,CollecTRI:7589368,default activation +TF1,GENE53,1.0,CollecTRI;SIGNOR_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:35423598;CollecTRI:26948520,regulon +TF2,GENE54,1.0,CollecTRI;TRRUST_CollecTRI,,PMID +TF3,GENE55,1.0,CollecTRI;TRRUST_CollecTRI,CollecTRI:34032851,default activation +TF4,GENE56,1.0,CollecTRI;DoRothEA-A_CollecTRI;HTRI_CollecTRI;TRRUST_CollecTRI,CollecTRI:11915872;CollecTRI:15270323,regulon +TF5,GENE57,1.0,CollecTRI;TRRUST_CollecTRI;HTRI_CollecTRI,,PMID +TF6,GENE58,1.0,CollecTRI;SIGNOR_CollecTRI;HTRI_CollecTRI,CollecTRI:5914735,default activation +TF7,GENE59,-1.0,CollecTRI;GEREDB_CollecTRI,CollecTRI:37140880;CollecTRI:39992318,regulon +AP1,GENE60,1.0,CollecTRI;ExTRI_CollecTRI,,PMID +NFKB,GENE61,1.0,CollecTRI;NTNU.Curated_CollecTRI;ExTRI_CollecTRI,CollecTRI:10142986,default activation +TF0,GENE62,1.0,CollecTRI;HTRI_CollecTRI,CollecTRI:23943433;CollecTRI:2012094,regulon +TF1,GENE63,1.0,CollecTRI;HTRI_CollecTRI,,PMID +TF2,GENE64,1.0,CollecTRI;TRRUST_CollecTRI;SIGNOR_CollecTRI;DoRothEA-A_CollecTRI,CollecTRI:1349692,default activation +TF3,GENE65,1.0,CollecTRI;ExTRI_CollecTRI,CollecTRI:5576274;CollecTRI:17212375,regulon +TF4,GENE66,-1.0,CollecTRI;TRRUST_CollecTRI;DoRothEA-A_CollecTRI;SIGNOR_CollecTRI,,PMID +TF5,GENE67,1.0,CollecTRI;TRRUST_CollecTRI;ExTRI_CollecTRI;HTRI_CollecTRI,CollecTRI:23085573,default activation +TF6,GENE68,1.0,CollecTRI;HTRI_CollecTRI,CollecTRI:15503357;CollecTRI:18507992,regulon +TF7,GENE69,1.0,CollecTRI;GEREDB_CollecTRI,,PMID +AP1,GENE70,1.0,CollecTRI;ExTRI_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:18304215,default activation +NFKB,GENE71,1.0,CollecTRI;HTRI_CollecTRI,CollecTRI:14477295;CollecTRI:23031795,regulon +TF0,GENE72,1.0,CollecTRI;HTRI_CollecTRI,,PMID +TF1,GENE73,-1.0,CollecTRI;NTNU.Curated_CollecTRI;ExTRI_CollecTRI,CollecTRI:10207568,default activation +TF2,GENE74,1.0,CollecTRI;TRRUST_CollecTRI;SIGNOR_CollecTRI;DoRothEA-A_CollecTRI,CollecTRI:26885978;CollecTRI:33629567,regulon +TF3,GENE75,1.0,CollecTRI;NTNU.Curated_CollecTRI;SIGNOR_CollecTRI;DoRothEA-A_CollecTRI,,PMID +TF4,GENE76,1.0,CollecTRI;SIGNOR_CollecTRI;NTNU.Curated_CollecTRI,CollecTRI:13632575,default activation +TF5,GENE77,1.0,CollecTRI;SIGNOR_CollecTRI;TRRUST_CollecTRI,CollecTRI:14663809;CollecTRI:31650520,regulon +TF6,GENE78,1.0,CollecTRI;ExTRI_CollecTRI;NTNU.Curated_CollecTRI,,PMID +TF7,GENE79,1.0,CollecTRI;GEREDB_CollecTRI,CollecTRI:19563781,default activation diff --git a/tests/data/activity/decoupler.json b/tests/data/activity/decoupler.json new file mode 100644 index 0000000..ad6c9b4 --- /dev/null +++ b/tests/data/activity/decoupler.json @@ -0,0 +1 @@ +{"data":{"samples":{"profiles":["s0","s1","s2","s3","s4"],"genes":["TARGET168","OTHER16","OTHER12","OTHER40","TARGET185","TARGET9","TARGET14","TARGET91","TARGET98","TARGET1","TARGET21","TARGET138","TARGET237","OTHER38","TARGET190","TARGET226","TARGET186","TARGET113","TARGET86","TARGET147","TARGET76","TARGET67","TARGET172","TARGET209","TARGET132","TARGET217","TARGET58","TARGET211","TARGET85","TARGET32","TARGET46","OTHER20","TARGET28","TARGET152","TARGET191","TARGET50","OTHER49","TARGET70","OTHER32","TARGET196","OTHER13","OTHER41","TARGET11","OTHER45","OTHER29","TARGET183","TARGET228","OTHER52","TARGET165","OTHER7","OTHER8","OTHER9","TARGET207","TARGET151","TARGET169","TARGET0","OTHER27","OTHER3","TARGET199","TARGET156","OTHER4","TARGET139","TARGET174","TARGET141","TARGET29","TARGET235","TARGET83","TARGET75","OTHER58","TARGET79","TARGET171","TARGET192","TARGET122","TARGET161","TARGET102","TARGET143","OTHER44","OTHER10","TARGET195","OTHER19","TARGET68","TARGET188","TARGET114","TARGET118","TARGET225","TARGET194","TARGET88","TARGET87","OTHER5","TARGET52","TARGET145","OTHER48","TARGET160","TARGET125","TARGET184","TARGET177","TARGET126","OTHER28","TARGET42","TARGET40","TARGET214","TARGET7","TARGET80","TARGET224","OTHER39","TARGET34","TARGET101","TARGET164","TARGET150","TARGET71","TARGET180","OTHER15","TARGET81","TARGET219","OTHER36","TARGET179","OTHER2","TARGET53","TARGET33","TARGET144","TARGET203","OTHER47","TARGET204","TARGET41","TARGET231","OTHER33","TARGET197","TARGET92","OTHER25","TARGET37","TARGET27","TARGET18","OTHER57","TARGET170","OTHER17","TARGET97","OTHER11","TARGET227","TARGET5","TARGET35","TARGET2","TARGET61","OTHER53","OTHER18","TARGET117","TARGET106","OTHER55","TARGET99","OTHER59","TARGET13","OTHER50","TARGET38","OTHER14","OTHER37","TARGET154","TARGET205","OTHER30","TARGET218","OTHER42","TARGET72","OTHER1","TARGET48","OTHER56","TARGET215","TARGET157","TARGET178","TARGET124","TARGET59","TARGET105","TARGET45","TARGET146","TARGET104","TARGET25","TARGET4","OTHER46","TARGET212","TARGET8","TARGET47","TARGET221","TARGET62","OTHER51","TARGET78","OTHER24","TARGET167","TARGET223","TARGET64","TARGET103","TARGET110","TARGET16","TARGET187","TARGET24","OTHER34","TARGET77","TARGET140","TARGET232","TARGET236","TARGET176","OTHER35","OTHER43","OTHER23","TARGET229","TARGET198","TARGET130","TARGET233","TARGET60","TARGET22","TARGET12","TARGET213","OTHER31","OTHER22","TARGET162","TARGET193","TARGET131","TARGET200","TARGET95","TARGET43","TARGET220","TARGET230","OTHER26","TARGET133","TARGET30","TARGET66","TARGET189","OTHER6","TARGET63","TARGET153","TARGET182","TARGET112","TARGET238","TARGET234","TARGET119","TARGET111","TARGET49","OTHER0","TARGET31","TARGET6","TARGET142","TARGET173","OTHER54","TARGET55","TARGET222","TARGET148","OTHER21","TARGET20"]},"statistics":{"profiles":["t"],"genes":["TARGET168","OTHER16","OTHER12","OTHER40","TARGET185","TARGET9","TARGET14","TARGET91","TARGET98","TARGET1","TARGET21","TARGET138","TARGET237","OTHER38","TARGET190","TARGET226","TARGET186","TARGET113","TARGET86","TARGET147","TARGET76","TARGET67","TARGET172","TARGET209","TARGET132","TARGET217","TARGET58","TARGET211","TARGET85","TARGET32","TARGET46","OTHER20","TARGET28","TARGET152","TARGET191","TARGET50","OTHER49","TARGET70","OTHER32","TARGET196","OTHER13","OTHER41","TARGET11","OTHER45","OTHER29","TARGET183","TARGET228","OTHER52","TARGET165","OTHER7","OTHER8","OTHER9","TARGET207","TARGET151","TARGET169","TARGET0","OTHER27","OTHER3","TARGET199","TARGET156","OTHER4","TARGET139","TARGET174","TARGET141","TARGET29","TARGET235","TARGET83","TARGET75","OTHER58","TARGET79","TARGET171","TARGET192","TARGET122","TARGET161","TARGET102","TARGET143","OTHER44","OTHER10","TARGET195","OTHER19","TARGET68","TARGET188","TARGET114","TARGET118","TARGET225","TARGET194","TARGET88","TARGET87","OTHER5","TARGET52","TARGET145","OTHER48","TARGET160","TARGET125","TARGET184","TARGET177","TARGET126","OTHER28","TARGET42","TARGET40","TARGET214","TARGET7","TARGET80","TARGET224","OTHER39","TARGET34","TARGET101","TARGET164","TARGET150","TARGET71","TARGET180","OTHER15","TARGET81","TARGET219","OTHER36","TARGET179","OTHER2","TARGET53","TARGET33","TARGET144","TARGET203","OTHER47","TARGET204","TARGET41","TARGET231","OTHER33","TARGET197","TARGET92","OTHER25","TARGET37","TARGET27","TARGET18","OTHER57","TARGET170","OTHER17","TARGET97","OTHER11","TARGET227","TARGET5","TARGET35","TARGET2","TARGET61","OTHER53","OTHER18","TARGET117","TARGET106","OTHER55","TARGET99","OTHER59","TARGET13","OTHER50","TARGET38","OTHER14","OTHER37","TARGET154","TARGET205","OTHER30","TARGET218","OTHER42","TARGET72","OTHER1","TARGET48","OTHER56","TARGET215","TARGET157","TARGET178","TARGET124","TARGET59","TARGET105","TARGET45","TARGET146","TARGET104","TARGET25","TARGET4","OTHER46","TARGET212","TARGET8","TARGET47","TARGET221","TARGET62","OTHER51","TARGET78","OTHER24","TARGET167","TARGET223","TARGET64","TARGET103","TARGET110","TARGET16","TARGET187","TARGET24","OTHER34","TARGET77","TARGET140","TARGET232","TARGET236","TARGET176","OTHER35","OTHER43","OTHER23","TARGET229","TARGET198","TARGET130","TARGET233","TARGET60","TARGET22","TARGET12","TARGET213","OTHER31","OTHER22","TARGET162","TARGET193","TARGET131","TARGET200","TARGET95","TARGET43","TARGET220","TARGET230","OTHER26","TARGET133","TARGET30","TARGET66","TARGET189","OTHER6","TARGET63","TARGET153","TARGET182","TARGET112","TARGET238","TARGET234","TARGET119","TARGET111","TARGET49","OTHER0","TARGET31","TARGET6","TARGET142","TARGET173","OTHER54","TARGET55","TARGET222","TARGET148","OTHER21","TARGET20"]},"missing_0":{"profiles":["s0"],"genes":["TARGET168","OTHER16","OTHER12","OTHER40","TARGET185","TARGET9","TARGET14","TARGET91","TARGET98","TARGET1","TARGET21","TARGET138","TARGET237","OTHER38","TARGET190","TARGET226","TARGET186","TARGET113","TARGET86","TARGET76","TARGET67","TARGET172","TARGET209","TARGET132","TARGET217","TARGET58","TARGET211","TARGET85","TARGET32","TARGET46","OTHER20","TARGET28","TARGET152","TARGET191","TARGET50","OTHER49","TARGET70","OTHER32","TARGET196","OTHER13","OTHER41","TARGET11","OTHER45","OTHER29","TARGET183","TARGET228","OTHER52","TARGET165","OTHER7","OTHER8","OTHER9","TARGET207","TARGET151","TARGET169","TARGET0","OTHER27","OTHER3","TARGET199","TARGET156","OTHER4","TARGET139","TARGET174","TARGET141","TARGET29","TARGET235","TARGET75","OTHER58","TARGET79","TARGET171","TARGET192","TARGET122","TARGET161","TARGET102","TARGET143","OTHER44","OTHER10","TARGET195","OTHER19","TARGET68","TARGET188","TARGET114","TARGET118","TARGET225","TARGET194","TARGET88","TARGET87","OTHER5","TARGET52","TARGET145","OTHER48","TARGET160","TARGET125","TARGET184","TARGET177","TARGET126","OTHER28","TARGET42","TARGET40","TARGET214","TARGET7","TARGET80","TARGET224","OTHER39","TARGET34","TARGET101","TARGET164","TARGET150","TARGET71","TARGET180","OTHER15","TARGET81","TARGET219","OTHER36","TARGET179","OTHER2","TARGET53","TARGET144","TARGET203","OTHER47","TARGET204","TARGET41","TARGET231","TARGET197","TARGET92","OTHER25","TARGET37","TARGET27","TARGET18","OTHER57","TARGET170","OTHER17","TARGET97","OTHER11","TARGET227","TARGET5","TARGET35","TARGET2","TARGET61","OTHER53","OTHER18","TARGET117","TARGET106","OTHER55","TARGET99","OTHER59","TARGET13","OTHER50","TARGET38","OTHER14","OTHER37","TARGET154","TARGET205","OTHER30","TARGET218","OTHER42","TARGET72","OTHER1","TARGET48","OTHER56","TARGET215","TARGET157","TARGET178","TARGET124","TARGET59","TARGET105","TARGET45","TARGET146","TARGET104","TARGET25","TARGET4","OTHER46","TARGET212","TARGET8","TARGET47","TARGET221","TARGET62","OTHER51","TARGET78","OTHER24","TARGET167","TARGET223","TARGET64","TARGET103","TARGET110","TARGET16","TARGET187","TARGET24","OTHER34","TARGET77","TARGET140","TARGET232","TARGET236","TARGET176","OTHER35","OTHER43","TARGET229","TARGET198","TARGET130","TARGET233","TARGET60","TARGET22","TARGET12","TARGET213","OTHER31","OTHER22","TARGET162","TARGET193","TARGET131","TARGET200","TARGET95","TARGET43","TARGET220","TARGET230","OTHER26","TARGET30","TARGET66","TARGET189","OTHER6","TARGET63","TARGET153","TARGET182","TARGET112","TARGET238","TARGET234","TARGET119","TARGET111","OTHER0","TARGET31","TARGET6","TARGET142","TARGET173","OTHER54","TARGET55","TARGET222","TARGET148","TARGET20"]},"missing_1":{"profiles":["s1"],"genes":["TARGET168","OTHER16","OTHER12","OTHER40","TARGET185","TARGET9","TARGET14","TARGET91","TARGET98","TARGET1","TARGET21","TARGET138","TARGET237","OTHER38","TARGET190","TARGET226","TARGET186","TARGET113","TARGET86","TARGET147","TARGET76","TARGET67","TARGET172","TARGET209","TARGET132","TARGET217","TARGET58","TARGET211","TARGET85","TARGET32","TARGET46","OTHER20","TARGET28","TARGET152","TARGET191","TARGET50","OTHER49","TARGET70","OTHER32","TARGET196","OTHER13","OTHER41","TARGET11","OTHER45","OTHER29","TARGET183","TARGET228","OTHER52","TARGET165","OTHER7","OTHER8","OTHER9","TARGET207","TARGET151","TARGET169","TARGET0","OTHER27","OTHER3","TARGET199","TARGET156","OTHER4","TARGET139","TARGET174","TARGET141","TARGET29","TARGET235","TARGET83","TARGET75","OTHER58","TARGET79","TARGET171","TARGET192","TARGET122","TARGET161","TARGET102","TARGET143","OTHER44","OTHER10","TARGET195","OTHER19","TARGET68","TARGET188","TARGET114","TARGET118","TARGET225","TARGET194","TARGET88","TARGET87","OTHER5","TARGET52","TARGET145","OTHER48","TARGET160","TARGET184","TARGET177","TARGET126","OTHER28","TARGET42","TARGET40","TARGET214","TARGET7","TARGET80","TARGET224","OTHER39","TARGET34","TARGET101","TARGET164","TARGET150","TARGET71","TARGET180","OTHER15","TARGET81","TARGET219","OTHER36","TARGET179","OTHER2","TARGET53","TARGET33","TARGET144","TARGET203","OTHER47","TARGET204","TARGET41","TARGET231","OTHER33","TARGET197","TARGET92","OTHER25","TARGET37","TARGET27","TARGET18","OTHER57","TARGET170","OTHER17","TARGET97","OTHER11","TARGET227","TARGET5","TARGET35","TARGET2","TARGET61","OTHER53","OTHER18","TARGET117","TARGET106","OTHER55","TARGET99","OTHER59","TARGET13","OTHER50","TARGET38","OTHER14","OTHER37","TARGET154","TARGET205","OTHER30","TARGET218","OTHER42","TARGET72","OTHER1","OTHER56","TARGET215","TARGET157","TARGET178","TARGET124","TARGET59","TARGET105","TARGET45","TARGET146","TARGET104","TARGET25","TARGET4","OTHER46","TARGET212","TARGET8","TARGET47","TARGET221","TARGET62","OTHER51","TARGET78","OTHER24","TARGET167","TARGET223","TARGET64","TARGET103","TARGET110","TARGET16","TARGET187","TARGET24","OTHER34","TARGET77","TARGET140","TARGET232","TARGET236","TARGET176","OTHER35","OTHER43","OTHER23","TARGET229","TARGET198","TARGET130","TARGET233","TARGET60","TARGET22","TARGET12","TARGET213","OTHER31","OTHER22","TARGET162","TARGET193","TARGET131","TARGET200","TARGET95","TARGET43","TARGET220","TARGET230","OTHER26","TARGET133","TARGET30","TARGET66","TARGET189","OTHER6","TARGET63","TARGET153","TARGET182","TARGET112","TARGET238","TARGET234","TARGET119","TARGET111","TARGET49","OTHER0","TARGET31","TARGET6","TARGET142","TARGET173","OTHER54","TARGET55","TARGET222","TARGET148","OTHER21","TARGET20"]}},"networks":{"collectri":{"source":["TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19"],"target":["TARGET148","TARGET233","TARGET22","TARGET139","TARGET236","TARGET105","TARGET4","TARGET64","TARGET79","TARGET235","TARGET173","TARGET86","TARGET226","TARGET131","TARGET85","TARGET122","TARGET2","TARGET81","TARGET203","TARGET80","TARGET180","TARGET118","TARGET231","TARGET189","TARGET45","TARGET138","TARGET227","TARGET207","TARGET30","TARGET28","TARGET238","TARGET92","TARGET59","TARGET179","TARGET195","TARGET160","TARGET110","TARGET63","TARGET178","TARGET182","TARGET174","TARGET111","TARGET152","TARGET47","TARGET132","TARGET231","TARGET88","TARGET167","TARGET103","TARGET58","TARGET118","TARGET217","TARGET104","TARGET186","TARGET62","TARGET177","TARGET222","TARGET234","TARGET16","TARGET233","TARGET177","TARGET18","TARGET118","TARGET37","TARGET95","TARGET43","TARGET205","TARGET219","TARGET187","TARGET104","TARGET101","TARGET183","TARGET114","TARGET237","TARGET35","TARGET27","TARGET75","TARGET16","TARGET58","TARGET189","TARGET117","TARGET164","TARGET186","TARGET28","TARGET154","TARGET226","TARGET71","TARGET204","TARGET112","TARGET46","TARGET195","TARGET199","TARGET9","TARGET227","TARGET13","TARGET221","TARGET156","TARGET91","TARGET87","TARGET130","TARGET95","TARGET161","TARGET62","TARGET157","TARGET61","TARGET153","TARGET85","TARGET150","TARGET117","TARGET20","TARGET126","TARGET235","TARGET5","TARGET146","TARGET49","TARGET204","TARGET124","TARGET184","TARGET103","TARGET171","TARGET64","TARGET81","TARGET173","TARGET188","TARGET22","TARGET117","TARGET101","TARGET224","TARGET63","TARGET48","TARGET169","TARGET101","TARGET118","TARGET87","TARGET41","TARGET95","TARGET188","TARGET187","TARGET199","TARGET235","TARGET1","TARGET230","TARGET104","TARGET191","TARGET86","TARGET61","TARGET88","TARGET228","TARGET25","TARGET14","TARGET226","TARGET169","TARGET78","TARGET50","TARGET5","TARGET52","TARGET231","TARGET33","TARGET86","TARGET35","TARGET185","TARGET58","TARGET87","TARGET172","TARGET143","TARGET85","TARGET196","TARGET154","TARGET131","TARGET106","TARGET1","TARGET31","TARGET232","TARGET211","TARGET146","TARGET178","TARGET60","TARGET49","TARGET125","TARGET200","TARGET46","TARGET238","TARGET114","TARGET141","TARGET101","TARGET143","TARGET147","TARGET58","TARGET139","TARGET81","TARGET55","TARGET113","TARGET221","TARGET131","TARGET83","TARGET70","TARGET223","TARGET38","TARGET88","TARGET140","TARGET8","TARGET172","TARGET79","TARGET133","TARGET199","TARGET112","TARGET161","TARGET165","TARGET43","TARGET114","TARGET55","TARGET233","TARGET191","TARGET80","TARGET192","TARGET170","TARGET119","TARGET196","TARGET34","TARGET124","TARGET162","TARGET7","TARGET37","TARGET101","TARGET154","TARGET118","TARGET205","TARGET151","TARGET27","TARGET188","TARGET193","TARGET112","TARGET113","TARGET67","TARGET98","TARGET152","TARGET124","TARGET138","TARGET48","TARGET170","TARGET1","TARGET199","TARGET40","TARGET139","TARGET205","TARGET104","TARGET7","TARGET229","TARGET47","TARGET83","TARGET6","TARGET224","TARGET130","TARGET66","TARGET29","TARGET12","TARGET236","TARGET194","TARGET49","TARGET146","TARGET148","TARGET68","TARGET32","TARGET233","TARGET222","TARGET209","TARGET238","TARGET81","TARGET192","TARGET199","TARGET106","TARGET34","TARGET168","TARGET77","TARGET28","TARGET198","TARGET215","TARGET37","TARGET13","TARGET220","TARGET176","TARGET196","TARGET0","TARGET146","TARGET221","TARGET27","TARGET172","TARGET142","TARGET24","TARGET144","TARGET25","TARGET117","TARGET197","TARGET95","TARGET164","TARGET104","TARGET85","TARGET14","TARGET151","TARGET42","TARGET187","TARGET193","TARGET6","TARGET146","TARGET42","TARGET55","TARGET174","TARGET233","TARGET103","TARGET220","TARGET80","TARGET169","TARGET62","TARGET97","TARGET125","TARGET63","TARGET110","TARGET218","TARGET172","TARGET225","TARGET212","TARGET184","TARGET168","TARGET238","TARGET194","TARGET27","TARGET234","TARGET58","TARGET171","TARGET16","TARGET80","TARGET72","TARGET105","TARGET119","TARGET30","TARGET214","TARGET196","TARGET78","TARGET191","TARGET131","TARGET125","TARGET199","TARGET168","TARGET190","TARGET21","TARGET139","TARGET11","TARGET102","TARGET144","TARGET99","TARGET168","TARGET38","TARGET193","TARGET236","TARGET60","TARGET53","TARGET233","TARGET213","TARGET76","TARGET139","TARGET145","TARGET8","TARGET212","TARGET33","TARGET88","TARGET104","TARGET211"],"weight":[1.0,1.0,1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,-1.0,1.0,-1.0,1.0,1.0,1.0,-1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,-1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]},"one_sign":{"source":["TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF0","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF1","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF2","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF3","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF4","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF5","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF6","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF7","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF8","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF9","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF10","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF11","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF12","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF13","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF14","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF15","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF16","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF17","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF18","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19","TF19"],"target":["TARGET148","TARGET233","TARGET22","TARGET139","TARGET236","TARGET105","TARGET4","TARGET64","TARGET79","TARGET235","TARGET173","TARGET86","TARGET226","TARGET131","TARGET85","TARGET122","TARGET2","TARGET81","TARGET203","TARGET80","TARGET180","TARGET118","TARGET231","TARGET189","TARGET45","TARGET138","TARGET227","TARGET207","TARGET30","TARGET28","TARGET238","TARGET92","TARGET59","TARGET179","TARGET195","TARGET160","TARGET110","TARGET63","TARGET178","TARGET182","TARGET174","TARGET111","TARGET152","TARGET47","TARGET132","TARGET231","TARGET88","TARGET167","TARGET103","TARGET58","TARGET118","TARGET217","TARGET104","TARGET186","TARGET62","TARGET177","TARGET222","TARGET234","TARGET16","TARGET233","TARGET177","TARGET18","TARGET118","TARGET37","TARGET95","TARGET43","TARGET205","TARGET219","TARGET187","TARGET104","TARGET101","TARGET183","TARGET114","TARGET237","TARGET35","TARGET27","TARGET75","TARGET16","TARGET58","TARGET189","TARGET117","TARGET164","TARGET186","TARGET28","TARGET154","TARGET226","TARGET71","TARGET204","TARGET112","TARGET46","TARGET195","TARGET199","TARGET9","TARGET227","TARGET13","TARGET221","TARGET156","TARGET91","TARGET87","TARGET130","TARGET95","TARGET161","TARGET62","TARGET157","TARGET61","TARGET153","TARGET85","TARGET150","TARGET117","TARGET20","TARGET126","TARGET235","TARGET5","TARGET146","TARGET49","TARGET204","TARGET124","TARGET184","TARGET103","TARGET171","TARGET64","TARGET81","TARGET173","TARGET188","TARGET22","TARGET117","TARGET101","TARGET224","TARGET63","TARGET48","TARGET169","TARGET101","TARGET118","TARGET87","TARGET41","TARGET95","TARGET188","TARGET187","TARGET199","TARGET235","TARGET1","TARGET230","TARGET104","TARGET191","TARGET86","TARGET61","TARGET88","TARGET228","TARGET25","TARGET14","TARGET226","TARGET169","TARGET78","TARGET50","TARGET5","TARGET52","TARGET231","TARGET33","TARGET86","TARGET35","TARGET185","TARGET58","TARGET87","TARGET172","TARGET143","TARGET85","TARGET196","TARGET154","TARGET131","TARGET106","TARGET1","TARGET31","TARGET232","TARGET211","TARGET146","TARGET178","TARGET60","TARGET49","TARGET125","TARGET200","TARGET46","TARGET238","TARGET114","TARGET141","TARGET101","TARGET143","TARGET147","TARGET58","TARGET139","TARGET81","TARGET55","TARGET113","TARGET221","TARGET131","TARGET83","TARGET70","TARGET223","TARGET38","TARGET88","TARGET140","TARGET8","TARGET172","TARGET79","TARGET133","TARGET199","TARGET112","TARGET161","TARGET165","TARGET43","TARGET114","TARGET55","TARGET233","TARGET191","TARGET80","TARGET192","TARGET170","TARGET119","TARGET196","TARGET34","TARGET124","TARGET162","TARGET7","TARGET37","TARGET101","TARGET154","TARGET118","TARGET205","TARGET151","TARGET27","TARGET188","TARGET193","TARGET112","TARGET113","TARGET67","TARGET98","TARGET152","TARGET124","TARGET138","TARGET48","TARGET170","TARGET1","TARGET199","TARGET40","TARGET139","TARGET205","TARGET104","TARGET7","TARGET229","TARGET47","TARGET83","TARGET6","TARGET224","TARGET130","TARGET66","TARGET29","TARGET12","TARGET236","TARGET194","TARGET49","TARGET146","TARGET148","TARGET68","TARGET32","TARGET233","TARGET222","TARGET209","TARGET238","TARGET81","TARGET192","TARGET199","TARGET106","TARGET34","TARGET168","TARGET77","TARGET28","TARGET198","TARGET215","TARGET37","TARGET13","TARGET220","TARGET176","TARGET196","TARGET0","TARGET146","TARGET221","TARGET27","TARGET172","TARGET142","TARGET24","TARGET144","TARGET25","TARGET117","TARGET197","TARGET95","TARGET164","TARGET104","TARGET85","TARGET14","TARGET151","TARGET42","TARGET187","TARGET193","TARGET6","TARGET146","TARGET42","TARGET55","TARGET174","TARGET233","TARGET103","TARGET220","TARGET80","TARGET169","TARGET62","TARGET97","TARGET125","TARGET63","TARGET110","TARGET218","TARGET172","TARGET225","TARGET212","TARGET184","TARGET168","TARGET238","TARGET194","TARGET27","TARGET234","TARGET58","TARGET171","TARGET16","TARGET80","TARGET72","TARGET105","TARGET119","TARGET30","TARGET214","TARGET196","TARGET78","TARGET191","TARGET131","TARGET125","TARGET199","TARGET168","TARGET190","TARGET21","TARGET139","TARGET11","TARGET102","TARGET144","TARGET99","TARGET168","TARGET38","TARGET193","TARGET236","TARGET60","TARGET53","TARGET233","TARGET213","TARGET76","TARGET139","TARGET145","TARGET8","TARGET212","TARGET33","TARGET88","TARGET104","TARGET211"],"weight":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0]}},"cases":[{"name":"ulm","data":"samples","network":"collectri","method":"ulm","tmin":5,"options":{},"bar":"s4"},{"name":"ulm, tmin 3, weights of one sign","data":"samples","network":"one_sign","method":"ulm","tmin":3,"options":{}},{"name":"ulm, t-statistics","data":"statistics","network":"collectri","method":"ulm","tmin":5,"options":{}},{"name":"mlm","data":"samples","network":"collectri","method":"mlm","tmin":5,"options":{}},{"name":"weighted mean","data":"samples","network":"collectri","method":"waggr","tmin":5,"options":{"times":0}},{"name":"weighted sum","data":"samples","network":"collectri","method":"waggr","tmin":5,"options":{"times":0,"fun":"wsum"}},{"name":"weighted mean, 100 permutations","data":"samples","network":"collectri","method":"waggr","tmin":5,"options":{"times":100,"seed":42}},{"name":"ulm, missing values, s0","data":"missing_0","network":"collectri","method":"ulm","tmin":5,"options":{}},{"name":"mlm, missing values, s0","data":"missing_0","network":"collectri","method":"mlm","tmin":5,"options":{}},{"name":"ulm, missing values, s1","data":"missing_1","network":"collectri","method":"ulm","tmin":5,"options":{}},{"name":"mlm, missing values, s1","data":"missing_1","network":"collectri","method":"mlm","tmin":5,"options":{}}],"found":{"ulm":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"],"bar_names":["TF11","TF5","TF10","TF2","TF4","TF0","TF8","TF6","TF16","TF14","TF1","TF7","TF17","TF15","TF9"]},"ulm, tmin 3, weights of one sign":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"ulm, t-statistics":{"profiles":["t"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"mlm":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"weighted mean":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"weighted sum":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"weighted mean, 100 permutations":{"profiles":["s0","s1","s2","s3","s4"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER28","TARGET86","TARGET49","TARGET140","OTHER27","TARGET61","TARGET157","OTHER36","TARGET106","TARGET122","TARGET81","OTHER52","TARGET152","TARGET190","OTHER41","TARGET76","TARGET197","OTHER4","TARGET145","TARGET156","TARGET173","OTHER11","TARGET126","TARGET71","OTHER54","OTHER0","TARGET16","TARGET221","TARGET176","TARGET20","TARGET167","TARGET153","TARGET59","TARGET189","OTHER15","TARGET9","OTHER26","TARGET35","TARGET146","TARGET97","OTHER13","OTHER34","TARGET27","OTHER39","TARGET32","TARGET133","TARGET174","TARGET102","TARGET131","TARGET164","TARGET33","TARGET64","TARGET193","TARGET148","OTHER48","TARGET187","TARGET0","TARGET217","OTHER45","OTHER38","TARGET80","OTHER14","TARGET70","TARGET191","TARGET50","TARGET91","TARGET179","TARGET92","TARGET95","TARGET104","TARGET12","OTHER29","TARGET225","TARGET7","TARGET88","OTHER18","TARGET182","TARGET113","TARGET87","TARGET85","TARGET232","TARGET99","TARGET228","OTHER21","TARGET83","OTHER55","OTHER46","OTHER40","OTHER50","TARGET214","TARGET14","TARGET77","OTHER53","TARGET227","TARGET223","TARGET151","TARGET124","TARGET238","TARGET68","TARGET62","TARGET195","TARGET25","OTHER44","TARGET139","OTHER56","TARGET43","TARGET200","TARGET5","TARGET38","TARGET78","TARGET231","TARGET118","OTHER23","OTHER25","TARGET226","TARGET233","OTHER58","TARGET28","OTHER33","TARGET101","TARGET218","TARGET184","OTHER47","TARGET53","TARGET161","TARGET30","TARGET55","OTHER5","TARGET170","TARGET205","OTHER32","TARGET144","TARGET196","TARGET211","TARGET66","TARGET183","TARGET224","TARGET2","TARGET114","TARGET111","TARGET160","TARGET143","TARGET58","TARGET213","TARGET177","TARGET37","TARGET60","TARGET11","OTHER35","TARGET198","OTHER24","TARGET229","TARGET150","OTHER37","OTHER42","TARGET215","TARGET234","TARGET171","TARGET192","TARGET31","TARGET103","TARGET237","TARGET79","TARGET117","TARGET1","TARGET132","TARGET142","TARGET236","TARGET180","TARGET220","TARGET178","OTHER8","TARGET125","OTHER2","TARGET165","TARGET230","TARGET172","TARGET63","TARGET67","TARGET185","TARGET209","TARGET154","OTHER1","OTHER19","TARGET13","OTHER30","TARGET130","TARGET138","OTHER6","TARGET212","TARGET21","TARGET188","TARGET48","TARGET112","TARGET110","TARGET45","TARGET98","TARGET203","OTHER7","TARGET219","TARGET147","OTHER10","OTHER31","OTHER43","OTHER59","OTHER57","TARGET162","TARGET4","TARGET222","TARGET52","TARGET42","TARGET29","TARGET40","TARGET204","TARGET186","TARGET47","TARGET105","TARGET41","TARGET24","TARGET18","TARGET6","TARGET75","TARGET8","TARGET46","OTHER20","TARGET169","TARGET207","TARGET22","OTHER9","TARGET199","TARGET235","TARGET141","OTHER51","OTHER17","OTHER49","TARGET119","OTHER22","TARGET34","OTHER3","TARGET194","TARGET72"]},"ulm, missing values, s0":{"profiles":["s0"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["TARGET48","OTHER36","TARGET119","TARGET198","TARGET195","TARGET237","TARGET42","OTHER7","TARGET131","TARGET28","OTHER24","TARGET205","TARGET52","OTHER14","TARGET126","TARGET70","OTHER27","TARGET203","TARGET207","TARGET153","TARGET124","OTHER45","OTHER18","TARGET41","TARGET20","OTHER30","TARGET122","TARGET47","TARGET59","TARGET99","OTHER35","TARGET34","OTHER3","TARGET186","TARGET139","TARGET145","TARGET224","TARGET31","OTHER20","TARGET68","TARGET103","OTHER5","TARGET152","TARGET218","TARGET81","TARGET22","TARGET220","TARGET211","TARGET118","TARGET14","TARGET7","TARGET167","OTHER26","TARGET148","TARGET97","OTHER10","TARGET140","OTHER48","OTHER32","TARGET188","TARGET58","TARGET190","TARGET111","TARGET170","TARGET75","TARGET182","TARGET5","TARGET35","TARGET130","OTHER42","TARGET11","OTHER40","OTHER49","TARGET27","TARGET9","OTHER22","OTHER29","TARGET1","TARGET85","TARGET212","TARGET29","OTHER52","OTHER57","TARGET62","OTHER54","TARGET160","OTHER31","TARGET151","TARGET189","TARGET177","TARGET88","OTHER13","OTHER58","OTHER34","OTHER0","TARGET106","TARGET232","TARGET71","OTHER38","TARGET113","TARGET233","TARGET223","OTHER15","TARGET92","TARGET171","TARGET18","OTHER47","OTHER39","TARGET142","OTHER53","OTHER6","OTHER19","TARGET112","OTHER28","TARGET204","TARGET164","TARGET50","TARGET217","TARGET221","OTHER41","TARGET60","OTHER25","TARGET143","OTHER56","TARGET225","TARGET79","TARGET132","OTHER11","TARGET215","TARGET53","TARGET192","TARGET196","TARGET227","OTHER51","TARGET146","TARGET180","TARGET234","TARGET30","TARGET64","TARGET77","TARGET238","TARGET13","TARGET222","TARGET199","OTHER46","TARGET138","TARGET213","OTHER17","TARGET98","TARGET209","TARGET231","TARGET4","TARGET80","TARGET16","TARGET154","TARGET187","TARGET87","TARGET150","TARGET183","TARGET12","TARGET67","TARGET200","TARGET76","TARGET185","TARGET172","TARGET117","OTHER50","TARGET102","TARGET2","OTHER55","TARGET24","TARGET162","TARGET45","TARGET178","TARGET21","OTHER44","TARGET38","TARGET43","TARGET8","TARGET72","TARGET91","TARGET219","TARGET228","TARGET101","TARGET6","TARGET161","TARGET176","TARGET110","TARGET61","TARGET193","TARGET173","TARGET157","TARGET40","TARGET194","TARGET125","OTHER4","TARGET184","TARGET179","TARGET226","TARGET95","OTHER1","OTHER2","TARGET78","TARGET197","TARGET104","TARGET141","TARGET105","TARGET32","TARGET46","OTHER9","OTHER8","TARGET236","TARGET165","TARGET0","TARGET174","TARGET156","TARGET25","TARGET37","TARGET191","TARGET66","TARGET229","TARGET214","TARGET169","TARGET114","OTHER37","TARGET55","OTHER59","OTHER43","TARGET144","TARGET86","TARGET235","TARGET63","TARGET230"]},"mlm, missing values, s0":{"profiles":["s0"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["TARGET48","OTHER36","TARGET119","TARGET198","TARGET195","TARGET237","TARGET42","OTHER7","TARGET131","TARGET28","OTHER24","TARGET205","TARGET52","OTHER14","TARGET126","TARGET70","OTHER27","TARGET203","TARGET207","TARGET153","TARGET124","OTHER45","OTHER18","TARGET41","TARGET20","OTHER30","TARGET122","TARGET47","TARGET59","TARGET99","OTHER35","TARGET34","OTHER3","TARGET186","TARGET139","TARGET145","TARGET224","TARGET31","OTHER20","TARGET68","TARGET103","OTHER5","TARGET152","TARGET218","TARGET81","TARGET22","TARGET220","TARGET211","TARGET118","TARGET14","TARGET7","TARGET167","OTHER26","TARGET148","TARGET97","OTHER10","TARGET140","OTHER48","OTHER32","TARGET188","TARGET58","TARGET190","TARGET111","TARGET170","TARGET75","TARGET182","TARGET5","TARGET35","TARGET130","OTHER42","TARGET11","OTHER40","OTHER49","TARGET27","TARGET9","OTHER22","OTHER29","TARGET1","TARGET85","TARGET212","TARGET29","OTHER52","OTHER57","TARGET62","OTHER54","TARGET160","OTHER31","TARGET151","TARGET189","TARGET177","TARGET88","OTHER13","OTHER58","OTHER34","OTHER0","TARGET106","TARGET232","TARGET71","OTHER38","TARGET113","TARGET233","TARGET223","OTHER15","TARGET92","TARGET171","TARGET18","OTHER47","OTHER39","TARGET142","OTHER53","OTHER6","OTHER19","TARGET112","OTHER28","TARGET204","TARGET164","TARGET50","TARGET217","TARGET221","OTHER41","TARGET60","OTHER25","TARGET143","OTHER56","TARGET225","TARGET79","TARGET132","OTHER11","TARGET215","TARGET53","TARGET192","TARGET196","TARGET227","OTHER51","TARGET146","TARGET180","TARGET234","TARGET30","TARGET64","TARGET77","TARGET238","TARGET13","TARGET222","TARGET199","OTHER46","TARGET138","TARGET213","OTHER17","TARGET98","TARGET209","TARGET231","TARGET4","TARGET80","TARGET16","TARGET154","TARGET187","TARGET87","TARGET150","TARGET183","TARGET12","TARGET67","TARGET200","TARGET76","TARGET185","TARGET172","TARGET117","OTHER50","TARGET102","TARGET2","OTHER55","TARGET24","TARGET162","TARGET45","TARGET178","TARGET21","OTHER44","TARGET38","TARGET43","TARGET8","TARGET72","TARGET91","TARGET219","TARGET228","TARGET101","TARGET6","TARGET161","TARGET176","TARGET110","TARGET61","TARGET193","TARGET173","TARGET157","TARGET40","TARGET194","TARGET125","OTHER4","TARGET184","TARGET179","TARGET226","TARGET95","OTHER1","OTHER2","TARGET78","TARGET197","TARGET104","TARGET141","TARGET105","TARGET32","TARGET46","OTHER9","OTHER8","TARGET236","TARGET165","TARGET0","TARGET174","TARGET156","TARGET25","TARGET37","TARGET191","TARGET66","TARGET229","TARGET214","TARGET169","TARGET114","OTHER37","TARGET55","OTHER59","OTHER43","TARGET144","TARGET86","TARGET235","TARGET63","TARGET230"]},"ulm, missing values, s1":{"profiles":["s1"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER11","TARGET118","TARGET160","TARGET41","TARGET85","TARGET132","TARGET43","OTHER15","TARGET119","OTHER38","TARGET16","TARGET60","OTHER58","TARGET214","TARGET157","TARGET31","TARGET103","TARGET55","TARGET53","TARGET45","TARGET106","TARGET169","TARGET88","TARGET24","TARGET117","TARGET198","TARGET231","OTHER45","TARGET1","TARGET191","OTHER49","TARGET9","TARGET80","TARGET142","TARGET183","OTHER22","OTHER25","TARGET77","TARGET189","TARGET0","TARGET195","TARGET172","TARGET199","TARGET87","OTHER6","TARGET200","TARGET113","TARGET222","TARGET34","TARGET182","TARGET196","TARGET180","TARGET162","TARGET124","OTHER13","OTHER56","TARGET71","TARGET228","TARGET6","TARGET28","TARGET152","OTHER35","TARGET110","TARGET167","OTHER4","TARGET171","TARGET111","TARGET11","OTHER18","TARGET219","TARGET114","TARGET177","TARGET32","TARGET220","TARGET72","TARGET211","TARGET42","TARGET95","OTHER14","TARGET188","TARGET141","TARGET18","TARGET13","OTHER40","TARGET99","TARGET193","TARGET14","TARGET52","TARGET35","OTHER17","TARGET232","OTHER30","TARGET122","TARGET207","TARGET154","TARGET47","OTHER59","OTHER43","TARGET61","TARGET156","TARGET139","TARGET37","TARGET126","TARGET97","TARGET102","TARGET133","TARGET2","TARGET5","TARGET178","OTHER33","TARGET190","OTHER21","TARGET221","OTHER20","OTHER26","TARGET224","TARGET130","TARGET4","OTHER24","TARGET179","TARGET192","TARGET146","TARGET173","TARGET194","TARGET238","TARGET101","TARGET70","OTHER2","OTHER32","TARGET58","TARGET138","OTHER29","TARGET7","TARGET227","TARGET68","TARGET213","TARGET145","TARGET161","TARGET217","TARGET92","TARGET184","TARGET197","TARGET143","OTHER41","TARGET236","OTHER23","OTHER51","TARGET33","TARGET164","OTHER50","TARGET205","OTHER1","TARGET112","TARGET105","TARGET79","TARGET131","TARGET223","TARGET237","TARGET75","OTHER53","TARGET98","TARGET78","OTHER57","TARGET140","TARGET150","TARGET203","TARGET215","OTHER7","OTHER48","OTHER36","OTHER52","TARGET186","TARGET67","TARGET83","TARGET76","TARGET185","TARGET209","TARGET38","TARGET218","OTHER10","TARGET22","TARGET20","TARGET229","OTHER34","TARGET66","OTHER46","TARGET21","OTHER19","TARGET148","TARGET63","TARGET64","TARGET230","TARGET91","TARGET144","TARGET165","TARGET81","TARGET147","OTHER44","TARGET12","TARGET176","OTHER55","TARGET30","TARGET25","TARGET104","OTHER54","OTHER5","OTHER28","TARGET49","TARGET40","OTHER47","TARGET226","TARGET8","TARGET59","TARGET204","TARGET187","TARGET27","OTHER0","TARGET235","TARGET212","TARGET153","TARGET46","TARGET151","OTHER9","TARGET233","OTHER8","OTHER3","TARGET29","TARGET174","TARGET62","TARGET170","TARGET50","TARGET234","OTHER31","OTHER39","OTHER27","TARGET225","OTHER42","TARGET86","OTHER37"]},"mlm, missing values, s1":{"profiles":["s1"],"sources":["TF0","TF1","TF10","TF11","TF12","TF13","TF14","TF15","TF16","TF17","TF18","TF19","TF2","TF3","TF4","TF5","TF6","TF7","TF8","TF9"],"genes":["OTHER11","TARGET118","TARGET160","TARGET41","TARGET85","TARGET132","TARGET43","OTHER15","TARGET119","OTHER38","TARGET16","TARGET60","OTHER58","TARGET214","TARGET157","TARGET31","TARGET103","TARGET55","TARGET53","TARGET45","TARGET106","TARGET169","TARGET88","TARGET24","TARGET117","TARGET198","TARGET231","OTHER45","TARGET1","TARGET191","OTHER49","TARGET9","TARGET80","TARGET142","TARGET183","OTHER22","OTHER25","TARGET77","TARGET189","TARGET0","TARGET195","TARGET172","TARGET199","TARGET87","OTHER6","TARGET200","TARGET113","TARGET222","TARGET34","TARGET182","TARGET196","TARGET180","TARGET162","TARGET124","OTHER13","OTHER56","TARGET71","TARGET228","TARGET6","TARGET28","TARGET152","OTHER35","TARGET110","TARGET167","OTHER4","TARGET171","TARGET111","TARGET11","OTHER18","TARGET219","TARGET114","TARGET177","TARGET32","TARGET220","TARGET72","TARGET211","TARGET42","TARGET95","OTHER14","TARGET188","TARGET141","TARGET18","TARGET13","OTHER40","TARGET99","TARGET193","TARGET14","TARGET52","TARGET35","OTHER17","TARGET232","OTHER30","TARGET122","TARGET207","TARGET154","TARGET47","OTHER59","OTHER43","TARGET61","TARGET156","TARGET139","TARGET37","TARGET126","TARGET97","TARGET102","TARGET133","TARGET2","TARGET5","TARGET178","OTHER33","TARGET190","OTHER21","TARGET221","OTHER20","OTHER26","TARGET224","TARGET130","TARGET4","OTHER24","TARGET179","TARGET192","TARGET146","TARGET173","TARGET194","TARGET238","TARGET101","TARGET70","OTHER2","OTHER32","TARGET58","TARGET138","OTHER29","TARGET7","TARGET227","TARGET68","TARGET213","TARGET145","TARGET161","TARGET217","TARGET92","TARGET184","TARGET197","TARGET143","OTHER41","TARGET236","OTHER23","OTHER51","TARGET33","TARGET164","OTHER50","TARGET205","OTHER1","TARGET112","TARGET105","TARGET79","TARGET131","TARGET223","TARGET237","TARGET75","OTHER53","TARGET98","TARGET78","OTHER57","TARGET140","TARGET150","TARGET203","TARGET215","OTHER7","OTHER48","OTHER36","OTHER52","TARGET186","TARGET67","TARGET83","TARGET76","TARGET185","TARGET209","TARGET38","TARGET218","OTHER10","TARGET22","TARGET20","TARGET229","OTHER34","TARGET66","OTHER46","TARGET21","OTHER19","TARGET148","TARGET63","TARGET64","TARGET230","TARGET91","TARGET144","TARGET165","TARGET81","TARGET147","OTHER44","TARGET12","TARGET176","OTHER55","TARGET30","TARGET25","TARGET104","OTHER54","OTHER5","OTHER28","TARGET49","TARGET40","OTHER47","TARGET226","TARGET8","TARGET59","TARGET204","TARGET187","TARGET27","OTHER0","TARGET235","TARGET212","TARGET153","TARGET46","TARGET151","OTHER9","TARGET233","OTHER8","OTHER3","TARGET29","TARGET174","TARGET62","TARGET170","TARGET50","TARGET234","OTHER31","OTHER39","OTHER27","TARGET225","OTHER42","TARGET86","OTHER37"]}},"collectri":[["NFKB","GENE1",1.0,"ExTRI;HTRI;NTNU.Curated","25227886","default activation"],["TF0","GENE2",1.0,"ExTRI;TRRUST","29648508;2274839","regulon"],["TF2","GENE4",1.0,"DoRothEA-A;SIGNOR","17794492","default activation"],["TF3","GENE5",1.0,"NTNU.Curated;TRRUST","30512912;38294422","regulon"],["TF5","GENE7",1.0,"DoRothEA-A;ExTRI;TRRUST","37811468","default activation"],["TF6","GENE8",1.0,"ExTRI","13245454;2680991","regulon"],["AP1","GENE10",-1.0,"ExTRI;HTRI;TRRUST","15595509","default activation"],["NFKB","GENE11",1.0,"ExTRI","24735529;26759502","regulon"],["TF1","GENE13",1.0,"SIGNOR;TRRUST","20005059","default activation"],["TF2","GENE14",1.0,"HTRI;SIGNOR;TRRUST","33365588;33864342","regulon"],["TF4","GENE16",1.0,"HTRI;NTNU.Curated;SIGNOR","35260727","default activation"],["TF5","GENE17",-1.0,"ExTRI;SIGNOR","26599339;16363165","regulon"],["TF7","GENE19",1.0,"GEREDB","21441119","default activation"],["AP1","GENE20",1.0,"NTNU.Curated;TRRUST","24903470;37331386","regulon"],["TF0","GENE22",1.0,"ExTRI","30127555","default activation"],["TF1","GENE23",1.0,"DoRothEA-A","13903247;23112137","regulon"],["TF3","GENE25",1.0,"ExTRI;HTRI","38788725","default activation"],["TF4","GENE26",1.0,"DoRothEA-A;SIGNOR","38851791;24591103","regulon"],["TF6","GENE28",1.0,"ExTRI;HTRI","8547575","default activation"],["TF7","GENE29",1.0,"GEREDB","3598493;12573552","regulon"],["NFKB","GENE31",-1.0,"DoRothEA-A;HTRI","2067820","default activation"],["TF0","GENE32",1.0,"DoRothEA-A","22254324;37821372","regulon"],["TF2","GENE34",1.0,"ExTRI;SIGNOR","27111797","default activation"],["TF3","GENE35",1.0,"ExTRI;NTNU.Curated;SIGNOR","27593714;8727838","regulon"],["TF5","GENE37",1.0,"ExTRI;HTRI","38763501","default activation"],["TF6","GENE38",-1.0,"DoRothEA-A;SIGNOR","16352926;27015928","regulon"],["AP1","GENE40",1.0,"ExTRI;TRRUST","29909043","default activation"],["NFKB","GENE41",1.0,"DoRothEA-A;NTNU.Curated;TRRUST","4206728;16325847","regulon"],["TF1","GENE43",1.0,"HTRI;NTNU.Curated;TRRUST","12684997","default activation"],["TF2","GENE44",1.0,"ExTRI;HTRI","22953960;29515989","regulon"],["TF4","GENE46",1.0,"DoRothEA-A;ExTRI;HTRI","10896452","default activation"],["TF5","GENE47",1.0,"HTRI;NTNU.Curated","16967975;11207598","regulon"],["TF7","GENE49",1.0,"GEREDB","28553852","default activation"],["AP1","GENE50",1.0,"SIGNOR","18254288;37318562","regulon"],["TF0","GENE52",-1.0,"ExTRI","7589368","default activation"],["TF1","GENE53",1.0,"NTNU.Curated;SIGNOR","35423598;26948520","regulon"],["TF3","GENE55",1.0,"TRRUST","34032851","default activation"],["TF4","GENE56",1.0,"DoRothEA-A;HTRI;TRRUST","11915872;15270323","regulon"],["TF6","GENE58",1.0,"HTRI;SIGNOR","5914735","default activation"],["TF7","GENE59",-1.0,"GEREDB","37140880;39992318","regulon"],["NFKB","GENE61",1.0,"ExTRI;NTNU.Curated","10142986","default activation"],["TF0","GENE62",1.0,"HTRI","23943433;2012094","regulon"],["TF2","GENE64",1.0,"DoRothEA-A;SIGNOR;TRRUST","1349692","default activation"],["TF3","GENE65",1.0,"ExTRI","5576274;17212375","regulon"],["TF5","GENE67",1.0,"ExTRI;HTRI;TRRUST","23085573","default activation"],["TF6","GENE68",1.0,"HTRI","15503357;18507992","regulon"],["AP1","GENE70",1.0,"ExTRI;NTNU.Curated","18304215","default activation"],["NFKB","GENE71",1.0,"HTRI","14477295;23031795","regulon"],["TF1","GENE73",-1.0,"ExTRI;NTNU.Curated","10207568","default activation"],["TF2","GENE74",1.0,"DoRothEA-A;SIGNOR;TRRUST","26885978;33629567","regulon"],["TF4","GENE76",1.0,"NTNU.Curated;SIGNOR","13632575","default activation"],["TF5","GENE77",1.0,"SIGNOR;TRRUST","14663809;31650520","regulon"],["TF7","GENE79",1.0,"GEREDB","19563781","default activation"]]} \ No newline at end of file diff --git a/tests/data/activity/decoupler.npz b/tests/data/activity/decoupler.npz new file mode 100644 index 0000000..7935abd Binary files /dev/null and b/tests/data/activity/decoupler.npz differ diff --git a/tests/data/activity/progeny_model_human_full.csv b/tests/data/activity/progeny_model_human_full.csv new file mode 100644 index 0000000..130bab9 --- /dev/null +++ b/tests/data/activity/progeny_model_human_full.csv @@ -0,0 +1,301 @@ +"gene","pathway","weight","p.value" +"GENE1017","VEGF","-3.5932898738305035","0.090877414215356112" +"GENE4775","Hypoxia","1.1681542489617196","0.69201442133635283" +"GENE2177","PI3K","-1.4455062478847427","0.021786228520795703" +"GENE1533","PI3K","-1.2583293299354017","0.36137670860625803" +"GENE4567","JAK-STAT","-3.632412109484926","0.73228004993870854" +"GENE2347","JAK-STAT","-0.5185781967233597","0.31262304191477597" +"GENE0270","Estrogen","0.66926454695449888","0.4471530532464385" +"GENE4050","Trail","-2.8543352253309684","0.8254390680231154" +"GENE3379","JAK-STAT","3.8772564662855613","0.33179546520113945" +"GENE4065","p53","-1.5190604263999565","0.14402516721747816" +"GENE0597","WNT","-4.5575522831139672","0.30092075210995972" +"GENE1301","NFkB","-0.22810153293790239","0.16260179691016674" +"GENE0330","TNFa","4.7037133312723229","0.83947157836519182" +"GENE1799","TNFa","3.1926416065261463","0.66430747718550265" +"GENE3913","TGFb","2.5548910220713918","0.13886730559170246" +"GENE1749","WNT","1.5779417162655978","0.32588181388564408" +"GENE0037","VEGF","0.92293076455607737","0.83143021888099611" +"GENE1129","TGFb","-0.87613826765001468","0.82253910438157618" +"GENE0729","EGFR","-3.0156140185537255","0.14939992036670446" +"GENE0878","MAPK","-4.4458922626695561","0.42127968952991068" +"GENE0485","EGFR","-2.3573448848210248","0.44518265151418746" +"GENE4012","JAK-STAT","-3.565520447432891","0.88392737577669322" +"GENE2849","Androgen","-1.9758382785152979","0.41499732667580247" +"GENE2900","Androgen","1.4569773767510044","0.68989216187037528" +"GENE2378","VEGF","-1.7693698290772324","0.7169208771083504" +"GENE4650","WNT","-3.0768828343170496","0.012379169929772615" +"GENE1446","JAK-STAT","-2.088750814283125","0.037829448468983173" +"GENE2159","VEGF","-3.4362723806469733","0.090877414215356112" +"GENE3476","TGFb","1.6075150784285228","0.39882396743632853" +"GENE1948","p53","-3.0035743399293411","0.90021457592956722" +"GENE2580","EGFR","-0.29099814226170212","0.44490262842737138" +"GENE1530","PI3K","1.1589172246139656","0.75515309209004045" +"GENE4439","PI3K","2.4030511522622096","0.71147023956291378" +"GENE4136","p53","3.787821573094468","0.16675635031424463" +"GENE4633","p53","-3.5204526869922534","0.86914238915778697" +"GENE4344","PI3K","1.8490936053820888","0.089184535434469581" +"GENE1222","Hypoxia","-1.1130843359895091","0.16301366337575018" +"GENE2426","WNT","-0.3611682763879715","0.8493536883033812" +"GENE2087","TGFb","2.8948805706198746","0.62813034118153155" +"GENE2483","Hypoxia","-1.2142629056690524","0.20845995098352432" +"GENE2858","JAK-STAT","1.3587248605660838","0.67286082683131099" +"GENE1696","TGFb","-0.18711528077733322","0.96420569089241326" +"GENE0526","Trail","-0.98017258349762293","0.88173885201103985" +"GENE1069","Hypoxia","2.8213187665912898","0.96176468697376549" +"GENE0022","NFkB","-0.44914757950146933","0.464693185640499" +"GENE1128","p53","-0.42499097906262545","0.072877357248216867" +"GENE0983","VEGF","1.3927569474985433","0.87430875352583826" +"GENE1791","EGFR","1.830365010431581","0.19887280627153814" +"GENE3910","Androgen","-1.8467486316933874","0.53458975860849023" +"GENE1639","Trail","2.2937465258123728","0.10556219099089503" +"GENE4939","JAK-STAT","-1.2717300862957845","0.38399501726962626" +"GENE0465","TGFb","-1.772886616954545","0.74770592455752194" +"GENE1200","PI3K","-4.6662733694068761","0.69579099793918431" +"GENE3863","JAK-STAT","-0.29098160935196971","0.098391772015020251" +"GENE1134","p53","0.63392804761597943","0.61153179337270558" +"GENE0084","VEGF","-1.414939975612318","0.020925253862515092" +"GENE1895","PI3K","2.4843111161293381","0.6497122491709888" +"GENE3101","Hypoxia","1.2403043904493578","0.9783780702855438" +"GENE2300","TGFb","0.19980613517924004","0.84250445920042694" +"GENE3990","MAPK","3.6154069753498943","0.84278584201820195" +"GENE4971","TNFa","-3.004859957700361","0.38745216256938875" +"GENE1328","EGFR","0.57128093042247463","0.7930711021181196" +"GENE0557","JAK-STAT","1.6914139287529857","0.40619655908085406" +"GENE0287","Hypoxia","-1.9906852751984463","0.25334890512749553" +"GENE3217","Androgen","-0.51370824910330615","0.41930398903787136" +"GENE3702","MAPK","-0.11171208659691316","0.49549953290261328" +"GENE1522","WNT","-0.89001047973427727","0.85824595694430172" +"GENE0858","VEGF","0.13939020603559599","0.73226485354825854" +"GENE4672","p53","-0.3093434120336328","0.97442849050275981" +"GENE0990","Androgen","-1.6625269188180014","0.10005490970797837" +"GENE3175","Estrogen","1.5230887046898229","0.83259083703160286" +"GENE0316","Trail","-1.1530138650602546","0.45055417786352336" +"GENE0733","Androgen","-1.2527364563784713","0.85045913583599031" +"GENE4907","Estrogen","0.96267065111814853","0.56975154369138181" +"GENE2330","WNT","3.3905421678852394","0.96154251391999424" +"GENE1167","MAPK","-3.5224525878253905","0.95475073391571641" +"GENE3514","TNFa","0.39602603021109711","0.28377950889989734" +"GENE3992","p53","0.79469819760162086","0.73960649780929089" +"GENE1706","NFkB","0.058450989962151811","0.070742575684562325" +"GENE0501","VEGF","5.1205467774418665","0.90011954680085182" +"GENE3788","Hypoxia","2.5142554231691028","0.13342057261615992" +"GENE0536","Estrogen","-1.0690753712308785","0.55588607769459486" +"GENE3286","JAK-STAT","-1.2504548571500738","0.60040803253650665" +"GENE3747","EGFR","1.8276973736992417","0.13296103896573186" +"GENE0029","JAK-STAT","2.0143990693556533","0.5093123372644186" +"GENE3662","JAK-STAT","1.4385836465436352","0.36704990756697953" +"GENE1942","Trail","-1.2094233217085735","0.0033045473974198103" +"GENE1820","Estrogen","1.0781088128730596","0.04797986033372581" +"GENE2281","TGFb","-0.15366177106090009","0.88204845017753541" +"GENE1317","EGFR","3.6998391206524714","0.82351704011671245" +"GENE4669","WNT","-1.7098151026927779","0.4130309009924531" +"GENE1966","VEGF","0.065274589786724471","0.92173321801237762" +"GENE0369","NFkB","-2.0501189615070903","0.47740293340757489" +"GENE2499","p53","-1.964498151130474","0.4749944235663861" +"GENE4182","EGFR","0.0082039139136266554","0.65783606702461839" +"GENE0355","WNT","-0.46685435582449203","0.16332045383751392" +"GENE1073","TGFb","-0.9977764382072255","0.095194371184334159" +"GENE0361","VEGF","3.0994259257680756","0.86151479231193662" +"GENE1340","NFkB","0.17499383300972787","0.23987916973419487" +"GENE1266","TNFa","2.6374022611300978","0.55540413199923933" +"GENE1841","Androgen","-1.9624482381388955","0.8494065951090306" +"GENE2866","TNFa","-0.49124517528938111","0.34964758064597845" +"GENE4343","PI3K","-2.8078676738380288","0.9757385952398181" +"GENE0751","p53","2.8817862946198063","0.86940553016029298" +"GENE0219","MAPK","-1.962719981874498","0.75412405095994473" +"GENE0135","VEGF","2.9484898078518458","0.22367342002689838" +"GENE4207","MAPK","-1.9823944905441429","0.7698164654430002" +"GENE0532","Estrogen","-0.1889946891510772","0.87471358431503177" +"GENE4504","TNFa","-5.7502833676483114","0.043793108081445098" +"GENE3123","Trail","-0.49373220167419624","0.19976070709526539" +"GENE0912","WNT","0.029488970649717811","0.85374235827475786" +"GENE3428","TGFb","-3.8381754065936944","0.65225412836298347" +"GENE2178","WNT","-0.57562748740019598","0.86933201923966408" +"GENE4455","Androgen","-0.69327489080978111","0.81363658746704459" +"GENE2153","MAPK","-3.6791771692937685","0.36144763790071011" +"GENE1148","Androgen","1.7971778818465793","0.090895846020430326" +"GENE1101","Trail","-2.4257100219561445","0.57465529046021402" +"GENE1242","VEGF","-0.43792846344952119","0.65473041357472539" +"GENE3682","MAPK","1.1285363213023383","0.82220477634109557" +"GENE1218","TGFb","-1.0508687513991366","0.45557551505044103" +"GENE4115","EGFR","1.4887484499080645","0.98432889836840332" +"GENE0273","Hypoxia","0.25796350639295063","0.75039837812073529" +"GENE0418","Trail","2.9765485137093761","0.13355568703263998" +"GENE0867","JAK-STAT","-1.3253639022994732","0.21517351269721985" +"GENE4782","NFkB","-2.3213100029400859","0.84837953769601882" +"GENE4499","MAPK","0.71754846899473679","0.46534118545241654" +"GENE3821","Estrogen","-0.38969276838566241","0.91576779028400779" +"GENE1611","Hypoxia","-0.59056401602832664","0.91655773832462728" +"GENE0818","Trail","0.99328084743988454","0.81985458382405341" +"GENE2652","Trail","0.96982557633239752","0.56690115761011839" +"GENE4730","Trail","0.037569001059502713","0.61547716124914587" +"GENE0664","Estrogen","1.2695491293538355","0.79480028990656137" +"GENE3210","Hypoxia","1.5088881752101402","0.43969654850661755" +"GENE0719","WNT","1.6671780697160985","0.4125872531440109" +"GENE0500","TGFb","1.9315225924353046","0.56548528512939811" +"GENE3045","VEGF","2.5877599358219157","0.15164783201180398" +"GENE2809","Estrogen","-0.27310204169483254","0.80746084544807673" +"GENE0423","WNT","-0.88027737626987823","0.49564136634580791" +"GENE0421","VEGF","-2.4545678289198039","0.14050918305292726" +"GENE0989","EGFR","-0.47530613669064808","0.11028927913866937" +"GENE4236","MAPK","-1.8537163134086199","0.11079106340184808" +"GENE4222","Estrogen","0.82247071160180441","0.0047142794355750084" +"GENE3598","VEGF","-0.39772915363620476","0.23840435524471104" +"GENE3580","Hypoxia","-1.1148992787209899","0.87882287916727364" +"GENE3700","Hypoxia","-1.9543142448523863","0.077218657359480858" +"GENE2319","TGFb","0.12147049745777252","0.13853856013156474" +"GENE1154","JAK-STAT","-1.1944589591810104","0.047524574911221862" +"GENE2625","JAK-STAT","-2.5178973949500194","0.033918873406946659" +"GENE3523","WNT","-2.8200944031388318","0.91608901834115386" +"GENE0504","NFkB","2.2026081588443169","0.84020039392635226" +"GENE4454","TGFb","-1.3544840525612754","0.17887141648679972" +"GENE0785","JAK-STAT","-1.5243470151013887","0.49526794510893524" +"GENE3796","p53","-0.58349720404897076","0.30695852707140148" +"GENE3912","JAK-STAT","-1.1503770587434636","0.27437696186825633" +"GENE1572","Estrogen","-0.88788373637391538","0.12824385543353856" +"GENE4401","TGFb","-0.6251408879250322","0.42938191327266395" +"GENE1833","NFkB","-1.2060088527897073","0.69594496372155845" +"GENE2461","Hypoxia","-2.1878694400583609","0.33506071846932173" +"GENE2624","NFkB","1.4294123826436538","0.7012464334256947" +"GENE4225","Androgen","-0.21762452646137986","0.84053491475060582" +"GENE0309","MAPK","-2.8875958706705749","0.88263200665824115" +"GENE2922","Estrogen","1.6122465271844266","0.76658935006707907" +"GENE4078","p53","-3.4796701566065424","0.32202762761153281" +"GENE0441","Hypoxia","-0.80264061899993022","0.042117007775232196" +"GENE2904","Hypoxia","-0.57516497750265272","0.89270992227829993" +"GENE3189","TGFb","-1.8768148001027249","0.63810514518991113" +"GENE4405","Hypoxia","0.57533427759070033","0.55449311621487141" +"GENE0470","JAK-STAT","-3.0108022486560189","0.76632994809187949" +"GENE3686","Androgen","3.0385940266251503","0.096861294470727444" +"GENE1360","JAK-STAT","0.7348187119016909","0.7480164875742048" +"GENE1822","MAPK","3.399724817619354","0.87521798769012094" +"GENE1790","JAK-STAT","1.2883939545538357","0.0076685955282300711" +"GENE0349","PI3K","-3.3756020572733942","0.93491698545403779" +"GENE3144","TNFa","1.2952919887380847","0.40696565806865692" +"GENE0894","TNFa","0.89758845356279526","0.78876338968984783" +"GENE4686","TGFb","2.0526043165203767","0.97612762451171875" +"GENE1956","Trail","2.1499564456210005","0.33340254588983953" +"GENE0474","Hypoxia","0.91661922600068946","0.78721482097171247" +"GENE4862","TNFa","1.2631735161722231","0.089360916055738926" +"GENE0455","MAPK","-1.160932796985255","0.043699902016669512" +"GENE4856","Hypoxia","3.1683842844068759","0.51493373815901577" +"GENE3306","PI3K","-3.5279858801541004","0.048089327523484826" +"GENE3556","MAPK","-3.7612439404804574","0.08919612760655582" +"GENE3193","Hypoxia","-2.5834380880571213","0.68832552153617144" +"GENE0015","p53","1.8193408926058519","0.034838533727452159" +"GENE3366","MAPK","-2.2155113641426172","0.43091092444956303" +"GENE4158","Androgen","-0.76824774943124596","0.75929405121132731" +"GENE4486","p53","0.16546966742272209","0.8317969546187669" +"GENE1668","TNFa","-0.96776494803004764","0.38246251689270139" +"GENE1059","TNFa","-4.1694822508235259","0.61555481888353825" +"GENE4477","MAPK","2.3351739278510184","0.34498665039427578" +"GENE4983","JAK-STAT","-0.15365154144462004","0.53227877407334745" +"GENE2597","JAK-STAT","1.0608427905461397","0.2600091069471091" +"GENE4096","Androgen","0.0098175916219315068","0.5728606223128736" +"GENE2012","PI3K","-1.0604628597407106","0.90584667841903865" +"GENE4084","MAPK","3.0297333089939973","0.56602187850512564" +"GENE2110","EGFR","1.6309192711343303","0.056486817775294185" +"GENE1172","Estrogen","-3.0130437260005145","0.10211374540813267" +"GENE0797","Trail","-2.3143603436560141","0.32834185590036213" +"GENE4960","p53","2.6031098745660621","0.99060050724074244" +"GENE1596","NFkB","-1.8112815501440742","0.71436533937230706" +"GENE4072","MAPK","0.031126961859490567","0.078974183881655335" +"GENE4430","PI3K","-2.8560103930976006","0.61421644943766296" +"GENE2079","TNFa","1.3759594031206619","0.49323367280885577" +"GENE3372","Estrogen","1.1883930028976788","0.2690976548474282" +"GENE2762","TGFb","-0.54063048800816838","0.29311086283996701" +"GENE0610","JAK-STAT","3.1081521522839202","0.47527830023318529" +"GENE3354","PI3K","-1.0214849163322735","0.96407854626886547" +"GENE1265","EGFR","-0.58368547139933857","0.42363210208714008" +"GENE3068","NFkB","2.2028763300729453","0.84409709577448666" +"GENE4906","TGFb","0.91733042939757292","0.95382735133171082" +"GENE2265","TGFb","1.7038160786892143","0.2864735247567296" +"GENE2840","p53","2.9323998377369875","0.54084939789026976" +"GENE2156","TNFa","0.48772658981201461","0.58137831790372729" +"GENE4897","MAPK","1.2988438678281391","0.25628689443692565" +"GENE1760","Estrogen","-1.2822363121168041","0.87808396830223501" +"GENE4664","TNFa","0.9289195413302247","0.88230238761752844" +"GENE4059","VEGF","-0.61840089428672362","0.98733468470163643" +"GENE4075","Estrogen","2.5520414178026347","0.10162090114317834" +"GENE3506","EGFR","-0.095222439990609642","0.61877500219270587" +"GENE2964","p53","0.060202331937387707","0.70473376987501979" +"GENE3421","MAPK","-0.0071605400117456018","0.14858843083493412" +"GENE3586","NFkB","-4.0691452861666271","0.89081059163436294" +"GENE4212","MAPK","-1.9856399251776142","0.16267030010931194" +"GENE2043","PI3K","3.9277161121115953","0.12186646950431168" +"GENE3070","VEGF","1.4130575460377051","0.97755685728043318" +"GENE1463","Trail","0.079349959433477418","0.62711026542820036" +"GENE1221","Trail","-0.31703717925867603","0.019199430011212826" +"GENE3292","WNT","-0.32451510238486297","0.82916414458304644" +"GENE0462","Androgen","0.87764382330591384","0.99887747527100146" +"GENE1957","EGFR","1.5613985840995386","0.86809551622718573" +"GENE2283","PI3K","-1.9624112440550623","0.39691107487305999" +"GENE4049","Androgen","-0.2831714636654406","0.93624178250320256" +"GENE1498","JAK-STAT","-2.5124979059333934","0.32477289601229131" +"GENE2221","TNFa","-0.85806735197022321","0.9724475615657866" +"GENE3937","TGFb","1.0175525324544445","0.5625984538346529" +"GENE4767","VEGF","-2.8937794858037784","0.57932903780601919" +"GENE4503","EGFR","2.0390256573982257","0.82469035359099507" +"GENE2033","JAK-STAT","2.3570939518139817","0.19844447751529515" +"GENE0731","JAK-STAT","-0.020517530415935147","0.6189896350260824" +"GENE4281","WNT","0.53724974227193911","0.22456461493857205" +"GENE2813","PI3K","2.6840577449170371","0.025223730131983757" +"GENE3805","JAK-STAT","-1.167212885955035","0.75650512473657727" +"GENE4589","NFkB","-1.8740017816248422","0.63815694465301931" +"GENE3914","MAPK","-2.3043141121582069","0.057868913048878312" +"GENE0966","Trail","-1.950366861657195","0.67379781324416399" +"GENE2492","JAK-STAT","-1.4228271965297734","0.69154477072879672" +"GENE1191","JAK-STAT","0.74382495236651924","0.74522895971313119" +"GENE2017","PI3K","-1.8870288346092958","0.1240618706215173" +"GENE3774","Androgen","-0.54873461613481778","0.15117178671061993" +"GENE0291","WNT","0.30893698825036991","0.17743062530644238" +"GENE3979","Androgen","-2.5773064030421042","0.00886946776881814" +"GENE0247","TGFb","2.8382046376134387","0.63378864317201078" +"GENE2482","p53","2.6474011988140242","0.37275088508613408" +"GENE3388","Estrogen","-3.6099515476320505","0.27772004902362823" +"GENE3418","Androgen","-0.9681390530282491","0.17250850074924529" +"GENE2984","Hypoxia","-0.74642356013178124","0.20371114136651158" +"GENE2722","JAK-STAT","0.33817180442268335","0.22187088173814118" +"GENE2884","NFkB","-2.3459672056447545","0.25343150459229946" +"GENE0056","Hypoxia","-0.7523934739218886","0.17009768937714398" +"GENE0934","Androgen","1.5662552792870947","0.92167676403187215" +"GENE3153","JAK-STAT","1.9823426088629994","0.34531345847062767" +"GENE4425","TNFa","0.45856894962340461","0.085109209176152945" +"GENE4590","Androgen","0.67370189735198294","0.37781780515797436" +"GENE4738","Androgen","-1.2944639983793602","0.54334351955913007" +"GENE2966","VEGF","-0.8864133641331492","0.78701092605479062" +"GENE3552","VEGF","2.323503521526189","0.32031352771446109" +"GENE2152","EGFR","0.13010202077468541","0.83803851832635701" +"GENE4257","p53","-1.3649351606046325","0.9984704852104187" +"GENE2432","Hypoxia","3.2686833169863823","0.21344299241900444" +"GENE0127","Hypoxia","-1.1696984888122226","0.43729649134911597" +"GENE4356","JAK-STAT","2.2598720011870674","0.37872999627143145" +"GENE0717","MAPK","1.1624020865531568","0.96557670854963362" +"GENE4156","Hypoxia","0.75858613415313092","0.64111650222912431" +"GENE2496","p53","-0.62148175187074695","0.80613533384166658" +"GENE4277","EGFR","1.7727800026184866","0.91906218882650137" +"GENE1452","Hypoxia","-3.2837294998166171","0.058622448006644845" +"GENE3997","EGFR","-1.9771274830171399","0.47764285560697317" +"GENE4387","Estrogen","-0.48800685898190849","0.69688278832472861" +"GENE0676","Hypoxia","0.3121138514455794","0.04467150941491127" +"GENE1993","EGFR","0.20410391656847657","0.75685431691817939" +"GENE4707","TGFb","-0.57491634326566088","0.59167372481897473" +"GENE0148","TGFb","-0.58638977275645476","0.51014046301133931" +"GENE4246","WNT","0.93895411611443813","0.45260176644660532" +"GENE0121","TGFb","-1.3251770245744232","0.025481966091319919" +"GENE0169","VEGF","-4.3807453315897726","0.9618436093442142" +"GENE0598","Trail","0.0077093433605890833","0.98762743966653943" +"GENE1491","TNFa","1.7258856581222659","0.63435955811291933" +"GENE4204","Hypoxia","1.9604467181712448","0.4255413468927145" +"GENE2815","WNT","-0.58344954220847134","0.26662590517662466" +"GENE2578","VEGF","-0.12202695166626844","0.080569487996399403" +"GENE1580","Trail","3.0237368023689539","0.3301415634341538" +"GENE3869","VEGF","-1.2872181298458492","0.8940501029137522" +"GENE1115","Estrogen","-0.26190095887113168","0.30667519848793745" +"GENE3616","NFkB","0.97042290681760623","0.40242384118027985" +"GENE3551","EGFR","-1.2903648690940215","0.57379139750264585" +"GENE4147","VEGF","-0.92791243928075129","0.59698838437907398" +"GENE2167","Androgen","1.1344993939453125","0.57001713756471872" +"GENE2513","TGFb","-1.4468156675190016","0.21807128796353936" +"GENE1783","Hypoxia","0.91176910741744754","0.82012674445286393" diff --git a/tests/data/activity/progeny_model_mouse_full.csv b/tests/data/activity/progeny_model_mouse_full.csv new file mode 100644 index 0000000..c971f92 --- /dev/null +++ b/tests/data/activity/progeny_model_mouse_full.csv @@ -0,0 +1,301 @@ +"gene","pathway","weight","p.value" +"Gene2403","EGFR","1.3391856401385303","0.79338595201261342" +"Gene0039","WNT","-1.1641396136821005","0.0043177721090614796" +"Gene3833","WNT","0.64767461038260854","0.69813990965485573" +"Gene1552","EGFR","-1.5716739716343564","0.50696648471057415" +"Gene3176","PI3K","0.43579719682022849","0.26944410311989486" +"Gene0721","Trail","0.95331893725698935","0.63464980851858854" +"Gene4754","MAPK","0.13990889446153901","0.99765660241246223" +"Gene1961","PI3K","-2.0819252608457588","0.50507434387691319" +"Gene1432","NFkB","2.3328279676385053","0.93323299963958561" +"Gene0386","Estrogen","-0.1463494048843578","0.19120888714678586" +"Gene2955","WNT","-2.5701548893266475","0.48476254451088607" +"Gene4910","EGFR","-1.3775909110034874","0.63412564806640148" +"Gene4460","WNT","0.18210496085088093","0.20230313204228878" +"Gene4896","Hypoxia","-1.6853836196147627","0.42960191075690091" +"Gene1044","Trail","-3.6776298054510477","0.67068845382891595" +"Gene2843","TGFb","-2.0651192973593084","0.8215878619812429" +"Gene0327","JAK-STAT","-0.22871777912246022","0.35869119339622557" +"Gene4039","Hypoxia","0.13147766389737187","0.61827155598439276" +"Gene3800","EGFR","2.9352640526453402","0.34706960222683847" +"Gene1311","Hypoxia","-1.3821291578323354","0.75596656510606408" +"Gene3330","Trail","-1.8105495575545003","0.56542690470814705" +"Gene0837","Estrogen","-1.418015243255502","0.62758803297765553" +"Gene1205","Estrogen","1.9286261433012026","0.39221455692313612" +"Gene3565","p53","-1.7712699049862923","0.6583818884100765" +"Gene3237","NFkB","-0.29794105794078302","0.68310624733567238" +"Gene1768","VEGF","4.1350209022491677","0.77632555551826954" +"Gene4038","MAPK","-2.5931876027708087","0.76765642268583179" +"Gene2296","EGFR","-1.6937027023545774","0.011093436041846871" +"Gene0435","VEGF","-0.94232505143965328","0.23081328999251127" +"Gene1693","JAK-STAT","-3.249862908923872","0.69771443726494908" +"Gene3316","JAK-STAT","0.36425454588296879","0.9039487901609391" +"Gene1852","VEGF","-4.1896956435859103","0.088952073827385902" +"Gene0838","Trail","-0.81959368096061902","0.66270021651871502" +"Gene3212","EGFR","-1.7829733564790502","0.64175726054236293" +"Gene2490","Trail","-1.4655160144125516","0.93388468748889863" +"Gene4474","PI3K","2.226220964935584","0.90570346661843359" +"Gene4703","Trail","1.3651007600129228","0.028259973740205169" +"Gene4649","PI3K","3.5037730198055597","0.32696621166542172" +"Gene4630","NFkB","2.0330745129704466","0.86635087220929563" +"Gene3977","WNT","0.28018898409272042","0.20949795097112656" +"Gene3878","TGFb","-1.2905198708803227","0.48985729296691716" +"Gene0556","Trail","0.42641142316142189","0.46562649216502905" +"Gene3322","WNT","0.55103078787062088","0.53206355520524085" +"Gene3003","WNT","-0.59627115506918327","0.94356501707807183" +"Gene4346","Hypoxia","-2.9907653875442701","0.53625835804268718" +"Gene0306","TNFa","1.4432853939367984","0.54024756886065006" +"Gene2435","WNT","-1.6546160318842691","0.63881783653050661" +"Gene0945","Trail","-2.5386670016501549","0.55912937084212899" +"Gene2772","p53","0.067346925496499996","0.061127950670197606" +"Gene4826","JAK-STAT","-0.83595210328091341","0.29238359327428043" +"Gene1279","PI3K","1.8980982410349088","0.48362484388053417" +"Gene0749","WNT","-0.84541972115279596","0.2988622565753758" +"Gene4286","TNFa","2.2346381837906684","0.53375536995008588" +"Gene0224","PI3K","0.57890837761888447","0.44438073644414544" +"Gene2507","WNT","0.40095618446777642","0.79267228557728231" +"Gene4220","Trail","-0.45244897017962704","0.055983771570026875" +"Gene3448","NFkB","2.3863548907425778","0.36502901115454733" +"Gene2095","p53","-0.91226282568453798","0.89141795155592263" +"Gene3268","EGFR","-1.3242312415898176","0.51923995860852301" +"Gene0219","WNT","0.41898193141225959","0.36864099744707346" +"Gene4904","EGFR","0.40251980526772174","0.41756269102916121" +"Gene0626","EGFR","3.9976612700209539","0.022696062223985791" +"Gene4360","Estrogen","-1.3949932904729438","0.59048856003209949" +"Gene2765","MAPK","1.6070803092236181","0.49303546291776001" +"Gene2240","TGFb","-0.21956847723500622","0.10290711070410907" +"Gene4588","Trail","3.0690510992825226","0.40479038725607097" +"Gene4105","p53","1.1336226630048853","0.052468776004388928" +"Gene3721","TNFa","1.2981686719281653","0.03656733687967062" +"Gene0534","JAK-STAT","0.57185025704861958","0.23109614686109126" +"Gene3022","Androgen","-1.8292578737564062","0.97980273026041687" +"Gene2423","Trail","-1.2184893737309215","0.046293978812173009" +"Gene1838","Trail","1.115299615903566","0.1319506443105638" +"Gene1526","Estrogen","-2.0688553343701286","0.92844044556841254" +"Gene2389","TGFb","2.459646255004762","0.087940061232075095" +"Gene0401","Estrogen","-2.5845135962184473","0.94258554046973586" +"Gene3279","Trail","-2.3692357826856738","0.67411416117101908" +"Gene4780","Trail","-2.2885546059951265","0.0038287381175905466" +"Gene3929","TNFa","0.18459271626452436","0.84925125818699598" +"Gene0034","PI3K","2.1882893772776857","0.51559084351174533" +"Gene3728","TGFb","-3.1248613091405062","0.42234981525689363" +"Gene0116","NFkB","-0.81139897900039715","0.8596117568667978" +"Gene2046","Trail","0.14697270663719031","0.5899088061414659" +"Gene3288","Trail","-3.1710105558216259","0.063867842545732856" +"Gene2751","MAPK","-0.51660697476779494","0.13518742262385786" +"Gene0877","JAK-STAT","-0.39548857523687214","0.41111476812511683" +"Gene2374","VEGF","2.077438073774438","0.83028127579018474" +"Gene0347","JAK-STAT","1.3346162231339898","0.38713270286098123" +"Gene3984","Estrogen","-2.6192085727174654","0.62279525934718549" +"Gene1302","MAPK","-2.2800105538558011","0.60527244489639997" +"Gene3755","MAPK","-1.4972489385680487","0.54022703599184752" +"Gene4178","TGFb","-4.2280779704382239","0.58436949132010341" +"Gene0571","TNFa","-2.3396083380355397","0.77280472894199193" +"Gene4866","Estrogen","1.6070744291500669","0.69821955659426749" +"Gene0855","VEGF","-0.81623004064143512","0.84109317953698337" +"Gene3580","TNFa","1.506844801008882","0.97751217242330313" +"Gene4688","Hypoxia","1.5932907324453425","0.23192704585380852" +"Gene4385","EGFR","-1.5181363042656051","0.857306067366153" +"Gene2672","WNT","-0.92230853745065999","0.68470452073961496" +"Gene1882","Androgen","3.8087531161889161","0.30019149486906826" +"Gene2206","p53","-0.69726393614118365","0.16122143156826496" +"Gene0068","Trail","0.43766188673838857","0.7672343710437417" +"Gene4618","PI3K","2.2899091233228241","0.98711163341067731" +"Gene0823","Estrogen","-0.24709137021253633","0.8580974314827472" +"Gene0173","VEGF","0.29478583845068251","0.67606980749405921" +"Gene0391","PI3K","-2.4819323663269413","0.81171457259915769" +"Gene2350","p53","4.0084869287736806","0.4857147631701082" +"Gene0505","PI3K","-1.5439591406836553","0.46057287929579616" +"Gene4475","NFkB","-2.3577781471612935","0.31191033381037414" +"Gene3983","PI3K","-1.1393597479785564","0.020581638906151056" +"Gene2289","EGFR","3.5096416909321886","0.41271494468674064" +"Gene3444","NFkB","-3.5507386283181646","0.9799998844973743" +"Gene3553","Trail","-0.41953382688887825","0.13540277513675392" +"Gene0935","MAPK","-2.4680881625366013","0.73067593690939248" +"Gene1316","Hypoxia","-2.060959563305536","0.52530405716970563" +"Gene3327","PI3K","0.053771269901854497","0.96159941633231938" +"Gene4927","EGFR","0.95208395553800196","0.72023034188896418" +"Gene3936","EGFR","3.3395581364680522","0.91061638621613383" +"Gene2878","JAK-STAT","-0.79069335763142279","0.3038501632399857" +"Gene2408","Hypoxia","0.87267852284168967","0.62866796413436532" +"Gene2438","Estrogen","-5.5357415614832925","0.2082374426536262" +"Gene0090","Estrogen","0.28615581602193418","0.8504987028427422" +"Gene2836","Estrogen","3.2960449933896037","0.051622653380036354" +"Gene1623","MAPK","1.0141360183131944","0.76327592344023287" +"Gene1514","JAK-STAT","-0.1848070848312704","0.71528993314132094" +"Gene4473","EGFR","-0.30649864559041651","0.76775719318538904" +"Gene2600","Hypoxia","-1.1683642644986325","0.63072845665737987" +"Gene2499","Trail","0.95178884071617986","0.81652353960089386" +"Gene1689","MAPK","0.87970970931564463","0.4163177867885679" +"Gene3510","PI3K","2.9723901261070278","0.93772162706591189" +"Gene3733","TNFa","-4.239116401339472","0.2907832725904882" +"Gene1954","NFkB","0.043824573867454149","0.11401614802889526" +"Gene3387","p53","-1.1871237722780283","0.60193589655682445" +"Gene4142","Trail","-0.55593772289535826","0.025367458583787084" +"Gene4344","Trail","-0.57985563523805317","0.08453310769982636" +"Gene1511","JAK-STAT","-1.4581358459007145","0.33361064945347607" +"Gene0773","EGFR","0.58708296190525233","0.070408669067546725" +"Gene3524","Hypoxia","-1.4344610622518505","0.70507984771393239" +"Gene3375","Hypoxia","-3.1303054569343267","0.074199503986164927" +"Gene3650","Estrogen","2.1697600344454626","0.32501377933658659" +"Gene2145","TGFb","-0.52347318852039826","0.35894192080013454" +"Gene0040","VEGF","-0.40043358057324951","0.38833261653780937" +"Gene0041","EGFR","-0.088691623214894141","0.7854433711618185" +"Gene4239","NFkB","-2.3086950398636366","0.60434823972173035" +"Gene0113","WNT","1.2331151719457922","0.76258975313976407" +"Gene3890","TNFa","0.6550024293438963","0.23586044274270535" +"Gene4651","JAK-STAT","-2.7346480789581498","0.28598467679694295" +"Gene3152","NFkB","0.55424898322578564","0.31755666900426149" +"Gene2255","EGFR","-0.51236632683718353","0.92190670827403665" +"Gene3158","VEGF","1.64474611239503","0.45043682237155735" +"Gene0133","JAK-STAT","-0.48077769991673958","0.12594790128059685" +"Gene2690","TNFa","0.55124198288740422","0.1439860921818763" +"Gene4211","TNFa","2.6834505110760878","0.31107065500691533" +"Gene1641","JAK-STAT","3.6033132051263879","0.975288923131302" +"Gene4527","TNFa","-2.1537165306677073","0.23176327371038496" +"Gene0794","Estrogen","-0.23078681040228835","0.038202836643904448" +"Gene4111","p53","-2.7354715080239296","0.56325698620639741" +"Gene4387","Androgen","0.95510508457335686","0.81846295855939388" +"Gene4612","TNFa","0.61214600763393567","0.03995634475722909" +"Gene1813","VEGF","-1.1390513600399184","0.030119340401142836" +"Gene3159","EGFR","-1.9268103159790493","0.88195783784613013" +"Gene2705","WNT","4.7238976211823331","0.096223073778674006" +"Gene1830","JAK-STAT","-0.037408015428999311","0.88629000913351774" +"Gene1359","Trail","0.5954265671108665","0.87995914625935256" +"Gene1581","JAK-STAT","-1.709260065027302","0.83587610209360719" +"Gene2904","JAK-STAT","3.0152084822469516","0.55724372901022434" +"Gene4778","WNT","2.4380318666546161","0.46732663409784436" +"Gene2579","EGFR","0.23399481770886263","0.76411396893672645" +"Gene0667","VEGF","0.18600788296920454","0.8762132846750319" +"Gene3435","p53","1.7064585073184542","0.70195243135094643" +"Gene4811","TGFb","-0.50334625887205742","0.68679866543971002" +"Gene4611","Estrogen","-0.29622485937512366","0.63687040167860687" +"Gene0798","VEGF","-3.7357802858360531","0.83118250803090632" +"Gene2511","TNFa","0.9196797387653376","0.36510155024006963" +"Gene3652","MAPK","5.6239830078719946","0.57055697869509459" +"Gene1778","Trail","0.63903267267693087","0.63015224202536047" +"Gene2889","VEGF","-2.9704450947276846","0.61495260032825172" +"Gene1749","EGFR","1.6846105177352693","0.036818969761952758" +"Gene0738","VEGF","1.1936489513716986","0.017285815672948956" +"Gene2522","Trail","0.73245698872792242","0.068011863855645061" +"Gene2002","Estrogen","0.18630624774291074","0.29527199151925743" +"Gene4298","TGFb","1.1902422202195948","0.12670403812080622" +"Gene2214","JAK-STAT","-0.93358383736041517","0.84471472515724599" +"Gene1792","EGFR","-3.205518539646008","0.58289262908510864" +"Gene2756","p53","1.1387772554587428","0.54033178859390318" +"Gene2056","EGFR","0.5142307652532494","0.56440987950190902" +"Gene0759","WNT","2.1655795374543469","0.11147763696499169" +"Gene1560","NFkB","-2.4380020282449948","0.50326519645750523" +"Gene2194","JAK-STAT","-3.0611084982452428","0.16893640090711415" +"Gene1258","Hypoxia","-3.3042235488433418","0.33043641829863191" +"Gene3070","WNT","1.5724985257807289","0.28917464683763683" +"Gene4107","EGFR","2.1171947121214467","0.87186171230860054" +"Gene2228","JAK-STAT","-1.4773297801783458","0.98307592235505581" +"Gene2617","VEGF","-0.27770494279313662","0.44973988016135991" +"Gene3920","WNT","-2.8397520203429347","0.42873723129741848" +"Gene0699","NFkB","1.7371106265162244","0.29871173482388258" +"Gene3277","Trail","-3.2499788986468916","0.25954933161847293" +"Gene0340","MAPK","0.28632691218662715","0.79335058596916497" +"Gene2169","PI3K","1.4209593449806248","0.018193165073171258" +"Gene1249","TGFb","-0.11094007242014431","0.034577911254018545" +"Gene3792","TGFb","0.34362640684656059","0.028594020986929536" +"Gene3426","Hypoxia","-1.2781996042594332","0.56842501601204276" +"Gene3670","VEGF","-1.3254214627675824","0.8891935006249696" +"Gene3101","WNT","0.15752343130463498","0.55485696136020124" +"Gene3814","PI3K","-0.10284120539413723","0.19732345477677882" +"Gene2560","PI3K","-0.52154825387101933","0.79243055800907314" +"Gene3413","NFkB","3.1325363767631549","0.94751293351873755" +"Gene2235","Androgen","-0.74403021999852093","0.071240220684558153" +"Gene3993","Hypoxia","3.4838888785939019","0.37103899521753192" +"Gene1698","VEGF","-2.1977130815309054","0.66976512712426484" +"Gene1376","NFkB","1.7326568880679298","0.0099983566906303167" +"Gene2265","MAPK","1.0695766722051379","0.74989388138055801" +"Gene3845","TGFb","-2.788455550729918","0.98584624193608761" +"Gene4018","Trail","2.9921279642880969","0.27390965772792697" +"Gene2565","Estrogen","-2.4848549740346182","0.13951965235173702" +"Gene3956","p53","-2.0533756343308394","0.97640424338169396" +"Gene1098","Hypoxia","-1.1293218806611234","0.30630163196474314" +"Gene2166","NFkB","-2.1460639207442624","0.56942411349155009" +"Gene2041","PI3K","-0.71539742398799933","0.5382075272500515" +"Gene2934","p53","-1.1586675688445391","0.96145927417092025" +"Gene2538","TGFb","-0.18323187760481008","0.45217612222768366" +"Gene4053","NFkB","-0.72176175025232236","0.37867098022252321" +"Gene4971","PI3K","-4.8882975524098944","0.064423084491863847" +"Gene4992","WNT","-0.83860203528198818","0.6710256733931601" +"Gene0814","Hypoxia","-0.7354307708562664","0.06544710579328239" +"Gene4724","p53","-1.1264926516457396","0.20196890737861395" +"Gene1000","Trail","-2.7795140696983123","0.91858324501663446" +"Gene3682","VEGF","-1.2648721967553842","0.95688528008759022" +"Gene0664","Androgen","3.2218658386403658","0.23934163362719119" +"Gene2539","EGFR","-0.4786714931458525","0.40693932888098061" +"Gene3189","JAK-STAT","-1.190556865820821","0.79614728805609047" +"Gene2018","VEGF","-0.32473015547477796","0.88062859629280865" +"Gene0619","VEGF","-5.4060278308539349","0.95615916862152517" +"Gene1144","TGFb","-2.4223664843154693","0.62719422089867294" +"Gene2182","Trail","2.2746366619482545","0.21794080291874707" +"Gene1636","JAK-STAT","0.56147275828707932","0.525597776286304" +"Gene1243","Trail","-0.91294034312204575","0.23981459438800812" +"Gene2282","EGFR","0.034264585395479115","0.94064981117844582" +"Gene2185","TGFb","4.4123863201516729","0.023136063246056437" +"Gene4258","Androgen","-4.462256930662071","0.63471232960000634" +"Gene4333","p53","1.5047082175211839","0.97127534286119044" +"Gene3531","NFkB","1.2037166191117994","0.10732453688979149" +"Gene2663","Estrogen","1.2496365398255482","0.22476256988011301" +"Gene2877","EGFR","1.8184644201911702","0.14622274180874228" +"Gene1218","VEGF","-0.29447760994785299","0.35273172636516392" +"Gene3561","Estrogen","1.955777035858475","0.33344115596264601" +"Gene4921","WNT","0.90427720719367455","0.086290895473212004" +"Gene2129","Trail","1.9002585349049301","0.97114906017668545" +"Gene2520","EGFR","-2.4246580218553069","0.77094365796074271" +"Gene4149","WNT","0.95213611567613421","0.030109912855550647" +"Gene0309","PI3K","2.5321809620440638","0.946407520910725" +"Gene1965","TNFa","0.21329437035417853","0.47144383820705116" +"Gene4522","p53","-2.0207978240723841","0.58993875584565103" +"Gene1742","VEGF","2.3577848482877699","0.98722024005837739" +"Gene1827","TGFb","-3.6764558542569299","0.34438837179914117" +"Gene0310","Androgen","-2.4244367300294654","0.66187479509972036" +"Gene0616","Hypoxia","-1.544577801329168","0.047501199645921588" +"Gene1261","MAPK","-0.70015793631053902","0.88099701562896371" +"Gene1092","p53","3.3624270021108704","0.9319800054654479" +"Gene3200","JAK-STAT","-4.581311225064459","0.95491374540142715" +"Gene1411","Estrogen","-0.0095086307560029032","0.75537035707384348" +"Gene4086","EGFR","-3.1123532179867075","0.82855475321412086" +"Gene2211","WNT","1.3997268555472857","0.48365692188963294" +"Gene4119","NFkB","0.60281465248574673","0.72542186360806227" +"Gene4948","Trail","-3.3266273700584614","0.88577184756286442" +"Gene0547","Hypoxia","-0.98265103185157077","0.062430157093331218" +"Gene1242","TNFa","-0.71963926065372297","0.90294478391297162" +"Gene0756","Androgen","3.338300874514518","0.028252076590433717" +"Gene1817","Estrogen","1.8826984019695803","0.32196285529062152" +"Gene2531","JAK-STAT","3.2456119522372635","0.12814937764778733" +"Gene1818","JAK-STAT","1.7822315876569537","0.11014191387221217" +"Gene1782","NFkB","-1.062030019757916","0.51334223989397287" +"Gene1198","p53","2.0409663768221362","0.83229316095821559" +"Gene1163","Androgen","2.230569984127317","0.44570896681398153" +"Gene4278","TGFb","0.39119342022025816","0.75591149181127548" +"Gene1558","Androgen","2.6971406000239546","0.89972952473908663" +"Gene4547","EGFR","0.27179543833406589","0.3080472273286432" +"Gene0240","WNT","3.1089063674518513","0.55685402313247323" +"Gene1619","WNT","-0.27779343254114069","0.99919467885047197" +"Gene4755","Estrogen","-0.2413604764514353","0.20209424523636699" +"Gene4390","p53","1.5045564234960818","0.0053999614901840687" +"Gene4975","Androgen","0.6625761263142107","0.30408637248910964" +"Gene2112","Androgen","-3.2046902616852369","0.49675341485999525" +"Gene3309","VEGF","-1.5456495665410446","0.028377162525430322" +"Gene2300","JAK-STAT","-2.2456942455525226","0.8726026697549969" +"Gene0909","TNFa","0.78166919890661735","0.93407864565961063" +"Gene4168","EGFR","-2.2427600491664741","0.93087594117969275" +"Gene1929","WNT","-0.13306906721966349","0.046528774313628674" +"Gene0649","TGFb","1.9912509340608446","0.8354293555021286" +"Gene2602","TGFb","-4.5760004362748345","0.83588953572325408" +"Gene0406","p53","-1.6034300053750028","0.55896683293394744" +"Gene2973","VEGF","1.6415939737319114","0.88115051831118762" +"Gene0965","TNFa","-2.0749221121687746","0.78491733130067587" +"Gene0054","Trail","1.3423352809924016","0.90440573450177908" +"Gene2964","Estrogen","1.4041134769490213","0.71143480599857867" +"Gene0777","p53","-0.62446456244285831","0.5755039828363806" +"Gene4634","Hypoxia","-0.23798080956619685","0.48907258710823953" +"Gene2272","VEGF","0.44884966413876964","0.20428211474791169" +"Gene3257","WNT","-2.2476631471581192","0.19022295670583844" +"Gene2928","JAK-STAT","2.1634866401930108","0.8383846185170114" +"Gene2595","Androgen","2.5230852472178391","0.88805390545167029" diff --git a/tests/data/activity/progeny_package.tar.gz b/tests/data/activity/progeny_package.tar.gz new file mode 100644 index 0000000..3877b6e Binary files /dev/null and b/tests/data/activity/progeny_package.tar.gz differ diff --git a/tests/data/activity/progeny_top3.csv b/tests/data/activity/progeny_top3.csv new file mode 100644 index 0000000..441e782 --- /dev/null +++ b/tests/data/activity/progeny_top3.csv @@ -0,0 +1,44 @@ +"gene","pathway" +"GENE1017","VEGF" +"GENE2177","PI3K" +"GENE4065","p53" +"GENE1301","NFkB" +"GENE4650","WNT" +"GENE1446","JAK-STAT" +"GENE2159","VEGF" +"GENE1128","p53" +"GENE1639","Trail" +"GENE0084","VEGF" +"GENE1706","NFkB" +"GENE3747","EGFR" +"GENE1942","Trail" +"GENE1820","Estrogen" +"GENE0355","WNT" +"GENE1073","TGFb" +"GENE1340","NFkB" +"GENE4504","TNFa" +"GENE1148","Androgen" +"GENE0989","EGFR" +"GENE4222","Estrogen" +"GENE2319","TGFb" +"GENE2625","JAK-STAT" +"GENE0441","Hypoxia" +"GENE3686","Androgen" +"GENE1790","JAK-STAT" +"GENE4862","TNFa" +"GENE0455","MAPK" +"GENE3306","PI3K" +"GENE0015","p53" +"GENE2110","EGFR" +"GENE4072","MAPK" +"GENE4075","Estrogen" +"GENE1221","Trail" +"GENE2813","PI3K" +"GENE3914","MAPK" +"GENE0291","WNT" +"GENE3979","Androgen" +"GENE4425","TNFa" +"GENE1452","Hypoxia" +"GENE0676","Hypoxia" +"GENE0121","TGFb" +"GENE2578","VEGF" diff --git a/tests/test_activity.py b/tests/test_activity.py new file mode 100644 index 0000000..8f0a115 --- /dev/null +++ b/tests/test_activity.py @@ -0,0 +1,532 @@ +"""Tests of the activities of regulators and pathways: ULM, MLM and the weighted +aggregate. + +The activities are compared with decoupler 2.2.0's, which validation/activity.py wrote +to tests/data/activity, its permutations replayed; with values derived by hand on +small networks; with the exact distribution of a weighted mean over every permutation +of a few genes; and with invariants that a wrong implementation would break. The bar +plot is compared with decoupler's and the heatmap read back from what it draws. +""" + +import doctest +import itertools +import json +import math +from pathlib import Path + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal + +from cytepool import activity +from cytepool.activity import Activities, mlm, ulm, weighted_aggregate +from cytepool.cube import Cube +from cytepool.design import Design +from cytepool.differential_expression import limma_voom +from cytepool.gene_sets import GeneSets +from cytepool.networks import Network +from cytepool.normalization import filter_by_expression, log_cpm, tmm_factors +from cytepool.plotting import plot_activities, plot_top_activities +from cytepool.stats.linear_models import fit_linear_model +from cytepool.table import Table + +DATA = Path(__file__).parent / "data" / "activity" +FIXTURE = json.loads((DATA / "decoupler.json").read_text()) +ARRAYS = np.load(DATA / "decoupler.npz") +EXACT = 1e-10 # least squares and sums in closed form +CASES = {case["name"]: case for case in FIXTURE["cases"]} + + +def _network(name: str) -> Network: + return Network(Table(FIXTURE["networks"][name])) + + +def _data(name: str) -> tuple[np.ndarray, list[str], list[str]]: + described = FIXTURE["data"][name] + return ARRAYS[name], described["profiles"], described["genes"] + + +def _missing() -> np.ndarray: + """The samples with the values that decoupler did not see missing: those of the + first two samples outside the genes of their own profiles.""" + values, _, genes = _data("samples") + missing = values.copy() + for row in range(2): + observed = set(_data(f"missing_{row}")[2]) + missing[row, [gene not in observed for gene in genes]] = np.nan + return missing + + +def _scaled(ours: np.ndarray, theirs: np.ndarray) -> float: + assert_array_equal(np.isnan(ours), np.isnan(theirs)) + return float(np.nanmax(np.abs(ours - theirs)) / np.nanmax(np.abs(theirs))) + + +def _relative(ours: np.ndarray, theirs: np.ndarray) -> float: + assert_array_equal(np.isnan(ours), np.isnan(theirs)) + return float(np.nanmax(np.abs(ours - theirs) / np.abs(theirs))) + + +def _run(case: dict) -> tuple[Activities, list[int], list[int]]: + """CytePool's activities of a case, the rows decoupler scored, and the columns of + decoupler's sources.""" + function = {"ulm": ulm, "mlm": mlm}[case["method"]] + values, profiles, genes = _data(case["data"]) + rows = list(range(len(profiles))) + if case["data"].startswith("missing_"): + values, profiles, genes = _missing(), *_data("samples")[1:] + rows = [int(case["data"][-1])] + result = function( + values, + _network(case["network"]), + gene_ids=genes, + sample_ids=profiles, + min_targets=case["tmin"], + ) + found = FIXTURE["found"][case["name"]] + columns = [result.sources.index(source) for source in found["sources"]] + return result, rows, columns + + +LINEAR = [ + pytest.param(case, id=case["name"]) + for case in FIXTURE["cases"] + if case["method"] in ("ulm", "mlm") +] + + +# Against decoupler. + + +@pytest.mark.parametrize("case", LINEAR) +def test_the_linear_models_are_those_of_decoupler(case: dict) -> None: + result, rows, columns = _run(case) + name = case["name"] + pick = np.ix_(rows, columns) + + # The sources scored in these profiles are decoupler's, after its tmin. + scored = ~np.all(np.isnan(result.scores[rows]), axis=0) + assert sorted(np.array(result.sources)[scored]) == sorted( + FIXTURE["found"][name]["sources"] + ) + assert _scaled(result.scores[pick], ARRAYS[f"{name}/raw_scores"]) < EXACT + assert _scaled(result.estimates[pick], ARRAYS[f"{name}/estimates"]) < EXACT + if case["method"] == "ulm": + assert _relative(result.p_values[pick], ARRAYS[f"{name}/raw_p"]) < EXACT + # Benjamini and Hochberg's over the sources of each profile, by scipy. + bh = ARRAYS[f"{name}/bh"] + assert _relative(result.adjusted_p_values[pick], bh) < EXACT + # decoupler keeps them in single precision. + single = result.adjusted_p_values[pick].astype(np.float32) + assert_array_equal(single, ARRAYS[f"{name}/padj"].astype(np.float32)) + else: + # decoupler's own are 1 - cdf; scipy's upper tail of its t-statistics. + assert _relative(result.p_values[pick], ARRAYS[f"{name}/t_sf"]) < EXACT + + +@pytest.mark.parametrize("name", ["weighted mean", "weighted sum"]) +def test_the_weighted_aggregate_is_that_of_decoupler(name: str) -> None: + case = CASES[name] + values, profiles, genes = _data(case["data"]) + statistic = "sum" if case["options"].get("fun") == "wsum" else "mean" + result = weighted_aggregate( + values, + _network(case["network"]), + gene_ids=genes, + statistic=statistic, + n_permutations=0, + ) + columns = [result.sources.index(s) for s in FIXTURE["found"][name]["sources"]] + assert _scaled(result.scores[:, columns], ARRAYS[f"{name}/raw_scores"]) < EXACT + assert_array_equal(result.scores, result.estimates) + assert np.isnan(result.p_values).all() + + +class _Replay(np.random.Generator): + """A generator whose shuffles give the permutations decoupler drew.""" + + def __init__(self, orders: np.ndarray) -> None: + super().__init__(np.random.PCG64(0)) + self.orders = iter(orders) + + def shuffle(self, x: object, axis: int = 0) -> None: + x[:] = next(self.orders) # type: ignore[index] + + +def test_normalized_scores_are_decouplers_given_its_permutations() -> None: + name = "weighted mean, 100 permutations" + case = CASES[name] + values, profiles, genes = _data(case["data"]) + found = FIXTURE["found"][name] + # decoupler's permutations are of the genes in its own order, the zero genes out. + order = [genes.index(gene) for gene in found["genes"]] + permutations = ARRAYS[f"{name}/permutations"].astype(np.int64) + result = weighted_aggregate( + values[:, order], + _network(case["network"]), + gene_ids=found["genes"], + n_permutations=permutations.shape[0], + rng=_Replay(permutations), + ) + columns = [result.sources.index(s) for s in found["sources"]] + assert _scaled(result.scores[:, columns], ARRAYS[f"{name}/raw_scores"]) < EXACT + # The p-values differ from decoupler's on purpose (see the module docstring). + assert np.all((result.p_values > 0.0) & (result.p_values <= 1.0)) + + +def test_the_bar_plot_is_decouplers() -> None: + pytest.importorskip("matplotlib") + import matplotlib.figure + + result, _, _ = _run(CASES["ulm"]) + ax = plot_top_activities( + result, "s4", top=15, ax=matplotlib.figure.Figure().subplots() + ) + names = [label.get_text() for label in ax.get_yticklabels()] + assert names == FIXTURE["found"]["ulm"]["bar_names"] + widths = np.array([bar.get_width() for bar in ax.patches]) + assert _scaled(widths, ARRAYS["ulm/bar_widths"]) < EXACT + + +# Values derived by hand. + +# Four genes, one source of two targets, A: g1 (weight 1) and g2 (weight 2). With +# y = (1, 2, 3, 5): x = (1, 2, 0, 0), mean 3/4; y's mean 11/4. Sxx = 11/4, Sxy = +# -13/4, Syy = 35/4; slope -13/11, residual sum of squares 35/4 - 169/44 = 54/11 on 2 +# degrees of freedom, t = slope / sqrt(54/11 / (2 * 11/4)) = -13 / sqrt(108). With 2 +# degrees of freedom P(|T| > t) = 1 - t / sqrt(t^2 + 2) = 1 - 13 / sqrt(385). +FOUR = ["g1", "g2", "g3", "g4"] +Y = np.array([1.0, 2.0, 3.0, 5.0]) +PAIR = Network({"A": {"g1": 1.0, "g2": 2.0}}) + + +def test_ulm_of_a_source_with_two_targets_on_three_samples() -> None: + # The second sample is 2y + 7, which doubles the slope and keeps t; the third, + # -y, negates both. + values = np.array([Y, 2.0 * Y + 7.0, -Y]) + result = ulm(values, PAIR, gene_ids=FOUR, min_targets=2) + + t = -13.0 / math.sqrt(108.0) + assert_allclose(result.scores[:, 0], [t, t, -t], rtol=1e-14) + assert_allclose(result.estimates[:, 0], [-13 / 11, -26 / 11, 13 / 11], rtol=1e-14) + p = 1.0 - 13.0 / math.sqrt(385.0) + assert_allclose(result.p_values[:, 0], [p, p, p], rtol=1e-13) + # One source per profile: nothing to adjust. + assert_allclose(result.adjusted_p_values, result.p_values, rtol=0) + assert_array_equal(result.n_targets, [[2], [2], [2]]) + + +def test_mlm_of_a_network_of_one_source_is_ulm() -> None: + values = np.array([Y, 2.0 * Y + 7.0, -Y]) + single = mlm(values, PAIR, gene_ids=FOUR, min_targets=2) + assert_allclose(single.scores[:, 0], -13.0 / math.sqrt(108.0) * np.array( + [1.0, 1.0, -1.0]), rtol=1e-13) # fmt: skip + assert_allclose(single.estimates[:, 0], [-13 / 11, -26 / 11, 13 / 11], rtol=1e-13) + + +def test_the_weighted_mean_and_sum_by_hand() -> None: + values = np.array([Y]) + mean = weighted_aggregate(values, PAIR, gene_ids=FOUR, min_targets=2, + n_permutations=0) # fmt: skip + total = weighted_aggregate(values, PAIR, gene_ids=FOUR, min_targets=2, + statistic="sum", n_permutations=0) # fmt: skip + # (1 * 1 + 2 * 2) / (|1| + |2|) and 1 * 1 + 2 * 2. + assert_allclose(mean.scores, [[5.0 / 3.0]], rtol=1e-15) + assert_allclose(total.scores, [[5.0]], rtol=1e-15) + + +def test_ulm_with_weights_of_one_sign_is_the_two_sample_t_test() -> None: + # Regressing on a 0/1 indicator gives Student's pooled two-sample t-statistic of + # the targets against the other genes. + rng = np.random.default_rng(4) + y = rng.normal(size=(2, 30)) + genes = [f"g{j}" for j in range(30)] + targets = genes[:7] + result = ulm(y, {"set": targets}, gene_ids=genes) + inside, outside = y[:, :7], y[:, 7:] + pooled = np.sqrt( + (((inside - inside.mean(axis=1, keepdims=True)) ** 2).sum(axis=1) + + ((outside - outside.mean(axis=1, keepdims=True)) ** 2).sum(axis=1)) / 28 + ) # fmt: skip + t = (inside.mean(axis=1) - outside.mean(axis=1)) / ( + pooled * math.sqrt(1 / 7 + 1 / 23) + ) + assert_allclose(result.scores[:, 0], t, rtol=1e-12) + # A GeneSets is a network of the same sets. + as_sets = ulm(y, GeneSets({"set": targets}), gene_ids=genes) + assert_array_equal(as_sets.scores, result.scores) + + +def test_permutations_estimate_the_exact_null_distribution() -> None: + # Five genes, a source on two of them: over the 120 permutations of the genes, + # each equally likely, the weighted mean has an exact mean and standard deviation, + # which 5,000 random permutations estimate within Monte Carlo error. + y = np.array([[0.3, 2.0, -1.0, 4.0, 1.5]]) + genes = [f"g{j}" for j in range(5)] + network = {"A": {"g0": 1.0, "g1": -0.5}} + nulls = np.array( + [(y[0, p[0]] * 1.0 - 0.5 * y[0, p[1]]) / 1.5 + for p in itertools.permutations(range(5))] + ) # fmt: skip + observed = (0.3 - 1.0) / 1.5 + exact = (observed - nulls.mean()) / nulls.std(ddof=0) + tails = min(np.mean(nulls >= observed), np.mean(nulls <= observed)) + result = weighted_aggregate(y, network, gene_ids=genes, min_targets=2, + n_permutations=5000, rng=1) # fmt: skip + assert_allclose(result.estimates, [[observed]], rtol=1e-15) + # Monte Carlo error: the mean and sd of 5,000 draws, about 2% of the score. + assert_allclose(result.scores[0, 0], exact, rtol=0.05) + assert abs(result.p_values[0, 0] - min(1.0, 2 * tails)) < 0.03 + + +# Invariants. + +RNG = np.random.default_rng(11) +GENES = [f"G{j}" for j in range(60)] +VALUES = RNG.normal(size=(4, 60)) +NETWORK = Network( + { + f"S{k}": {gene: float(RNG.choice([-2.0, -1.0, 0.5, 1.0])) for gene in + RNG.choice(GENES, 8, replace=False)} + for k in range(6) + } +) # fmt: skip +FUNCTIONS = [ + pytest.param(ulm, id="ulm"), + pytest.param(mlm, id="mlm"), + pytest.param( + lambda *a, **k: weighted_aggregate(*a, n_permutations=0, **k), + id="weighted_aggregate", + ), +] + + +@pytest.mark.parametrize("function", FUNCTIONS) +def test_the_order_of_genes_sources_and_samples_does_not_matter(function) -> None: + base = function(VALUES, NETWORK, gene_ids=GENES) + genes = RNG.permutation(60) + shuffled = function(VALUES[:, genes], NETWORK, gene_ids=[GENES[j] for j in genes]) + assert_allclose(shuffled.scores, base.scores, rtol=1e-12, atol=1e-13) + # The sources keep the order of the network. + reversed_network = Network(NETWORK.edges.take(np.arange(len(NETWORK.edges))[::-1])) + flipped = function(VALUES, reversed_network, gene_ids=GENES) + assert flipped.sources == base.sources[::-1] + assert_allclose(flipped.scores[:, ::-1], base.scores, rtol=1e-12, atol=1e-13) + rows = [2, 0, 3, 1] + by_rows = function(VALUES[rows], NETWORK, gene_ids=GENES) + assert_allclose(by_rows.scores, base.scores[rows], rtol=1e-12, atol=1e-13) + + +@pytest.mark.parametrize("function", FUNCTIONS) +def test_negating_a_sources_weights_negates_its_activity(function) -> None: + edges = NETWORK.edges + weights = np.where(edges["source"] == "S2", -edges["weight"], edges["weight"]) + negated = Network(edges.with_columns({"weight": weights})) + base = function(VALUES, NETWORK, gene_ids=GENES) + flipped = function(VALUES, negated, gene_ids=GENES) + column = base.sources.index("S2") + assert_allclose(flipped.scores[:, column], -base.scores[:, column], rtol=1e-12) + assert_allclose(flipped.estimates[:, column], -base.estimates[:, column], + rtol=1e-12) # fmt: skip + others = [j for j in range(len(base.sources)) if j != column] + assert_allclose(flipped.scores[:, others], base.scores[:, others], rtol=1e-12, + atol=1e-13) # fmt: skip + + +def test_scaling_a_profile_scales_ulms_estimates_and_keeps_its_t() -> None: + base = ulm(VALUES, NETWORK, gene_ids=GENES) + scaled = ulm(3.0 * VALUES - 5.0, NETWORK, gene_ids=GENES) + assert_allclose(scaled.estimates, 3.0 * base.estimates, rtol=1e-12) + assert_allclose(scaled.scores, base.scores, rtol=1e-11) + assert_allclose(scaled.p_values, base.p_values, rtol=1e-10) + + +def test_min_targets_and_missing_values() -> None: + # S0 has 8 targets; with 3 of them missing in the first sample, it has 5 there. + targets = list(NETWORK["S0"]) + values = VALUES.copy() + values[0, [GENES.index(gene) for gene in targets[:3]]] = np.nan + result = ulm(values, NETWORK, gene_ids=GENES, min_targets=6) + column = result.sources.index("S0") + assert result.n_targets[0, column] == 5 and result.n_targets[1, column] == 8 + assert np.isnan(result.scores[0, column]) and np.isfinite(result.scores[1, column]) + # The first sample scored alone over its observed genes gives the same score. + alone = ulm(values[:1, ~np.isnan(values[0])], NETWORK, + gene_ids=[g for g, v in zip(GENES, values[0], strict=True) + if not np.isnan(v)], min_targets=5) # fmt: skip + again = ulm(values, NETWORK, gene_ids=GENES, min_targets=5) + assert_allclose(again.scores[0], alone.scores[0], rtol=1e-12) + with pytest.raises(ValueError, match="no source of the network has 9 targets"): + ulm(values, NETWORK, gene_ids=GENES, min_targets=9) + + +def test_genes_zero_everywhere_and_without_ids_are_left_out() -> None: + values = np.hstack([VALUES, np.zeros((4, 3))]) + genes = [*GENES, "Z1", "Z2", None] + zeros = ulm(values, Network({**NETWORK, "Z": {"Z1": 1.0, "Z2": 1.0}}), + gene_ids=genes) # fmt: skip + base = ulm(VALUES, NETWORK, gene_ids=GENES) + assert "Z" not in zeros.sources + assert_allclose(zeros.scores, base.scores, rtol=1e-12) + + +def test_edges_of_weight_zero_are_not_targets() -> None: + edges = NETWORK.edges + padded = Network( + Table( + { + "source": [*edges["source"].tolist(), "S0", "S0"], + "target": [*edges["target"].tolist(), "G59", "G58"], + "weight": [*edges["weight"].tolist(), 0.0, 0.0], + } + ) + ) + base = ulm(VALUES, NETWORK, gene_ids=GENES) + padded_result = ulm(VALUES, padded, gene_ids=GENES) + assert_allclose(padded_result.scores, base.scores, rtol=1e-12) + assert_array_equal(padded_result.n_targets, base.n_targets) + + +# Profiles of cubes and of differential-expression results. + + +def _cube() -> Cube: + rng = np.random.default_rng(2) + means = np.exp(rng.uniform(2.0, 6.0, 60)) + counts = rng.negative_binomial(8, 8 / (8 + means), (2, 6, 60)).astype(np.uint32) + counts[1, :, 50:] = 0 # not expressed in the second group + return Cube( + {"counts": counts}, + samples={"sample_id": [f"S{i}" for i in range(6)], + "condition": ["ctrl", "stim"] * 3}, + groups={"group_id": ["B", "T"]}, + features={"feature_id": GENES}, + n_cells=np.array([[30, 30]] * 5 + [[5, 30]]), + ) # fmt: skip + + +def test_a_cubes_group_is_scored_as_its_log_cpm() -> None: + cube = _cube() + result = ulm(cube, NETWORK, group="B") + # Its samples with at least 10 cells, over the genes filterByExpr keeps. + counts = cube.layer("counts", "B")[:5] + keep = filter_by_expression(counts, group=np.zeros(5)) + values = log_cpm(counts[:, keep], norm_factors=tmm_factors(counts[:, keep]), + prior_count=2.0) # fmt: skip + expected = ulm(values, NETWORK, gene_ids=np.array(GENES)[keep].tolist()) + assert_allclose(result.scores, expected.scores, rtol=1e-12) + assert result.profiles["profile_id"].tolist() == [f"S{i}" for i in range(5)] + assert result.profiles["condition"].tolist() == ["ctrl", "stim"] * 2 + ["ctrl"] + assert result.settings["group"] == "B" and result.settings["min_targets"] == 5 + # The scores take a design built from the profiles, as pathway scores do. + fit = fit_linear_model(result.scores, Design(result.profiles, "~ condition")) + assert fit.coefficients.shape == (len(result.sources), 2) + + +def test_each_group_and_contrast_of_a_result_is_a_profile() -> None: + cube = _cube() + result = limma_voom(cube, Design(cube.samples, "~ condition"), + contrasts={"stim": "conditionstim"}, min_cells=0) # fmt: skip + found = ulm(result, NETWORK, min_targets=3) + assert found.profiles["profile_id"].tolist() == ["B", "T"] + assert found.profiles["contrast"].tolist() == ["stim", "stim"] + for row, group in enumerate(["B", "T"]): + table = result.table(group, "stim") + alone = ulm(table, NETWORK, min_targets=3) + assert alone.profiles["profile_id"].tolist() == ["t"] + columns = [found.sources.index(s) for s in alone.sources] + assert_allclose(found.scores[row, columns], alone.scores[0], rtol=1e-12) + # T tests fewer genes, so its sources have fewer targets. + assert (found.n_targets[1] <= found.n_targets[0]).all() + assert (found.n_targets[1] < found.n_targets[0]).any() + only = ulm(result, NETWORK, group="T", min_targets=3) + assert only.profiles["profile_id"].tolist() == ["T"] + # A mapping of gene id to statistic is one profile too. + table = result.table("B", "stim") + mapping = dict(zip(table["feature_id"].tolist(), table["t"].tolist(), strict=True)) + as_mapping = ulm(mapping, NETWORK, min_targets=3) + assert_allclose(as_mapping.scores[0], found.scores[0], rtol=1e-12) + + +# The record and its table. + + +def test_the_record_and_its_table() -> None: + result = ulm(VALUES, NETWORK, gene_ids=GENES, sample_ids=["a", "b", "c", "d"]) + assert repr(result) == "Activities(method='ulm', n_profiles=4, n_sources=6)" + assert result.sources == NETWORK.sources + for array in (result.scores, result.estimates, result.p_values, + result.adjusted_p_values, result.n_targets): # fmt: skip + assert not array.flags.writeable + table = result.table() + assert table.columns == ("source", "profile_id", "n_targets", "score", + "estimate", "p_value", "adjusted_p_value") # fmt: skip + assert table["source"].tolist()[:5] == ["S0"] * 4 + ["S1"] + assert table["profile_id"].tolist()[:5] == ["a", "b", "c", "d", "a"] + assert_array_equal(table["score"], result.scores.T.ravel()) + # The network kept: the sources scored and their targets among the genes. + assert result.network.edges.equals(NETWORK.edges) + + +def test_mlm_refuses_sources_it_cannot_tell_apart() -> None: + twins = Network({**{s: dict(NETWORK[s]) for s in NETWORK}, + "TWIN": dict(NETWORK["S1"])}) # fmt: skip + with pytest.raises(ValueError, match="cannot tell the sources 'TWIN' apart"): + mlm(VALUES, twins, gene_ids=GENES) + # Three sources of one target each and an intercept use up four genes. + with pytest.raises(ValueError, match="more genes than sources"): + mlm(np.array([Y]), {"A": ["g1"], "B": ["g2"], "C": ["g3"]}, gene_ids=FOUR, + min_targets=1) # fmt: skip + + +def test_messages() -> None: + with pytest.raises(ValueError, match="no target of the network is among the genes"): + ulm(VALUES, {"A": ["X1", "X2"]}, gene_ids=GENES) + with pytest.raises(TypeError, match="network must be a Network"): + ulm(VALUES, 3, gene_ids=GENES) + with pytest.raises(ValueError, match="gene_ids is needed with an array"): + ulm(VALUES, NETWORK) + with pytest.raises(ValueError, match="contrast applies to the statistics"): + ulm(VALUES, NETWORK, gene_ids=GENES, contrast="stim") + with pytest.raises(ValueError, match="gene_ids applies to an array or a cube"): + ulm({"G1": 1.0, "G2": 2.0}, NETWORK, gene_ids=GENES) + with pytest.raises(ValueError, match="min_targets must be at least 1"): + ulm(VALUES, NETWORK, gene_ids=GENES, min_targets=0) + with pytest.raises(ValueError, match="n_permutations must be 0, for no"): + weighted_aggregate(VALUES, NETWORK, gene_ids=GENES, n_permutations=1) + with pytest.raises(ValueError, match="statistic must be one of 'mean', 'sum'"): + weighted_aggregate(VALUES, NETWORK, gene_ids=GENES, statistic="median") + with pytest.raises(ValueError, match="To score a samples x genes table"): + ulm(Table({"G1": [1.0, 2.0], "G2": [3.0, 4.0]}), NETWORK) + with pytest.raises(ValueError, match="data holds infinite values"): + ulm(np.where(VALUES > 2, np.inf, VALUES), NETWORK, gene_ids=GENES) + + +# The heatmap. + + +def test_the_heatmap_draws_the_scores_of_the_top_sources() -> None: + pytest.importorskip("matplotlib") + import matplotlib.figure + + result = ulm(VALUES, NETWORK, gene_ids=GENES, sample_ids=["a", "b", "c", "d"]) + ax = plot_activities(result, top=3, color_by=["x", "y", "x", "y"], + ax=matplotlib.figure.Figure().subplots()) # fmt: skip + cells = next(image for image in ax.get_images() if image.get_gid() == "scores") + drawn = np.ma.filled(cells.get_array().astype(float), np.nan) + largest = np.max(np.abs(result.scores), axis=0) + shown = np.sort(np.argsort(-largest)[:3]) + # The profiles grouped by label, in the order the labels first appear. + assert_array_equal(drawn, result.scores[[0, 2, 1, 3]][:, shown].T) + labels = [label.get_text() for label in ax.get_yticklabels()] + assert labels == [result.sources[j] for j in shown] + with pytest.raises(TypeError, match="activities must be an Activities"): + plot_activities(result.scores) + with pytest.raises(ValueError, match="no source 'X' among the activities"): + plot_activities(result, sources=["X"]) + with pytest.raises(ValueError, match="the activities have 4 profiles"): + plot_top_activities(result) + + +def test_the_examples_run() -> None: + assert doctest.testmod(activity, raise_on_error=False).failed == 0 diff --git a/tests/test_masked_inputs.py b/tests/test_masked_inputs.py index ed8b5ee..6d78296 100644 --- a/tests/test_masked_inputs.py +++ b/tests/test_masked_inputs.py @@ -45,6 +45,7 @@ import cytepool from cytepool._masked_arrays import reject_masked +from cytepool.activity import ulm from cytepool.composition import propeller from cytepool.cube import Cube, CubeCollection, from_bulk, from_cells from cytepool.design import Design, make_contrasts @@ -54,6 +55,7 @@ from cytepool.io.anndata import to_anndata, to_mudata from cytepool.io.zarr import open_array, write_array, write_group from cytepool.meta_analysis import differential_expression_meta_analysis +from cytepool.networks import Network from cytepool.normalization import cpm, filter_by_expression, log_cpm from cytepool.over_representation import over_representation from cytepool.pathway_scores import gsva @@ -510,6 +512,23 @@ def _pathway_scores() -> Any: return gsva(log_cpm(COUNTS), {"set": genes[:3], "other": genes[3:]}, gene_ids=genes) +@functools.cache +def _activities() -> Any: + """ULM activities of two sources in the four samples of COUNTS.""" + genes = [f"g{j}" for j in range(COUNTS.shape[1])] + network = {"A": {"g1": 1.0, "g2": -1.0, "g5": 0.5}, "B": ["g0", "g3", "g4"]} + return ulm(log_cpm(COUNTS), network, gene_ids=genes, + sample_ids=SAMPLE_TABLE["sample_id"], min_targets=2) # fmt: skip + + +# Two sources of a network, over the genes of COUNTS or the features of a cube. +NETWORK = {"A": {"g1": 1.0, "g2": -1.0, "g5": 0.5}, "B": ["g0", "g3", "g4"]} +FEATURE_NETWORK = { + "A": ["feature_1", "feature_2", "feature_5"], + "B": ["feature_0", "feature_3", "feature_4"], +} + + @functools.cache def _composition_result() -> Any: """propeller of the cell numbers of four samples, with one contrast.""" @@ -1648,6 +1667,94 @@ def _forest_arguments(context: Context) -> dict[str, Any]: ) for name in ("gsva", "ssgsea", "combined_z_scores", "plage") }, + # Values, gene ids and sample ids of an array, and a network as a mapping, which + # Network reads as its edges; a cube's options and a result's are refused. + **{ + f"cytepool.activity.{name}": Entry( + lambda c, options=options: { + "data": c.expression, + "network": NETWORK, + "gene_ids": [f"g{j}" for j in range(7)] + [None], + "sample_ids": ["S1", "S2", "S3", "S4"], + "min_targets": 2, + **options, + }, + { + "data": "NaN", + "network": "None or NaN", + "gene_ids": "None", + "sample_ids": None, + }, + options=("min_targets", *options), + keys={"network": "A"}, + names={"network": "edges"}, + ) + for name, options in ( + ("ulm", {"n_threads": 1}), + ("mlm", {}), + ( + "weighted_aggregate", + {"statistic": "sum", "n_permutations": 10, "rng": 1, "n_threads": 1}, + ), + ) + }, + # A cube's group, its genes named by a column of its features table. + **{ + f"cytepool.activity.{name}[cube]": Entry( + lambda c: { + "data": _bulk_cube(), + "network": FEATURE_NETWORK, + "gene_ids": "feature_id", + "group": "bulk", + "min_cells": 1, + "expression_filter": False, + "min_targets": 2, + }, + {}, + options=("gene_ids", "group", "min_cells", "expression_filter"), + objects=("data", "network"), + ) + for name in ("ulm", "mlm", "weighted_aggregate") + }, + # The statistics of a group and contrast of a differential-expression result. + **{ + f"cytepool.activity.{name}[result]": Entry( + lambda c: { + "data": _tested(c), + "network": FEATURE_NETWORK, + "group": "bulk", + "contrast": "stim", + "id_column": "feature_id", + "statistic_column": "t", + "min_targets": 2, + }, + {}, + options=("group", "contrast", "id_column", "statistic_column"), + objects=("data", "network"), + ) + for name in ("ulm", "mlm", "weighted_aggregate") + }, + # Edges as a mapping of source to targets, and text that the network keeps. + "cytepool.networks.Network": Entry( + lambda c: { + "edges": {"A": {"g1": 1.0, "g2": -1.0}, "B": ["g3"]}, + "name": "mine", + "version": "1", + "url": "https://example.org/network.csv", + "licence": "CC0", + "citation": "a paper", + "file": "network.csv", + "sha256": "0" * 64, + }, + {"edges": "None or NaN"}, + options=("name", "version", "url", "licence", "citation", "file", "sha256"), + keys={"edges": "A"}, + ), + "cytepool.networks.Network.select": Entry( + lambda c: {"sources": ["B", "A"]}, + {"sources": None}, + instance=lambda c: Network({"A": ["g1", "g2"], "B": ["g3"]}), + ), # n and axis go through operator.index, which reads a 0-d masked integer. "cytepool.stats.multiple_testing.adjust_p_values": Entry( lambda c: {"p": [0.01, 0.2, 0.5], "n": 5, "axis": 0}, @@ -2151,6 +2258,31 @@ def _forest_arguments(context: Context) -> dict[str, Any]: options=("standardize",), objects=("scores", "ax"), ), + # The sources shown and the profiles' labels. + "cytepool.plotting.plot_activities": Entry( + lambda c: { + "activities": _activities(), + "sources": ["B", "A"], + "top": 2, + "color_by": ["a", "b", "a", "b"], + "standardize": True, + "ax": _axes(), + }, + {"sources": None, "color_by": None}, + options=("top", "standardize"), + objects=("activities", "ax"), + ), + "cytepool.plotting.plot_top_activities": Entry( + lambda c: { + "activities": _activities(), + "profile": "S2", + "top": 2, + "ax": _axes(), + }, + {}, + options=("profile", "top"), + objects=("activities", "ax"), + ), "cytepool.plotting.plot_dispersions": Entry( lambda c: { "dispersions": deseq2_dispersions( @@ -2379,6 +2511,16 @@ def _forest_arguments(context: Context) -> dict[str, Any]: ("path", "source", "version", "licence"), "a path, and text that the collection keeps", ), + "cytepool.networks.Network.__getitem__": (("source",), "a source, looked up"), + "cytepool.networks.Network.__contains__": (("source",), "a source, looked up"), + "cytepool.networks.collectri": ( + ("licence", "cache_dir"), + "a licence level and a directory", + ), + "cytepool.networks.progeny": ( + ("organism", "top", "max_p_value", "cache_dir"), + "an organism, a number of genes, a p-value and a directory", + ), } # Result containers: they store their fields as given, so a masked field keeps its @@ -2410,6 +2552,7 @@ def _forest_arguments(context: Context) -> dict[str, Any]: "cytepool.stats.quasi_likelihood.UnitDevianceMoments", "cytepool.variance_partitioning.VarianceFractions", "cytepool.pathway_scores.PathwayScores", + "cytepool.activity.Activities", } # Callables that modules with __all__ define but do not export. diff --git a/tests/test_networks.py b/tests/test_networks.py new file mode 100644 index 0000000..c5966e8 --- /dev/null +++ b/tests/test_networks.py @@ -0,0 +1,333 @@ +"""Tests of networks: the Network class and the loaders of CollecTRI and PROGENy. + +The loaders read files in the formats of the published ones, their data simulated, +which validation/activity.py wrote to tests/data/activity and which are pinned here in +place of the real files: rows in the format of CollecTRI's file, against decoupler's +reading of them, and a package in the layout of progeny's with R data files that R +wrote, against the values R wrote and the genes dplyr's ``top_n`` keeps. Downloads, +the cache, the refusal of a file that is not the one pinned, and offline use are +checked with a stand-in for the connection: the tests never connect. +""" + +import csv +import doctest +import hashlib +import io +import json +import pickle +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +from cytepool import networks +from cytepool.gene_sets import GeneSets +from cytepool.networks import Network, collectri, progeny +from cytepool.table import Table + +DATA = Path(__file__).parent / "data" / "activity" +ROWS = (DATA / "collectri_rows.csv").read_bytes() +PACKAGE = (DATA / "progeny_package.tar.gz").read_bytes() +DECOUPLER = json.loads((DATA / "decoupler.json").read_text()) +COLUMNS = ("source", "target", "weight", "resources", "references", "sign_decision") + + +def _digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +@pytest.fixture +def cache(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """An empty cache, the pieces pinned in place of the real files, and no + connection.""" + for key, data in (("collectri", ROWS), ("progeny", PACKAGE)): + pinned = networks._DOWNLOADS[key]._replace(size=len(data), sha256=_digest(data)) + monkeypatch.setitem(networks._DOWNLOADS, key, pinned) + + def no_connection(*args: Any, **kwargs: Any) -> None: + raise AssertionError("a test connected to the network") + + monkeypatch.setattr(urllib.request, "urlopen", no_connection) + return tmp_path / "cache" + + +def _place(cache: Path, key: str, data: bytes) -> Path: + path = cache / networks._DOWNLOADS[key].file + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + return path + + +def _rows(network: Network) -> set[tuple[Any, ...]]: + edges = network.edges + return set(zip(*(edges[name].tolist() for name in COLUMNS), strict=True)) + + +def _file_rows() -> list[list[str]]: + return list(csv.reader(io.StringIO(ROWS.decode())))[1:] + + +# CollecTRI. + + +def test_collectri_is_read_as_decoupler_reads_it(cache: Path) -> None: + _place(cache, "collectri", ROWS) + academic = collectri(licence="academic", cache_dir=cache) + + theirs = {tuple(row) for row in DECOUPLER["collectri"]} + # decoupler drops the interactions without a reference; CytePool keeps them. + without = {row for row in _rows(academic) if row[4] == ""} + assert len(without) == sum(1 for row in _file_rows() if row[4] == "") > 0 + assert _rows(academic) - without == theirs + assert len(academic.edges) == len(_file_rows()) + # The order of the file. + assert academic.edges["source"].tolist() == [row[0] for row in _file_rows()] + + +def test_the_commercial_level_leaves_out_what_geredb_alone_supports( + cache: Path, +) -> None: + _place(cache, "collectri", ROWS) + commercial = collectri(cache_dir=cache) + academic = collectri(licence="academic", cache_dir=cache) + + alone = [row for row in _file_rows() if row[3] == "CollecTRI;GEREDB_CollecTRI"] + assert len(alone) == 8 + left_out = _rows(academic) - _rows(commercial) + assert {(row[0], row[1]) for row in left_out} == {(r[0], r[1]) for r in alone} + assert "level 'commercial'" in commercial.licence + assert "level 'academic'" in academic.licence + assert commercial.name == "CollecTRI" + assert commercial.file == "CollecTRI_regulons.csv" + assert commercial.sha256 == _digest(ROWS) + assert "Mueller-Dott" in commercial.citation + with pytest.raises(ValueError, match="licence must be one of 'commercial'"): + collectri(licence="nonprofit", cache_dir=cache) + + +def test_a_file_that_is_not_the_one_pinned_is_refused(cache: Path) -> None: + changed = bytearray(ROWS) + changed[100] ^= 1 # the same size, another digest + path = _place(cache, "collectri", bytes(changed)) + with pytest.raises(ValueError, match="is not the file that CytePool pins"): + collectri(cache_dir=cache) + path.write_bytes(ROWS[:-1]) + with pytest.raises(ValueError, match=f"{len(ROWS) - 1:,} bytes"): + collectri(cache_dir=cache) + + +class _Answer(io.BytesIO): + """What urlopen returns: the bytes of a download, read in blocks.""" + + def __enter__(self) -> "_Answer": + return self + + +def test_a_download_is_kept_only_when_it_is_the_file_pinned( + cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + requests = [] + + def serve(data: bytes) -> Any: + def urlopen(request: urllib.request.Request, timeout: float) -> _Answer: + requests.append(request) + return _Answer(data) + + return urlopen + + monkeypatch.setattr(urllib.request, "urlopen", serve(ROWS[:-10])) + with pytest.raises(ValueError, match="the download of .* is not the file"): + collectri(cache_dir=cache) + assert list(cache.iterdir()) == [] # nothing is kept, not even in part + + monkeypatch.setattr(urllib.request, "urlopen", serve(ROWS)) + first = collectri(cache_dir=cache) + assert (cache / "CollecTRI_regulons.csv").read_bytes() == ROWS + assert requests[-1].full_url == networks._DOWNLOADS["collectri"].url + # The request names its client, which some servers require. + assert requests[-1].header_items()[0][1].startswith("CytePool/") + # Once in the cache, the file is read without a connection. + monkeypatch.setattr(urllib.request, "urlopen", None) + assert collectri(cache_dir=cache).edges.equals(first.edges) + + +def test_without_a_connection_the_message_says_where_to_put_the_file( + cache: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + def offline(request: Any, timeout: float) -> None: + raise urllib.error.URLError("no route to host") + + monkeypatch.setattr(urllib.request, "urlopen", offline) + with pytest.raises(OSError, match=r"could not download CollecTRI_regulons\.csv"): + collectri(cache_dir=cache) + assert list(cache.iterdir()) == [] + + +def test_the_cache_is_under_xdg_cache_home( + cache: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "home")) + _place(tmp_path / "home" / "cytepool" / "networks", "collectri", ROWS) + assert len(collectri(licence="academic").edges) == len(_file_rows()) + + +# PROGENy. + + +def _r_table(organism: str) -> dict[tuple[str, str], tuple[float, float]]: + """What R wrote of the model: (pathway, gene) to (weight, p-value).""" + text = (DATA / f"progeny_model_{organism}_full.csv").read_text() + return { + (row["pathway"], row["gene"]): (float(row["weight"]), float(row["p.value"])) + for row in csv.DictReader(io.StringIO(text)) + } + + +@pytest.mark.parametrize("organism", ["human", "mouse"]) +def test_progenys_r_data_are_read_as_r_wrote_them(cache: Path, organism: str) -> None: + # The human genes are a factor, the mouse genes text: both of R's codings. + _place(cache, "progeny", PACKAGE) + found = progeny(organism=organism, max_p_value=None, cache_dir=cache) + edges = found.edges + ours = { + (s, t): (w, p) + for s, t, w, p in zip( + edges["source"].tolist(), + edges["target"].tolist(), + edges["weight"].tolist(), + edges["p_value"].tolist(), + strict=True, + ) + } + assert ours == _r_table(organism) + # The pathways in alphabetical order, the genes of each by p-value. + sources = edges["source"].tolist() + assert sources == sorted(sources) + for pathway in set(sources): + p = edges["p_value"][edges["source"] == pathway] + assert np.all(np.diff(p) >= 0.0) + assert found.licence == "Apache License 2.0 (the R package progeny)" + assert found.name == f"PROGENy ({organism})" + + +def test_progeny_keeps_the_top_genes_of_each_pathway_as_dplyr_does(cache: Path) -> None: + _place(cache, "progeny", PACKAGE) + top = progeny(top=3, max_p_value=None, cache_dir=cache) + text = (DATA / "progeny_top3.csv").read_text() + expected = { + (row["pathway"], row["gene"]) for row in csv.DictReader(io.StringIO(text)) + } + kept = set(zip(top.edges["source"].tolist(), top.edges["target"].tolist(), + strict=True)) # fmt: skip + assert kept == expected + # One pathway has a tie at its third gene, which stays. + assert len(expected) == 3 * len(top) + 1 + + +def test_progeny_keeps_the_genes_below_a_p_value(cache: Path) -> None: + _place(cache, "progeny", PACKAGE) + found = progeny(max_p_value=0.2, cache_dir=cache) + expected = {key for key, (_, p) in _r_table("human").items() if p < 0.2} + assert set(zip(found.edges["source"].tolist(), found.edges["target"].tolist(), + strict=True)) == expected # fmt: skip + default = progeny(cache_dir=cache) + assert all(p < 0.05 for p in default.edges["p_value"].tolist()) + with pytest.raises(ValueError, match=r"max_p_value must be in \(0, 1\]"): + progeny(max_p_value=0.0, cache_dir=cache) + with pytest.raises(ValueError, match="top must be at least 1"): + progeny(top=0, cache_dir=cache) + with pytest.raises(ValueError, match="organism must be one of 'human', 'mouse'"): + progeny(organism="rat", cache_dir=cache) + + +def test_r_data_that_cannot_be_read_are_refused() -> None: + with pytest.raises(ValueError, match="not an R data file in the XDR format"): + networks._read_r_data(b"not R") + with pytest.raises(ValueError, match="end in the middle"): + networks._read_r_data(b"RDX3\nX\n\x00\x00\x00\x03\x00\x04") + + +# The Network class. + +EDGES = { + "STAT1": {"IRF1": 1.0, "SOCS1": 1.0, "MYC": -1.0}, + "IRF1": {"CXCL10": 2.0, "MYC": -0.5}, +} + + +def test_the_forms_of_a_network() -> None: + network = Network(EDGES, name="mine", version="1", licence="CC0") + assert network.sources == ("STAT1", "IRF1") + assert dict(network["IRF1"]) == {"CXCL10": 2.0, "MYC": -0.5} + assert network.edges.columns == ("source", "target", "weight") + table = Table( + { + "source": ["STAT1", "STAT1", "STAT1", "IRF1", "IRF1"], + "target": ["IRF1", "SOCS1", "MYC", "CXCL10", "MYC"], + "weight": [1.0, 1.0, -1.0, 2.0, -0.5], + "p_value": [0.1, 0.2, 0.3, 0.4, 0.5], + } + ) + from_table = Network(table) + assert from_table.edges.select(["source", "target", "weight"]).equals(network.edges) + assert_array_equal(from_table.edges["p_value"], [0.1, 0.2, 0.3, 0.4, 0.5]) + # A mapping of columns is a table; pandas DataFrames too. + assert Network(table.to_dict()).edges.equals(from_table.edges) + pandas = pytest.importorskip("pandas") + assert Network(pandas.DataFrame(table.to_dict())).edges.equals(from_table.edges) + # Lists of targets, and gene sets, weigh 1. + sets = Network(GeneSets({"up": ["A", "B"], "down": ["C"]})) + assert_array_equal(sets.edges["weight"], [1.0, 1.0, 1.0]) + assert ( + Network(table.select(["source", "target"])).edges["weight"].tolist() + == [1.0] * 5 + ) + # The mapping. + assert "STAT1" in network and "MYC" not in network and [] not in network + assert len(network) == 2 and list(network) == ["STAT1", "IRF1"] + with pytest.raises(KeyError, match="no source named 'STAT2' .did you mean 'STAT1'"): + network["STAT2"] + + +def test_a_network_selects_records_and_pickles() -> None: + network = Network(EDGES, name="mine", licence="CC0") + chosen = network.select(["IRF1", "STAT1"]) + assert chosen.sources == ("IRF1", "STAT1") + assert chosen.name == "mine" and chosen.licence == "CC0" + assert network.select("IRF1").edges["target"].tolist() == ["CXCL10", "MYC"] + copy = pickle.loads(pickle.dumps(network)) + assert copy.edges.equals(network.edges) and copy.name == "mine" + assert repr(network).startswith("Network: 2 sources, 5 edges, 4 target genes") + with pytest.raises(AttributeError, match="Network is immutable"): + network.name = "other" # type: ignore[misc] + with pytest.raises(ValueError, match="'IRF1' is selected more than once"): + network.select(["IRF1", "IRF1"]) + with pytest.raises(KeyError, match="no source named 'X'"): + network.select(["X"]) + + +def test_network_messages() -> None: + with pytest.raises(ValueError, match="from 'A' to 'B' is given twice"): + Network({"source": ["A", "A"], "target": ["B", "B"], "weight": [1.0, 2.0]}) + with pytest.raises(ValueError, match="the target of row 1 is missing"): + Network({"source": ["A", "A"], "target": ["B", None]}) + with pytest.raises(ValueError, match="edge 0 has the weight inf"): + Network({"A": {"B": np.inf}}) + with pytest.raises(ValueError, match="the network has no edges"): + Network({"A": []}) + with pytest.raises(ValueError, match="have no column 'target'"): + Network(Table({"source": ["A"], "gene": ["B"]})) + with pytest.raises(TypeError, match="edges must be a Table or pandas DataFrame"): + Network(3) + with pytest.raises(TypeError, match="must be gene ids"): + Network({"A": {1: 1.0}}) + with pytest.raises(ValueError, match="sha256 must be a SHA-256 digest"): + Network(EDGES, sha256="abc") + + +def test_the_examples_run() -> None: + assert doctest.testmod(networks, raise_on_error=False).failed == 0 diff --git a/validation/README.md b/validation/README.md index f3be40a..bcd0210 100644 --- a/validation/README.md +++ b/validation/README.md @@ -31,6 +31,10 @@ tier, and a few use a bound derived for their case. downloads 77 MB from GEO (series GSE96583) into a cache directory outside the repository, and it checks the SHA-256 of every file before reading it. Its docstring says where the files go and how to choose another directory. +- `activity.py` needs a connection too: CytePool's loaders download CollecTRI (4.3 MB) + and progeny (10.6 MB) once into `~/.cache/cytepool/networks`, checking their + SHA-256, and decoupler's loaders and the checks of the published digests read + Zenodo, Bioconductor and OmniPath. Run a script from the repository root, against the working tree: @@ -42,5 +46,6 @@ PYTHONPATH=. python validation/