cytocommunity2 1.0.2__tar.gz → 1.0.4__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.
- {cytocommunity2-1.0.2/src/cytocommunity2.egg-info → cytocommunity2-1.0.4}/PKG-INFO +3 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/README.md +2 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/pyproject.toml +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/__init__.py +1 -1
- cytocommunity2-1.0.4/src/cytocommunity2/_io.py +11 -0
- cytocommunity2-1.0.4/src/cytocommunity2/colors.py +34 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/config.py +10 -17
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/__init__.py +1 -5
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/coherence.py +5 -9
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/between.py +14 -24
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/cca.py +41 -36
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/within.py +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/differential_composition.py +15 -113
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/dominant_celltype.py +7 -19
- cytocommunity2-1.0.4/src/cytocommunity2/downstream/enrichment.py +176 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/moran.py +11 -19
- cytocommunity2-1.0.4/src/cytocommunity2/downstream/plotting/cn_selection.py +43 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/coherence.py +6 -15
- cytocommunity2-1.0.4/src/cytocommunity2/downstream/plotting/common.py +146 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/communication.py +48 -61
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dominant.py +15 -24
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dotplots.py +64 -125
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/recurrence.py +65 -107
- cytocommunity2-1.0.4/src/cytocommunity2/downstream/recurrence.py +112 -0
- cytocommunity2-1.0.4/src/cytocommunity2/downstream/wilcoxon.py +51 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/ensemble.py +9 -25
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/api.py +2 -3
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/dataset.py +3 -14
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/model.py +2 -2
- cytocommunity2-1.0.4/src/cytocommunity2/learning/selection.py +128 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/training.py +2 -2
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/paths.py +2 -2
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/spatial.py +3 -17
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4/src/cytocommunity2.egg-info}/PKG-INFO +3 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/SOURCES.txt +4 -2
- cytocommunity2-1.0.2/src/cytocommunity2/colors.py +0 -19
- cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -63
- cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/common.py +0 -79
- cytocommunity2-1.0.2/src/cytocommunity2/downstream/plotting/composition.py +0 -80
- cytocommunity2-1.0.2/src/cytocommunity2/downstream/runtime.py +0 -13
- cytocommunity2-1.0.2/src/cytocommunity2/learning/selection.py +0 -375
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/LICENSE +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/setup.cfg +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cytocommunity2
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Weakly supervised cellular-neighborhood learning for spatial omics
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -30,6 +30,8 @@ Dynamic: license-file
|
|
|
30
30
|
|
|
31
31
|
## Overview
|
|
32
32
|
|
|
33
|
+

