sampledisco 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sampledisco-0.4.2/src/sampledisco.egg-info → sampledisco-0.5.0}/PKG-INFO +2 -1
- {sampledisco-0.4.2 → sampledisco-0.5.0}/README.md +1 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/pyproject.toml +1 -1
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/__init__.py +6 -1
- sampledisco-0.5.0/src/sampledisco/sample_prediction/__init__.py +4 -0
- sampledisco-0.5.0/src/sampledisco/sample_prediction/prediction.py +186 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0/src/sampledisco.egg-info}/PKG-INFO +2 -1
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco.egg-info/SOURCES.txt +4 -1
- sampledisco-0.5.0/tests/test_sample_prediction.py +154 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/LICENSE +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/requirements.txt +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/setup.cfg +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/cli.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/config/config_demo.yaml +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/ATAC_ArchR.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/ATAC_RNA_harmony.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/ATAC_gene_activity.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/ATAC_peak_annotation.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/RNA_name_convertor.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/pseudo_correlation.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/test_pseudo_correlation.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/validation.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/parameter_selection/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/parameter_selection/autotune.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/ATAC_cell_type.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/ATAC_cell_type_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/atac_preprocess_cpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/atac_preprocess_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/cell_type_cpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/cell_type_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_batch_correction.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_glue.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_merge.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/rna_preprocess_cpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/rna_preprocess_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_association/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_association/association.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/RAISIN.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/RAISIN_TEST.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/cluster.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/cluster_severity_reconcile.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/proportion_test.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_distance/ChiSquare.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_distance/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_distance/distance_test.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_distance/jensenshannon.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_distance/sample_distance.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/blocks.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/sample_embedding.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/sample_embedding_gpu.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/CCA.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/CCA_test.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/TSCAN.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/trajectory_DGE_visualization.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/Grouping.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/batch_regress.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/embedding_keys.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/harmony_compat.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/imbalance_cell_type_handler.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/inspector.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/limma.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/merge_sample_meta.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/random_seed.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/safe_save.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/slim_adata.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/subsample.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/subsample_generator.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/tf_idf.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/unify_optimal.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/ATAC_visualization.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/DEG_visualization.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/multi_omics_visualization.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_embedding.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_helper.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_other.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/wrapper/__init__.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/wrapper/atac_wrapper.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/wrapper/multiomics_wrapper.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/wrapper/rna_wrapper.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/wrapper/wrapper.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco.egg-info/dependency_links.txt +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco.egg-info/entry_points.txt +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco.egg-info/requires.txt +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco.egg-info/top_level.txt +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/tests/test_config_validation.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/tests/test_inplace_uns_and_equal_weight.py +0 -0
- {sampledisco-0.4.2 → sampledisco-0.5.0}/tests/test_sample_embedding_equivalence.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sampledisco
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)
|
|
5
5
|
Author: Hongkai Ji
|
|
6
6
|
Author-email: Harry Jiang <hjiang55@jh.edu>
|
|
@@ -109,6 +109,7 @@ code/
|
|
|
109
109
|
│ ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
|
|
110
110
|
│ ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
|
|
111
111
|
│ ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
|
|
112
|
+
│ ├── sample_prediction/ # Cross-validated phenotype prediction from the PCs; kNN label transfer
|
|
112
113
|
│ ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
|
|
113
114
|
│ ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
|
|
114
115
|
│ └── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
|
|
@@ -61,6 +61,7 @@ code/
|
|
|
61
61
|
│ ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
|
|
62
62
|
│ ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
|
|
63
63
|
│ ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
|
|
64
|
+
│ ├── sample_prediction/ # Cross-validated phenotype prediction from the PCs; kNN label transfer
|
|
64
65
|
│ ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
|
|
65
66
|
│ ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
|
|
66
67
|
│ └── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "sampledisco"
|
|
12
|
-
version = "0.
|
|
12
|
+
version = "0.5.0"
|
|
13
13
|
description = "Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -6,6 +6,8 @@ pull scanpy / torch / scGLUE until you actually call into the pipeline):
|
|
|
6
6
|
import sampledisco
|
|
7
7
|
sampledisco.wrapper(...) # full pipeline (RNA / ATAC / multi-omics)
|
|
8
8
|
sampledisco.compute_sample_embedding(...) # the core method only
|
|
9
|
+
sampledisco.predict_phenotype(...) # cross-validated phenotype prediction from the PCs
|
|
10
|
+
sampledisco.transfer_labels(...) # kNN label transfer in the sample embedding
|
|
9
11
|
|
|
10
12
|
The CLI entry point is ``sampledisco --config <yaml>`` (see ``sampledisco.cli``).
|
|
11
13
|
"""
|
|
@@ -18,7 +20,7 @@ try:
|
|
|
18
20
|
except PackageNotFoundError: # running from a source tree that isn't installed
|
|
19
21
|
__version__ = "0.0.0+unknown"
|
|
20
22
|
|
|
21
|
-
__all__ = ["wrapper", "compute_sample_embedding"]
|
|
23
|
+
__all__ = ["wrapper", "compute_sample_embedding", "predict_phenotype", "transfer_labels"]
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def __getattr__(name: str):
|
|
@@ -29,4 +31,7 @@ def __getattr__(name: str):
|
|
|
29
31
|
if name == "compute_sample_embedding":
|
|
30
32
|
from sampledisco.sample_embedding import compute_sample_embedding
|
|
31
33
|
return compute_sample_embedding
|
|
34
|
+
if name in ("predict_phenotype", "transfer_labels"):
|
|
35
|
+
from sampledisco.sample_prediction import prediction
|
|
36
|
+
return getattr(prediction, name)
|
|
32
37
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Supervised use of the sample embedding: phenotype prediction and label transfer.
|
|
2
|
+
|
|
3
|
+
Both functions take a one-row-per-sample AnnData (``build_sample_adata``), read the
|
|
4
|
+
first ``n_pcs`` components of ``uns['X_DR_sample']`` (``obsm`` as fallback) and the
|
|
5
|
+
labels from ``.obs``.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Dict, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from anndata import AnnData
|
|
12
|
+
from scipy.spatial.distance import cdist
|
|
13
|
+
from sklearn.linear_model import LogisticRegression, Ridge
|
|
14
|
+
from sklearn.metrics import accuracy_score, balanced_accuracy_score, r2_score, roc_auc_score
|
|
15
|
+
from sklearn.model_selection import GridSearchCV, GroupKFold, StratifiedGroupKFold
|
|
16
|
+
from sklearn.pipeline import Pipeline
|
|
17
|
+
from sklearn.preprocessing import StandardScaler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _embedding(pseudo_adata: AnnData, n_pcs: Optional[int]) -> np.ndarray:
|
|
21
|
+
emb = pseudo_adata.uns.get("X_DR_sample")
|
|
22
|
+
if isinstance(emb, pd.DataFrame):
|
|
23
|
+
X = emb.loc[pseudo_adata.obs_names].to_numpy(dtype=float)
|
|
24
|
+
elif "X_DR_sample" in pseudo_adata.obsm:
|
|
25
|
+
X = np.asarray(pseudo_adata.obsm["X_DR_sample"], dtype=float)
|
|
26
|
+
else:
|
|
27
|
+
raise KeyError("'X_DR_sample' not found in .uns or .obsm; build the AnnData with build_sample_adata.")
|
|
28
|
+
return X[:, :n_pcs]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def predict_phenotype(
|
|
32
|
+
pseudo_adata: AnnData,
|
|
33
|
+
label_col: str,
|
|
34
|
+
*,
|
|
35
|
+
group_col: Optional[str] = None,
|
|
36
|
+
n_pcs: Optional[int] = None,
|
|
37
|
+
task: str = "classification",
|
|
38
|
+
score_by: str = "sample",
|
|
39
|
+
n_splits: int = 5,
|
|
40
|
+
random_state: int = 0,
|
|
41
|
+
) -> Tuple[pd.DataFrame, Dict]:
|
|
42
|
+
"""Out-of-fold prediction of a sample phenotype from the first ``n_pcs`` embedding PCs.
|
|
43
|
+
|
|
44
|
+
Standardized PCs feed an L2 logistic regression (``task="classification"``, binary or
|
|
45
|
+
multiclass) or a ridge regression (``task="regression"``). The penalty is chosen by an
|
|
46
|
+
inner 3-fold grouped CV inside each training split (scored by AUC, balanced accuracy
|
|
47
|
+
or R²) from C in {10, 1, 0.1, 0.01} or alpha in {0.1, 1, 10, 100}; a tie goes to the
|
|
48
|
+
weakest penalty. The outer split is ``n_splits``-fold, stratified by class for
|
|
49
|
+
classification and seeded by ``random_state``. With ``group_col`` (e.g. donor) every
|
|
50
|
+
sample of a group falls in the same fold, outer and inner, so draws of one donor are
|
|
51
|
+
never in training and test at once. Samples with a missing label are left out. This is
|
|
52
|
+
the recipe of the paper's Sound Life trait prediction.
|
|
53
|
+
|
|
54
|
+
``score_by="group"`` scores groups instead of samples: the out-of-fold probabilities
|
|
55
|
+
(or predictions) and the labels are averaged over ``group_col`` before scoring (the
|
|
56
|
+
majority label for classification), as for the paper's donor-level AUCs.
|
|
57
|
+
|
|
58
|
+
Returns ``(predictions, metrics)``. ``predictions`` is indexed by sample with columns
|
|
59
|
+
``label``, ``predicted``, ``fold`` and, for classification, ``prob_<class>``.
|
|
60
|
+
``metrics`` holds ``n_samples``, ``n_groups``, ``n_pcs``, the penalty chosen in each
|
|
61
|
+
fold (``C`` or ``alpha``) and ``auc`` (binary; the second class in sorted order is
|
|
62
|
+
positive), ``accuracy`` and ``balanced_accuracy`` (classification) or ``r2``
|
|
63
|
+
(regression), computed on the out-of-fold predictions at the ``score_by`` level.
|
|
64
|
+
"""
|
|
65
|
+
if task not in ("classification", "regression") or score_by not in ("sample", "group"):
|
|
66
|
+
raise ValueError("task must be 'classification' or 'regression', score_by 'sample' or 'group'.")
|
|
67
|
+
obs = pseudo_adata.obs
|
|
68
|
+
keep = obs[label_col].notna().to_numpy()
|
|
69
|
+
X = _embedding(pseudo_adata, n_pcs)[keep]
|
|
70
|
+
labels = obs[label_col].to_numpy()[keep]
|
|
71
|
+
groups = (obs[group_col] if group_col else obs.index.to_series()).astype(str).to_numpy()[keep]
|
|
72
|
+
|
|
73
|
+
if task == "classification":
|
|
74
|
+
classes, y = np.unique(labels, return_inverse=True)
|
|
75
|
+
model, param = LogisticRegression(max_iter=3000), "C"
|
|
76
|
+
grid, scoring = [10, 1, 0.1, 0.01], "roc_auc" if len(classes) == 2 else "balanced_accuracy"
|
|
77
|
+
outer = StratifiedGroupKFold(n_splits, shuffle=True, random_state=random_state).split(X, y, groups)
|
|
78
|
+
P = np.zeros((len(y), len(classes)))
|
|
79
|
+
else:
|
|
80
|
+
y = labels.astype(float)
|
|
81
|
+
model, param, grid, scoring = Ridge(), "alpha", [0.1, 1, 10, 100], "r2"
|
|
82
|
+
codes = np.unique(groups, return_inverse=True)[1]
|
|
83
|
+
shuffled = np.random.default_rng(random_state).permutation(codes.max() + 1)[codes]
|
|
84
|
+
outer = GroupKFold(n_splits).split(X, y, shuffled)
|
|
85
|
+
pred = np.zeros(len(y))
|
|
86
|
+
|
|
87
|
+
fold, chosen = np.zeros(len(y), dtype=int), []
|
|
88
|
+
pipe = Pipeline([("scale", StandardScaler()), ("model", model)])
|
|
89
|
+
for i, (tr, te) in enumerate(outer):
|
|
90
|
+
gs = GridSearchCV(pipe, {f"model__{param}": grid}, cv=GroupKFold(3), scoring=scoring)
|
|
91
|
+
gs.fit(X[tr], y[tr], groups=groups[tr])
|
|
92
|
+
chosen.append(gs.best_params_[f"model__{param}"])
|
|
93
|
+
fold[te] = i
|
|
94
|
+
if task == "classification":
|
|
95
|
+
P[np.ix_(te, gs.classes_)] = gs.predict_proba(X[te])
|
|
96
|
+
else:
|
|
97
|
+
pred[te] = gs.predict(X[te])
|
|
98
|
+
|
|
99
|
+
metrics = {"task": task, "score_by": score_by, "n_samples": len(y), "n_groups": len(np.unique(groups)),
|
|
100
|
+
"n_pcs": X.shape[1], param: chosen}
|
|
101
|
+
out = pd.DataFrame({"label": labels}, index=obs.index[keep])
|
|
102
|
+
if task == "classification":
|
|
103
|
+
out["predicted"] = classes[P.argmax(1)]
|
|
104
|
+
for j, c in enumerate(classes):
|
|
105
|
+
out[f"prob_{c}"] = P[:, j]
|
|
106
|
+
Y = np.eye(len(classes))[y] # one-hot; summed over a group = class counts
|
|
107
|
+
if score_by == "group":
|
|
108
|
+
Y = pd.DataFrame(Y).groupby(groups).sum().to_numpy()
|
|
109
|
+
P = pd.DataFrame(P).groupby(groups).mean().to_numpy()
|
|
110
|
+
y, hard = Y.argmax(1), P.argmax(1)
|
|
111
|
+
if len(classes) == 2:
|
|
112
|
+
metrics["auc"] = float(roc_auc_score(y, P[:, 1]))
|
|
113
|
+
metrics["accuracy"] = float(accuracy_score(y, hard))
|
|
114
|
+
metrics["balanced_accuracy"] = float(balanced_accuracy_score(y, hard))
|
|
115
|
+
else:
|
|
116
|
+
out["predicted"] = pred
|
|
117
|
+
if score_by == "group":
|
|
118
|
+
y, pred = (pd.Series(v).groupby(groups).mean().to_numpy() for v in (y, pred))
|
|
119
|
+
metrics["r2"] = float(r2_score(y, pred))
|
|
120
|
+
out["fold"] = fold
|
|
121
|
+
return out.infer_objects(), metrics
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _knn_vote(D: np.ndarray, y: np.ndarray, k: int, weighted: bool) -> Tuple[np.ndarray, np.ndarray]:
|
|
125
|
+
pred, conf = np.full(len(D), np.nan, dtype=object), np.full(len(D), np.nan)
|
|
126
|
+
for i, d in enumerate(D):
|
|
127
|
+
nn = np.argsort(d, kind="stable")[:k]
|
|
128
|
+
nn = nn[np.isfinite(d[nn])]
|
|
129
|
+
if nn.size == 0:
|
|
130
|
+
continue
|
|
131
|
+
w = 1.0 / np.maximum(d[nn], 1e-12) if weighted else np.ones(nn.size)
|
|
132
|
+
votes = pd.Series(w).groupby(y[nn], sort=False).sum() # labels in order of first (nearest) neighbour
|
|
133
|
+
pred[i], conf[i] = votes.idxmax(), votes.max() / w.sum()
|
|
134
|
+
return pred, conf
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def transfer_labels(
|
|
138
|
+
pseudo_adata: AnnData,
|
|
139
|
+
label_col: str,
|
|
140
|
+
*,
|
|
141
|
+
query: Optional[Sequence[bool]] = None,
|
|
142
|
+
n_neighbors: int = 5,
|
|
143
|
+
weighted: bool = False,
|
|
144
|
+
n_pcs: Optional[int] = None,
|
|
145
|
+
group_col: Optional[str] = None,
|
|
146
|
+
) -> Tuple[pd.DataFrame, Dict]:
|
|
147
|
+
"""Label query samples by a k-nearest-neighbour vote in the sample embedding.
|
|
148
|
+
|
|
149
|
+
References are the samples with a label in ``label_col`` that are not in the boolean
|
|
150
|
+
``query`` mask; every other sample is a query (by default, the unlabelled samples).
|
|
151
|
+
Each query takes the majority label of its ``n_neighbors`` nearest references
|
|
152
|
+
(Euclidean distance on the first ``n_pcs`` PCs); with ``weighted=True`` each vote
|
|
153
|
+
counts 1/distance. Ties go to the label of the nearest tied neighbour. The confidence
|
|
154
|
+
is the (weighted) share of the votes won by the assigned label.
|
|
155
|
+
|
|
156
|
+
To judge reliability, every reference is also labelled from the other references
|
|
157
|
+
(leave-one-out). With ``group_col`` (a donor, or ``sample`` for multi-omics units) the
|
|
158
|
+
references of the same group are left out as well, so a unit is never matched to
|
|
159
|
+
another unit of the same donor or sample.
|
|
160
|
+
|
|
161
|
+
Returns ``(table, metrics)``. ``table`` is indexed by sample with columns ``label``,
|
|
162
|
+
``query``, ``predicted`` (the transferred label for queries, the leave-one-out label for
|
|
163
|
+
references) and ``confidence``. ``metrics`` holds ``loo_accuracy``,
|
|
164
|
+
``loo_balanced_accuracy``, ``n_reference``, ``n_query``, ``n_neighbors`` and ``n_pcs``.
|
|
165
|
+
"""
|
|
166
|
+
obs = pseudo_adata.obs
|
|
167
|
+
X = _embedding(pseudo_adata, n_pcs)
|
|
168
|
+
labels = obs[label_col]
|
|
169
|
+
ref = labels.notna().to_numpy()
|
|
170
|
+
if query is not None:
|
|
171
|
+
ref &= ~np.asarray(query, dtype=bool)
|
|
172
|
+
y = labels.to_numpy()[ref]
|
|
173
|
+
|
|
174
|
+
D = cdist(X, X[ref])
|
|
175
|
+
g = (obs[group_col] if group_col else obs.index.to_series()).astype(str).to_numpy()[ref]
|
|
176
|
+
D[ref] = np.where(g[:, None] == g[None, :], np.inf, D[ref])
|
|
177
|
+
pred, conf = _knn_vote(D, y, n_neighbors, weighted)
|
|
178
|
+
|
|
179
|
+
table = pd.DataFrame({"label": labels.to_numpy(), "query": ~ref, "predicted": pred, "confidence": conf},
|
|
180
|
+
index=obs.index).infer_objects() # numeric labels back to a numeric dtype
|
|
181
|
+
loo = table[ref & pd.notna(pred)]
|
|
182
|
+
metrics = {"loo_accuracy": float(accuracy_score(loo["label"], loo["predicted"])),
|
|
183
|
+
"loo_balanced_accuracy": float(balanced_accuracy_score(loo["label"], loo["predicted"])),
|
|
184
|
+
"n_reference": int(ref.sum()), "n_query": int((~ref).sum()),
|
|
185
|
+
"n_neighbors": n_neighbors, "n_pcs": X.shape[1]}
|
|
186
|
+
return table, metrics
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sampledisco
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Cross-omics, cross-condition sample embedding for single-cell data (RNA + ATAC)
|
|
5
5
|
Author: Hongkai Ji
|
|
6
6
|
Author-email: Harry Jiang <hjiang55@jh.edu>
|
|
@@ -109,6 +109,7 @@ code/
|
|
|
109
109
|
│ ├── sample_clustering/ # Hierarchical (HRA / HRC / NN / UPGMA / consensus), K-means, proportion test, RAISIN
|
|
110
110
|
│ ├── sample_trajectory/ # CCA (supervised) and TSCAN (unsupervised) + GAM-based trajectory DGE
|
|
111
111
|
│ ├── sample_association/ # Per-PC variance explained vs sample-level covariates (permutation FDR)
|
|
112
|
+
│ ├── sample_prediction/ # Cross-validated phenotype prediction from the PCs; kNN label transfer
|
|
112
113
|
│ ├── visualization/ # Embedding plots, dendrograms, DGE volcanos, modality-aware multi-omics scatters
|
|
113
114
|
│ ├── utils/ # Shared helpers: seed, safe h5ad I/O, limma, TF-IDF, batch regress, Grouping
|
|
114
115
|
│ └── gene_activity/ # ATAC peak → gene activity inference + RNA-ATAC validation
|
|
@@ -53,6 +53,8 @@ src/sampledisco/sample_embedding/__init__.py
|
|
|
53
53
|
src/sampledisco/sample_embedding/blocks.py
|
|
54
54
|
src/sampledisco/sample_embedding/sample_embedding.py
|
|
55
55
|
src/sampledisco/sample_embedding/sample_embedding_gpu.py
|
|
56
|
+
src/sampledisco/sample_prediction/__init__.py
|
|
57
|
+
src/sampledisco/sample_prediction/prediction.py
|
|
56
58
|
src/sampledisco/sample_trajectory/CCA.py
|
|
57
59
|
src/sampledisco/sample_trajectory/CCA_test.py
|
|
58
60
|
src/sampledisco/sample_trajectory/TSCAN.py
|
|
@@ -89,4 +91,5 @@ src/sampledisco/wrapper/rna_wrapper.py
|
|
|
89
91
|
src/sampledisco/wrapper/wrapper.py
|
|
90
92
|
tests/test_config_validation.py
|
|
91
93
|
tests/test_inplace_uns_and_equal_weight.py
|
|
92
|
-
tests/test_sample_embedding_equivalence.py
|
|
94
|
+
tests/test_sample_embedding_equivalence.py
|
|
95
|
+
tests/test_sample_prediction.py
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""predict_phenotype and transfer_labels on synthetic sample embeddings with a known answer.
|
|
2
|
+
Run with `pytest tests/` or `python tests/test_sample_prediction.py`."""
|
|
3
|
+
import anndata as ad
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from sklearn.metrics import r2_score, roc_auc_score
|
|
8
|
+
|
|
9
|
+
from sampledisco import predict_phenotype, transfer_labels
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make(X, **obs):
|
|
13
|
+
a = ad.AnnData(X=X.astype(np.float32), obs=pd.DataFrame(obs, index=[f"s{i}" for i in range(len(X))]))
|
|
14
|
+
a.uns["X_DR_sample"] = pd.DataFrame(X, index=a.obs_names, columns=[f"PC{j + 1}" for j in range(X.shape[1])])
|
|
15
|
+
return a
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def separated(n=60, pc=0, seed=0):
|
|
19
|
+
"""Two classes 12 SD apart on one PC (``pc``), pure noise on the other four."""
|
|
20
|
+
rng = np.random.default_rng(seed)
|
|
21
|
+
y = np.repeat(["healthy", "severe"], n // 2)
|
|
22
|
+
X = rng.normal(size=(n, 5))
|
|
23
|
+
X[:, pc] += np.where(y == "severe", 6.0, -6.0)
|
|
24
|
+
return make(X, status=y)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_binary_perfect():
|
|
28
|
+
pred, m = predict_phenotype(separated(), "status")
|
|
29
|
+
assert m["auc"] == 1.0 and m["accuracy"] == 1.0 and m["n_pcs"] == 5
|
|
30
|
+
assert m["C"] == [10] * 5 # every C ties on separable data: weakest penalty
|
|
31
|
+
assert (pred["predicted"] == pred["label"]).all()
|
|
32
|
+
assert np.allclose(pred[["prob_healthy", "prob_severe"]].sum(1), 1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_only_first_n_pcs_used():
|
|
36
|
+
a = separated(pc=2) # signal on PC3 only
|
|
37
|
+
assert predict_phenotype(a, "status", n_pcs=3)[1]["auc"] == 1.0
|
|
38
|
+
assert predict_phenotype(a, "status", n_pcs=2)[1]["auc"] < 0.75
|
|
39
|
+
assert transfer_labels(a, "status", n_pcs=3)[1]["loo_accuracy"] == 1.0
|
|
40
|
+
assert transfer_labels(a, "status", n_pcs=2)[1]["loo_accuracy"] < 0.75
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_random_labels_near_chance():
|
|
44
|
+
rng = np.random.default_rng(1)
|
|
45
|
+
a = make(rng.normal(size=(200, 10)), status=rng.choice(["a", "b"], 200))
|
|
46
|
+
_, m = predict_phenotype(a, "status")
|
|
47
|
+
assert 0.3 < m["auc"] < 0.7
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_multiclass_and_regression():
|
|
51
|
+
rng = np.random.default_rng(2)
|
|
52
|
+
y = np.repeat([0, 1, 2], 30)
|
|
53
|
+
X = rng.normal(size=(90, 4))
|
|
54
|
+
X[:, 0] += 8 * y
|
|
55
|
+
a = make(X, grade=y, score=3 * X[:, 0] + rng.normal(size=90))
|
|
56
|
+
pred, m = predict_phenotype(a, "grade")
|
|
57
|
+
assert "auc" not in m and m["accuracy"] > 0.95 and {"prob_0", "prob_1", "prob_2"} <= set(pred.columns)
|
|
58
|
+
pred, m = predict_phenotype(a, "score", task="regression")
|
|
59
|
+
assert m["r2"] > 0.95 and len(m["alpha"]) == 5
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_groups_never_split_and_seeded():
|
|
63
|
+
"""Each donor's draws share a fold. The label is a donor trait unrelated to X, and donors sit
|
|
64
|
+
apart in 30 dimensions: when draws of a donor are split across folds the model memorizes donors."""
|
|
65
|
+
rng = np.random.default_rng(3)
|
|
66
|
+
donors = np.repeat([f"d{i}" for i in range(20)], 4)
|
|
67
|
+
status = np.repeat(rng.permutation(np.repeat(["neg", "pos"], 10)), 4)
|
|
68
|
+
X = np.repeat(rng.normal(size=(20, 30)), 4, axis=0) + rng.normal(size=(80, 30)) * 0.05
|
|
69
|
+
a = make(X, status=status, donor=donors)
|
|
70
|
+
pred, m = predict_phenotype(a, "status", group_col="donor")
|
|
71
|
+
assert (pred.groupby(a.obs["donor"])["fold"].nunique() == 1).all() and m["n_groups"] == 20
|
|
72
|
+
assert m["auc"] < 0.8 # no donor leakage: near chance
|
|
73
|
+
_, leaky = predict_phenotype(a, "status") # draws split across folds: inflated
|
|
74
|
+
assert leaky["auc"] > 0.95
|
|
75
|
+
again, _ = predict_phenotype(a, "status", group_col="donor")
|
|
76
|
+
assert again.equals(pred)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_score_by_group():
|
|
80
|
+
"""Group scores = scores of the per-group mean of the out-of-fold predictions."""
|
|
81
|
+
rng = np.random.default_rng(5)
|
|
82
|
+
donors = np.repeat([f"d{i}" for i in range(30)], 3)
|
|
83
|
+
trait = np.repeat(rng.choice(["neg", "pos"], 30), 3)
|
|
84
|
+
X = rng.normal(size=(90, 4))
|
|
85
|
+
X[:, 0] += np.where(trait == "pos", 1.0, -1.0)
|
|
86
|
+
a = make(X, trait=trait, donor=donors, bmi=X[:, 0] + rng.normal(size=90))
|
|
87
|
+
pred, m = predict_phenotype(a, "trait", group_col="donor", score_by="group")
|
|
88
|
+
d = pred.groupby(a.obs["donor"]).agg(label=("label", "first"), p=("prob_pos", "mean"))
|
|
89
|
+
assert np.isclose(m["auc"], roc_auc_score(d["label"] == "pos", d["p"]))
|
|
90
|
+
assert np.isclose(m["accuracy"], ((d["p"] > 0.5) == (d["label"] == "pos")).mean())
|
|
91
|
+
assert pred.equals(predict_phenotype(a, "trait", group_col="donor")[0]) # only the scoring changes
|
|
92
|
+
pred, m = predict_phenotype(a, "bmi", group_col="donor", task="regression", score_by="group")
|
|
93
|
+
d = pred.groupby(a.obs["donor"])[["label", "predicted"]].mean()
|
|
94
|
+
assert np.isclose(m["r2"], r2_score(d["label"], d["predicted"]))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_missing_labels_skipped_and_obsm_fallback():
|
|
98
|
+
a = separated()
|
|
99
|
+
a.obs.loc[["s0", "s1"], "status"] = np.nan
|
|
100
|
+
a.obsm["X_DR_sample"] = a.uns.pop("X_DR_sample").to_numpy()
|
|
101
|
+
pred, m = predict_phenotype(a, "status")
|
|
102
|
+
assert m["n_samples"] == 58 and "s0" not in pred.index and m["auc"] > 0.99
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_transfer_recovers_held_out_labels():
|
|
106
|
+
a = separated()
|
|
107
|
+
hidden = a.obs["status"].copy()
|
|
108
|
+
a.obs.loc[a.obs_names[::6], "status"] = np.nan
|
|
109
|
+
table, m = transfer_labels(a, "status")
|
|
110
|
+
q = table[table["query"]]
|
|
111
|
+
assert len(q) == 10 and (q["predicted"] == hidden[q.index]).all() and (q["confidence"] == 1).all()
|
|
112
|
+
assert m["loo_accuracy"] == 1.0 and m["n_reference"] == 50 and m["n_query"] == 10
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_transfer_mask_vote_tie_and_weights():
|
|
116
|
+
"""Query at 0; references at 1 (A), 2 (B), 3 (B), 4 (A). k=4 is a 2:2 tie -> nearest (A).
|
|
117
|
+
k=3: majority B (2/3); distance-weighted A wins (1 vs 1/2 + 1/3)."""
|
|
118
|
+
X = np.array([[0.0], [1.0], [2.0], [3.0], [4.0]])
|
|
119
|
+
a = make(X, cls=["A", "A", "B", "B", "A"])
|
|
120
|
+
query = np.array([True, False, False, False, False]) # labelled but masked
|
|
121
|
+
table, _ = transfer_labels(a, "cls", query=query, n_neighbors=4)
|
|
122
|
+
assert table.loc["s0", "predicted"] == "A" and table.loc["s0", "confidence"] == 0.5
|
|
123
|
+
table, _ = transfer_labels(a, "cls", query=query, n_neighbors=3)
|
|
124
|
+
assert table.loc["s0", "predicted"] == "B" and np.isclose(table.loc["s0", "confidence"], 2 / 3)
|
|
125
|
+
table, _ = transfer_labels(a, "cls", query=query, n_neighbors=3, weighted=True)
|
|
126
|
+
assert table.loc["s0", "predicted"] == "A" and np.isclose(table.loc["s0", "confidence"], 1 / (1 + 1 / 2 + 1 / 3))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_transfer_loo_excludes_group():
|
|
130
|
+
"""Paired units (one sample in two modalities) sit on top of each other: without group_col each
|
|
131
|
+
unit's nearest reference is its partner, so leave-one-out accuracy is inflated."""
|
|
132
|
+
rng = np.random.default_rng(4)
|
|
133
|
+
centres = rng.normal(size=(40, 3)) * 5
|
|
134
|
+
lab = rng.choice(["x", "y"], 40) # unrelated to position
|
|
135
|
+
X = np.repeat(centres, 2, axis=0) + rng.normal(size=(80, 3)) * 0.01
|
|
136
|
+
a = make(X, sample=np.repeat([f"p{i}" for i in range(40)], 2), cls=np.repeat(lab, 2))
|
|
137
|
+
_, leaky = transfer_labels(a, "cls", n_neighbors=1)
|
|
138
|
+
_, fair = transfer_labels(a, "cls", n_neighbors=1, group_col="sample")
|
|
139
|
+
assert leaky["loo_accuracy"] == 1.0 and fair["loo_accuracy"] < 0.75
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_numeric_labels_keep_dtype():
|
|
143
|
+
a = separated()
|
|
144
|
+
a.obs["sev"] = np.where(a.obs["status"] == "severe", 4.0, 1.0)
|
|
145
|
+
a.obs.loc["s3", "sev"] = np.nan
|
|
146
|
+
table, m = transfer_labels(a, "sev")
|
|
147
|
+
assert table["predicted"].dtype == float and table.loc["s3", "predicted"] == 1.0 and m["loo_accuracy"] == 1.0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
for name, fn in list(globals().items()):
|
|
152
|
+
if name.startswith("test_"):
|
|
153
|
+
fn()
|
|
154
|
+
print("ok", name)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/ATAC_peak_annotation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/gene_activity/test_pseudo_correlation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_batch_correction.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_cell_type_cpu.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/preparation/multi_omics_cell_type_gpu.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_clustering/proportion_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/sample_embedding.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_embedding/sample_embedding_gpu.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/sample_trajectory/trajectory_diff_gene.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/utils/imbalance_cell_type_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/multi_omics_visualization.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_embedding.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_helper.py
RENAMED
|
File without changes
|
{sampledisco-0.4.2 → sampledisco-0.5.0}/src/sampledisco/visualization/visualization_other.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|