|
|
34
|
+
|
|
33
35
|
To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
|
|
34
36
|
present CytoCommunity2, **a unified weakly-supervised framework** for
|
|
35
37
|
identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
|
|
14
16
|
present CytoCommunity2, **a unified weakly-supervised framework** for
|
|
15
17
|
identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared categorical colors used across CytoCommunity2 figures."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
CN_PALETTE = "d3_category20"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def cn_color_map(cns):
|
|
8
|
+
"""Map CN numbers or CN-prefixed labels to fixed spatial/OC colors."""
|
|
9
|
+
import sci_palettes
|
|
10
|
+
|
|
11
|
+
palette = list(sci_palettes.PALETTES[CN_PALETTE].values())
|
|
12
|
+
colors = {}
|
|
13
|
+
for cn in cns:
|
|
14
|
+
number = int(str(cn).removeprefix("CN"))
|
|
15
|
+
if not 1 <= number <= len(palette):
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"CN {cn!r} is outside the 1..{len(palette)} range of "
|
|
18
|
+
f"palette {CN_PALETTE!r}."
|
|
19
|
+
)
|
|
20
|
+
colors[cn] = palette[number - 1]
|
|
21
|
+
return colors
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def celltype_color_map(celltypes):
|
|
25
|
+
"""Return the canonical cell-type colors in the supplied display order."""
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
|
|
28
|
+
palette = []
|
|
29
|
+
for name in ("tab20", "tab20b", "tab20c", "Set1", "Dark2", "Accent", "Paired"):
|
|
30
|
+
palette.extend(plt.get_cmap(name).colors)
|
|
31
|
+
return {
|
|
32
|
+
celltype: palette[index]
|
|
33
|
+
for index, celltype in enumerate(celltypes)
|
|
34
|
+
}
|
|
@@ -9,11 +9,10 @@ from typing import Any, Mapping
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_CONFIG_FILE = "hyperparameters.json"
|
|
11
11
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
FIXED_DOWNSTREAM = {
|
|
12
|
+
# Statistical thresholds and numerical settings for downstream analyses.
|
|
13
|
+
DOWNSTREAM_PARAMETERS = {
|
|
15
14
|
"dominant_cell_type": {"frequency_threshold": 0.5},
|
|
16
|
-
"
|
|
15
|
+
"differential_composition": {"p_value_threshold": 0.05, "minimum_samples": 3},
|
|
17
16
|
"moran": {
|
|
18
17
|
"minimum_positive_count": 2,
|
|
19
18
|
"minimum_cn_cells": 10,
|
|
@@ -27,9 +26,10 @@ FIXED_DOWNSTREAM = {
|
|
|
27
26
|
"cca_p_threshold": 0.05,
|
|
28
27
|
"cca_minimum_common_samples": 3,
|
|
29
28
|
"cca_minimum_abs_rho": 0.0,
|
|
30
|
-
"
|
|
29
|
+
"cca_min_std": 1e-8,
|
|
31
30
|
"cca_max_iterations": 1000,
|
|
32
|
-
"
|
|
31
|
+
"cca_n_components": 2,
|
|
32
|
+
"cca_top_celltypes": 5,
|
|
33
33
|
},
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -40,7 +40,7 @@ class MainConfig:
|
|
|
40
40
|
cn_num_min: int
|
|
41
41
|
cn_num_max: int
|
|
42
42
|
final_num_runs: int
|
|
43
|
-
|
|
43
|
+
num_epochs: int
|
|
44
44
|
embedding_dimension: int
|
|
45
45
|
learning_rate: float
|
|
46
46
|
mini_batch_size: int
|
|
@@ -82,17 +82,10 @@ class CytoCommunityConfig:
|
|
|
82
82
|
return _load_config(config_file, config_class=cls)
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def _require_keys(values, required, section):
|
|
86
|
-
missing = [key for key in required if key not in values]
|
|
87
|
-
if missing:
|
|
88
|
-
raise ValueError(f"Missing configuration keys in {section}: {missing}")
|
|
89
|
-
|
|
90
|
-
|
|
91
85
|
def _load_config(config_file=None, config_class=CytoCommunityConfig):
|
|
92
86
|
"""Load one configuration file and resolve its configured input folder."""
|
|
93
87
|
config_file = Path(config_file or DEFAULT_CONFIG_FILE).expanduser().resolve()
|
|
94
88
|
raw = json.loads(config_file.read_text(encoding="utf-8"))
|
|
95
|
-
_require_keys(raw, ["input_dir", "random_seed", "main", "downstream"], "root")
|
|
96
89
|
|
|
97
90
|
project_root = config_file.parent
|
|
98
91
|
input_dir = Path(raw["input_dir"]).expanduser()
|
|
@@ -104,14 +97,14 @@ def _load_config(config_file=None, config_class=CytoCommunityConfig):
|
|
|
104
97
|
recurrence = RecurrenceConfig()
|
|
105
98
|
visualization = VisualizationConfig(**raw.get("visualization", {}))
|
|
106
99
|
downstream = dict(raw["downstream"])
|
|
107
|
-
for section,
|
|
108
|
-
downstream[section] = {**downstream.get(section, {}), **
|
|
100
|
+
for section, parameters in DOWNSTREAM_PARAMETERS.items():
|
|
101
|
+
downstream[section] = {**downstream.get(section, {}), **parameters}
|
|
109
102
|
|
|
110
103
|
if main.knn_k < 1:
|
|
111
104
|
raise ValueError("main.knn_k must be positive")
|
|
112
105
|
if main.cn_num_min < 1 or main.cn_num_min > main.cn_num_max:
|
|
113
106
|
raise ValueError("Require 1 <= main.cn_num_min <= main.cn_num_max")
|
|
114
|
-
if main.final_num_runs < 1 or main.
|
|
107
|
+
if main.final_num_runs < 1 or main.num_epochs < 1:
|
|
115
108
|
raise ValueError("Run and epoch counts must be positive")
|
|
116
109
|
if main.embedding_dimension < 1 or main.mini_batch_size < 1:
|
|
117
110
|
raise ValueError("Embedding dimension and mini-batch size must be positive")
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
The top-level :mod:`cytocommunity2` API intentionally remains small. These
|
|
4
|
-
functions live in a dedicated namespace because downstream analyses are
|
|
5
|
-
optional and evolve independently from model training.
|
|
1
|
+
"""Downstream analyses of learned cellular neighborhoods.
|
|
6
2
|
"""
|
|
7
3
|
|
|
8
4
|
from .coherence import run_coherence_analysis
|
|
@@ -20,15 +20,10 @@ class CoherenceMetrics:
|
|
|
20
20
|
pas: float
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _coherence_inputs(cluster_labels, coordinates):
|
|
24
|
-
labels = np.asarray(cluster_labels)
|
|
25
|
-
locations = np.asarray(coordinates, dtype=float)
|
|
26
|
-
return labels, locations
|
|
27
|
-
|
|
28
|
-
|
|
29
23
|
def compute_chaos(cluster_labels, coordinates):
|
|
30
24
|
"""Compute the normalized sum of within-CN nearest-neighbor distances."""
|
|
31
|
-
labels
|
|
25
|
+
labels = np.asarray(cluster_labels)
|
|
26
|
+
locations = np.asarray(coordinates, dtype=float)
|
|
32
27
|
locations = StandardScaler().fit_transform(locations)
|
|
33
28
|
distances = []
|
|
34
29
|
for label in np.unique(labels):
|
|
@@ -43,7 +38,8 @@ def compute_chaos(cluster_labels, coordinates):
|
|
|
43
38
|
|
|
44
39
|
def compute_pas(cluster_labels, coordinates, *, k=10):
|
|
45
40
|
"""Compute the percentage of abnormal spots (PAS) metric."""
|
|
46
|
-
labels
|
|
41
|
+
labels = np.asarray(cluster_labels)
|
|
42
|
+
locations = np.asarray(coordinates, dtype=float)
|
|
47
43
|
if not 1 <= k < len(labels):
|
|
48
44
|
raise ValueError("k must satisfy 1 <= k < number of cells")
|
|
49
45
|
neighbors = NearestNeighbors(n_neighbors=k + 1, algorithm="kd_tree").fit(
|
|
@@ -93,5 +89,5 @@ def run_coherence_analysis(config: CytoCommunityConfig, *, output_dir=None, pas_
|
|
|
93
89
|
output_dir = Path(output_dir or paths.downstream_data / "Coherence")
|
|
94
90
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
95
91
|
result = analyze_result_tables(paths.result_tables, pas_k=pas_k)
|
|
96
|
-
result.to_csv(output_dir / "
|
|
92
|
+
result.to_csv(output_dir / "CoherenceMetrics.csv", index=False)
|
|
97
93
|
return result
|
{cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/between.py
RENAMED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
import numpy as np
|
|
6
5
|
import pandas as pd
|
|
7
6
|
from scipy.stats import spearmanr
|
|
8
7
|
|
|
@@ -27,26 +26,22 @@ def select_cca_pairs(cca_pairs, top_n):
|
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
def analyze_between_cn(
|
|
30
|
-
scores_long, cca_pairs,
|
|
29
|
+
scores_long, cca_pairs, loading_tables, *, minimum_samples, top_n_cca_pairs
|
|
31
30
|
):
|
|
32
|
-
"""Calculate between-CN correlations without filesystem access.
|
|
33
|
-
|
|
34
|
-
``coordinate_tables`` maps ``(CN_A, CN_B)`` string tuples to coordinate
|
|
35
|
-
DataFrames returned by :func:`cca_coordinate_tables`.
|
|
36
|
-
"""
|
|
31
|
+
"""Calculate between-CN correlations without filesystem access."""
|
|
37
32
|
scores = normalize_scores_long(scores_long)
|
|
38
33
|
pairs = select_cca_pairs(cca_pairs, top_n_cca_pairs)
|
|
39
34
|
rows = []
|
|
40
35
|
for _, pair in pairs.iterrows():
|
|
41
36
|
cn_a, cn_b = str(pair["CN_A"]), str(pair["CN_B"])
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
top_a_index =
|
|
47
|
-
top_b_index =
|
|
48
|
-
top_a =
|
|
49
|
-
top_b =
|
|
37
|
+
loadings = loading_tables[(cn_a, cn_b)].copy()
|
|
38
|
+
loadings["CellType"] = loadings["CellType"].astype(str).str.strip()
|
|
39
|
+
loadings_a = loadings.dropna(subset=["CN_A_loading1"])
|
|
40
|
+
loadings_b = loadings.dropna(subset=["CN_B_loading1"])
|
|
41
|
+
top_a_index = loadings_a["CN_A_loading1"].abs().idxmax()
|
|
42
|
+
top_b_index = loadings_b["CN_B_loading1"].abs().idxmax()
|
|
43
|
+
top_a = loadings_a.loc[top_a_index, "CellType"]
|
|
44
|
+
top_b = loadings_b.loc[top_b_index, "CellType"]
|
|
50
45
|
scores_a = scores.loc[
|
|
51
46
|
(scores["CN"] == cn_a) & (scores["CellType"] == top_a),
|
|
52
47
|
["Sample", "Score"],
|
|
@@ -70,13 +65,8 @@ def analyze_between_cn(
|
|
|
70
65
|
"CN_A": cn_a,
|
|
71
66
|
"CN_B": cn_b,
|
|
72
67
|
"rho1_CCA": pair["rho1"],
|
|
73
|
-
"pval1_CCA": pair["pval1"],
|
|
74
|
-
"rho2_CCA": pair["rho2"] if "rho2" in pairs else np.nan,
|
|
75
|
-
"pval2_CCA": pair["pval2"] if "pval2" in pairs else np.nan,
|
|
76
68
|
"Top_CellType_A": top_a,
|
|
77
69
|
"Top_CellType_B": top_b,
|
|
78
|
-
"TopA_coord_can1": coordinates_a.loc[top_a_index, "CN_A_coord_can1"],
|
|
79
|
-
"TopB_coord_can1": coordinates_b.loc[top_b_index, "CN_B_coord_can1"],
|
|
80
70
|
"Spearman_rho": rho,
|
|
81
71
|
"Spearman_p": p_value,
|
|
82
72
|
"N_overlap": len(merged),
|
|
@@ -86,19 +76,19 @@ def analyze_between_cn(
|
|
|
86
76
|
if not result.empty:
|
|
87
77
|
result = result.sort_values(
|
|
88
78
|
"rho1_CCA", key=lambda values: values.abs(), ascending=False
|
|
89
|
-
)
|
|
79
|
+
).drop(columns="rho1_CCA")
|
|
90
80
|
return result
|
|
91
81
|
|
|
92
82
|
|
|
93
83
|
def run_between_cn_statistics(config_dir, minimum_samples, top_n_cca_pairs):
|
|
94
|
-
"""
|
|
84
|
+
"""Calculate and save Spearman statistics for top CCA pairs."""
|
|
95
85
|
config_dir = Path(config_dir)
|
|
96
86
|
scores = pd.read_csv(config_dir / "EnrichScoreMatrix_long.csv")
|
|
97
|
-
pairs = pd.read_csv(config_dir / "
|
|
87
|
+
pairs = pd.read_csv(config_dir / "CCA_results.csv")
|
|
98
88
|
selected = select_cca_pairs(pairs, top_n_cca_pairs)
|
|
99
89
|
tables = {
|
|
100
90
|
(str(row.CN_A), str(row.CN_B)): pd.read_csv(
|
|
101
|
-
config_dir / f"
|
|
91
|
+
config_dir / f"CCA_loadings_CN{row.CN_A}_vs_CN{row.CN_B}.csv"
|
|
102
92
|
)
|
|
103
93
|
for row in selected.itertuples(index=False)
|
|
104
94
|
}
|
{cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/cca.py
RENAMED
|
@@ -20,7 +20,7 @@ def pair_rng(base_seed, condition, cn_a, cn_b):
|
|
|
20
20
|
return np.random.default_rng(seed)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def clean_cca_data(x, y, minimum_common_samples,
|
|
23
|
+
def clean_cca_data(x, y, minimum_common_samples, min_std):
|
|
24
24
|
"""Align two matrices and remove incomplete or near-constant variables."""
|
|
25
25
|
if x is None or y is None or x.shape[1] == 0 or y.shape[1] == 0:
|
|
26
26
|
return None
|
|
@@ -29,15 +29,13 @@ def clean_cca_data(x, y, minimum_common_samples, sd_epsilon):
|
|
|
29
29
|
return None
|
|
30
30
|
x = x.loc[common]
|
|
31
31
|
y = y.loc[common]
|
|
32
|
-
x = x.loc[:, x.
|
|
33
|
-
y = y.loc[:, y.
|
|
34
|
-
x = x.loc[:, x.std(axis=0) > sd_epsilon]
|
|
35
|
-
y = y.loc[:, y.std(axis=0) > sd_epsilon]
|
|
32
|
+
x = x.loc[:, x.std(axis=0) > min_std]
|
|
33
|
+
y = y.loc[:, y.std(axis=0) > min_std]
|
|
36
34
|
if x.shape[1] == 0 or y.shape[1] == 0:
|
|
37
35
|
return None
|
|
38
36
|
complete = x.notna().all(axis=1) & y.notna().all(axis=1)
|
|
39
37
|
x, y = x.loc[complete], y.loc[complete]
|
|
40
|
-
if x.shape[0] < minimum_common_samples
|
|
38
|
+
if x.shape[0] < minimum_common_samples:
|
|
41
39
|
return None
|
|
42
40
|
return {
|
|
43
41
|
"X": x.values,
|
|
@@ -62,10 +60,23 @@ def build_score_list(frame, cn_list):
|
|
|
62
60
|
return matrices
|
|
63
61
|
|
|
64
62
|
|
|
63
|
+
def select_top_features(matrix, top_n=5):
|
|
64
|
+
"""Select cell types by decreasing mean enrichment before variance filtering."""
|
|
65
|
+
if matrix is None or matrix.shape[1] == 0:
|
|
66
|
+
return matrix
|
|
67
|
+
means = matrix.mean(axis=0).fillna(-np.inf)
|
|
68
|
+
selected = means.sort_values(ascending=False, kind="stable").head(top_n).index
|
|
69
|
+
return matrix.loc[:, selected]
|
|
70
|
+
|
|
71
|
+
|
|
65
72
|
def run_cca_for_scores(
|
|
66
73
|
score_list, cn_list, communication_params, random_seed, condition=None
|
|
67
74
|
):
|
|
68
75
|
"""Run CCA and permutation tests on in-memory enrichment matrices."""
|
|
76
|
+
score_list = {
|
|
77
|
+
cn: select_top_features(matrix, communication_params["cca_top_celltypes"])
|
|
78
|
+
for cn, matrix in score_list.items()
|
|
79
|
+
}
|
|
69
80
|
result_rows = []
|
|
70
81
|
cache = {}
|
|
71
82
|
for cn_a, cn_b in combinations(map(str, cn_list), 2):
|
|
@@ -75,7 +86,7 @@ def run_cca_for_scores(
|
|
|
75
86
|
score_list[cn_a],
|
|
76
87
|
score_list[cn_b],
|
|
77
88
|
communication_params["cca_minimum_common_samples"],
|
|
78
|
-
communication_params["
|
|
89
|
+
communication_params["cca_min_std"],
|
|
79
90
|
)
|
|
80
91
|
if cleaned is None:
|
|
81
92
|
continue
|
|
@@ -93,7 +104,7 @@ def run_cca_for_scores(
|
|
|
93
104
|
model.fit(x_matrix, y_matrix)
|
|
94
105
|
x_scores, y_scores = model.transform(x_matrix, y_matrix)
|
|
95
106
|
actual_components = min(
|
|
96
|
-
communication_params["
|
|
107
|
+
communication_params["cca_n_components"], x_scores.shape[1]
|
|
97
108
|
)
|
|
98
109
|
correlations = [
|
|
99
110
|
np.corrcoef(x_scores[:, index], y_scores[:, index])[0, 1]
|
|
@@ -111,7 +122,7 @@ def run_cca_for_scores(
|
|
|
111
122
|
shuffled_y = rng.permutation(y_matrix)
|
|
112
123
|
permutation_model = CCA(
|
|
113
124
|
n_components=min(
|
|
114
|
-
communication_params["
|
|
125
|
+
communication_params["cca_n_components"],
|
|
115
126
|
max_components,
|
|
116
127
|
),
|
|
117
128
|
scale=True,
|
|
@@ -143,7 +154,7 @@ def run_cca_for_scores(
|
|
|
143
154
|
row = {"CN_A": cn_a, "CN_B": cn_b, "n_samples": n_samples}
|
|
144
155
|
if condition is not None:
|
|
145
156
|
row["Condition"] = str(condition)
|
|
146
|
-
for component in range(communication_params["
|
|
157
|
+
for component in range(communication_params["cca_n_components"]):
|
|
147
158
|
row[f"rho{component + 1}"] = (
|
|
148
159
|
correlations[component] if component < len(correlations) else None
|
|
149
160
|
)
|
|
@@ -177,8 +188,8 @@ def run_cca_for_scores(
|
|
|
177
188
|
return {"results": results, "cache": cache}
|
|
178
189
|
|
|
179
190
|
|
|
180
|
-
def
|
|
181
|
-
results, cache, celltype_order,
|
|
191
|
+
def cca_loading_tables(
|
|
192
|
+
results, cache, celltype_order, n_components, condition=None
|
|
182
193
|
):
|
|
183
194
|
"""Convert fitted CCA scores into per-pair cell-type loading tables."""
|
|
184
195
|
tables = {}
|
|
@@ -198,47 +209,41 @@ def cca_coordinate_tables(
|
|
|
198
209
|
if cell_type in set(entry["X_ct"]) | set(entry["Y_ct"])
|
|
199
210
|
]
|
|
200
211
|
data = {"CellType": all_celltypes}
|
|
201
|
-
maximum = min(
|
|
212
|
+
maximum = min(n_components, entry["X_scores"].shape[1])
|
|
202
213
|
for component in range(maximum):
|
|
203
214
|
x_score = entry["X_scores"][:, component]
|
|
204
215
|
y_score = entry["Y_scores"][:, component]
|
|
205
216
|
x_loadings = {
|
|
206
217
|
cell_type: np.corrcoef(entry["X_mat"][:, index], x_score)[0, 1]
|
|
207
|
-
if len(x_score) > 1
|
|
208
|
-
else np.nan
|
|
209
218
|
for index, cell_type in enumerate(entry["X_ct"])
|
|
210
219
|
}
|
|
211
220
|
y_loadings = {
|
|
212
221
|
cell_type: np.corrcoef(entry["Y_mat"][:, index], y_score)[0, 1]
|
|
213
|
-
if len(y_score) > 1
|
|
214
|
-
else np.nan
|
|
215
222
|
for index, cell_type in enumerate(entry["Y_ct"])
|
|
216
223
|
}
|
|
217
|
-
data[f"
|
|
224
|
+
data[f"CN_A_loading{component + 1}"] = [
|
|
218
225
|
x_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
|
|
219
226
|
]
|
|
220
|
-
data[f"
|
|
227
|
+
data[f"CN_B_loading{component + 1}"] = [
|
|
221
228
|
y_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
|
|
222
229
|
]
|
|
223
|
-
data[f"rho{component + 1}"] = [row.get(f"rho{component + 1}", np.nan)] * len(all_celltypes)
|
|
224
|
-
data[f"pval{component + 1}"] = [row.get(f"pval{component + 1}", np.nan)] * len(all_celltypes)
|
|
225
230
|
tables[(cn_a, cn_b)] = pd.DataFrame(data)
|
|
226
231
|
return tables
|
|
227
232
|
|
|
228
233
|
|
|
229
|
-
def
|
|
230
|
-
results, cache, output_dir, celltype_order,
|
|
234
|
+
def write_cca_loadings(
|
|
235
|
+
results, cache, output_dir, celltype_order, n_components, condition=None
|
|
231
236
|
):
|
|
232
|
-
"""Write
|
|
237
|
+
"""Write per-pair cell-type CCA loading tables."""
|
|
233
238
|
output_dir = Path(output_dir)
|
|
234
239
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
-
tables =
|
|
236
|
-
results, cache, celltype_order,
|
|
240
|
+
tables = cca_loading_tables(
|
|
241
|
+
results, cache, celltype_order, n_components, condition
|
|
237
242
|
)
|
|
238
243
|
prefix = f"{condition}_" if condition is not None else ""
|
|
239
244
|
for (cn_a, cn_b), table in tables.items():
|
|
240
245
|
table.to_csv(
|
|
241
|
-
output_dir / f"
|
|
246
|
+
output_dir / f"CCA_loadings_{prefix}CN{cn_a}_vs_CN{cn_b}.csv",
|
|
242
247
|
index=False,
|
|
243
248
|
)
|
|
244
249
|
return tables
|
|
@@ -268,13 +273,13 @@ def run_cca_analysis(
|
|
|
268
273
|
pooled = run_cca_for_scores(
|
|
269
274
|
build_score_list(frame, cn_list), cn_list, params, config.random_seed
|
|
270
275
|
)
|
|
271
|
-
pooled["results"].to_csv(output_dir / "
|
|
272
|
-
|
|
276
|
+
pooled["results"].to_csv(output_dir / "CCA_results.csv", index=False)
|
|
277
|
+
write_cca_loadings(
|
|
273
278
|
pooled["results"],
|
|
274
279
|
pooled["cache"],
|
|
275
280
|
output_dir,
|
|
276
281
|
celltype_order,
|
|
277
|
-
params["
|
|
282
|
+
params["cca_n_components"],
|
|
278
283
|
)
|
|
279
284
|
condition_results = []
|
|
280
285
|
for condition in conditions:
|
|
@@ -286,13 +291,12 @@ def run_cca_analysis(
|
|
|
286
291
|
config.random_seed,
|
|
287
292
|
condition,
|
|
288
293
|
)
|
|
289
|
-
|
|
290
|
-
write_cca_coordinates(
|
|
294
|
+
write_cca_loadings(
|
|
291
295
|
result["results"],
|
|
292
296
|
result["cache"],
|
|
293
297
|
output_dir,
|
|
294
298
|
celltype_order,
|
|
295
|
-
params["
|
|
299
|
+
params["cca_n_components"],
|
|
296
300
|
condition,
|
|
297
301
|
)
|
|
298
302
|
condition_results.append(result["results"])
|
|
@@ -310,16 +314,17 @@ def run_cca_analysis(
|
|
|
310
314
|
key=lambda values: abs(values) if values.name == "rho1" else values,
|
|
311
315
|
ascending=[True, False],
|
|
312
316
|
)
|
|
313
|
-
by_condition.to_csv(output_dir / "
|
|
317
|
+
by_condition.to_csv(output_dir / "CCA_results_by_condition.csv", index=False)
|
|
314
318
|
return {"pooled": pooled["results"], "by_condition": by_condition}
|
|
315
319
|
|
|
316
320
|
|
|
317
321
|
__all__ = [
|
|
318
322
|
"build_score_list",
|
|
319
|
-
"
|
|
323
|
+
"cca_loading_tables",
|
|
320
324
|
"clean_cca_data",
|
|
321
325
|
"pair_rng",
|
|
322
326
|
"run_cca_analysis",
|
|
323
327
|
"run_cca_for_scores",
|
|
324
|
-
"
|
|
328
|
+
"select_top_features",
|
|
329
|
+
"write_cca_loadings",
|
|
325
330
|
]
|
{cytocommunity2-1.0.2 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/within.py
RENAMED
|
@@ -234,7 +234,7 @@ def run_within_cn_analysis(
|
|
|
234
234
|
}
|
|
235
235
|
outputs["pooled"].to_csv(output_dir / "SpearmanWithinCNs.csv", index=False)
|
|
236
236
|
for comparison in build_condition_comparisons(
|
|
237
|
-
scores["Condition"], config.downstream["
|
|
237
|
+
scores["Condition"], config.downstream["comparison_mode"]
|
|
238
238
|
):
|
|
239
239
|
result = compute_condition_comparison_stats(
|
|
240
240
|
wide_dict=wide,
|