cytocommunity2 1.0.1__tar.gz → 1.0.3__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.1/src/cytocommunity2.egg-info → cytocommunity2-1.0.3}/PKG-INFO +1 -1
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/pyproject.toml +1 -1
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/__init__.py +1 -1
- cytocommunity2-1.0.3/src/cytocommunity2/_io.py +11 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/colors.py +1 -6
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/config.py +9 -17
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/__init__.py +1 -5
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/coherence.py +5 -9
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/between.py +14 -24
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/cca.py +27 -36
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/within.py +1 -1
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/differential_composition.py +37 -110
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/dominant_celltype.py +22 -3
- cytocommunity2-1.0.3/src/cytocommunity2/downstream/enrichment.py +166 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/moran.py +9 -16
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/cn_selection.py +1 -6
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/coherence.py +2 -2
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/communication.py +22 -22
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/dominant.py +31 -5
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/dotplots.py +94 -26
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/recurrence.py +2 -2
- cytocommunity2-1.0.3/src/cytocommunity2/downstream/recurrence.py +112 -0
- cytocommunity2-1.0.3/src/cytocommunity2/downstream/wilcoxon.py +51 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/ensemble.py +9 -25
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/api.py +2 -3
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/dataset.py +3 -14
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/model.py +2 -2
- cytocommunity2-1.0.3/src/cytocommunity2/learning/selection.py +128 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/training.py +2 -2
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/paths.py +2 -2
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/visualization/spatial.py +1 -16
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3/src/cytocommunity2.egg-info}/PKG-INFO +1 -1
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/SOURCES.txt +4 -2
- cytocommunity2-1.0.1/src/cytocommunity2/downstream/plotting/composition.py +0 -80
- cytocommunity2-1.0.1/src/cytocommunity2/downstream/runtime.py +0 -13
- cytocommunity2-1.0.1/src/cytocommunity2/learning/selection.py +0 -348
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/LICENSE +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/README.md +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/setup.cfg +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/top_level.txt +0 -0
|
@@ -7,12 +7,7 @@ def celltype_color_map(celltypes):
|
|
|
7
7
|
|
|
8
8
|
palette = []
|
|
9
9
|
for name in ("tab20", "tab20b", "tab20c", "Set1", "Dark2", "Accent", "Paired"):
|
|
10
|
-
|
|
11
|
-
palette.extend(
|
|
12
|
-
list(cmap.colors)
|
|
13
|
-
if hasattr(cmap, "colors")
|
|
14
|
-
else [cmap(index / 255) for index in range(256)]
|
|
15
|
-
)
|
|
10
|
+
palette.extend(plt.get_cmap(name).colors)
|
|
16
11
|
return {
|
|
17
12
|
celltype: palette[index]
|
|
18
13
|
for index, celltype in enumerate(celltypes)
|
|
@@ -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,9 @@ 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,
|
|
33
32
|
},
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -40,7 +39,7 @@ class MainConfig:
|
|
|
40
39
|
cn_num_min: int
|
|
41
40
|
cn_num_max: int
|
|
42
41
|
final_num_runs: int
|
|
43
|
-
|
|
42
|
+
num_epochs: int
|
|
44
43
|
embedding_dimension: int
|
|
45
44
|
learning_rate: float
|
|
46
45
|
mini_batch_size: int
|
|
@@ -82,17 +81,10 @@ class CytoCommunityConfig:
|
|
|
82
81
|
return _load_config(config_file, config_class=cls)
|
|
83
82
|
|
|
84
83
|
|
|
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
84
|
def _load_config(config_file=None, config_class=CytoCommunityConfig):
|
|
92
85
|
"""Load one configuration file and resolve its configured input folder."""
|
|
93
86
|
config_file = Path(config_file or DEFAULT_CONFIG_FILE).expanduser().resolve()
|
|
94
87
|
raw = json.loads(config_file.read_text(encoding="utf-8"))
|
|
95
|
-
_require_keys(raw, ["input_dir", "random_seed", "main", "downstream"], "root")
|
|
96
88
|
|
|
97
89
|
project_root = config_file.parent
|
|
98
90
|
input_dir = Path(raw["input_dir"]).expanduser()
|
|
@@ -104,14 +96,14 @@ def _load_config(config_file=None, config_class=CytoCommunityConfig):
|
|
|
104
96
|
recurrence = RecurrenceConfig()
|
|
105
97
|
visualization = VisualizationConfig(**raw.get("visualization", {}))
|
|
106
98
|
downstream = dict(raw["downstream"])
|
|
107
|
-
for section,
|
|
108
|
-
downstream[section] = {**downstream.get(section, {}), **
|
|
99
|
+
for section, parameters in DOWNSTREAM_PARAMETERS.items():
|
|
100
|
+
downstream[section] = {**downstream.get(section, {}), **parameters}
|
|
109
101
|
|
|
110
102
|
if main.knn_k < 1:
|
|
111
103
|
raise ValueError("main.knn_k must be positive")
|
|
112
104
|
if main.cn_num_min < 1 or main.cn_num_min > main.cn_num_max:
|
|
113
105
|
raise ValueError("Require 1 <= main.cn_num_min <= main.cn_num_max")
|
|
114
|
-
if main.final_num_runs < 1 or main.
|
|
106
|
+
if main.final_num_runs < 1 or main.num_epochs < 1:
|
|
115
107
|
raise ValueError("Run and epoch counts must be positive")
|
|
116
108
|
if main.embedding_dimension < 1 or main.mini_batch_size < 1:
|
|
117
109
|
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.1 → cytocommunity2-1.0.3}/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.1 → cytocommunity2-1.0.3}/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,
|
|
@@ -75,7 +73,7 @@ def run_cca_for_scores(
|
|
|
75
73
|
score_list[cn_a],
|
|
76
74
|
score_list[cn_b],
|
|
77
75
|
communication_params["cca_minimum_common_samples"],
|
|
78
|
-
communication_params["
|
|
76
|
+
communication_params["cca_min_std"],
|
|
79
77
|
)
|
|
80
78
|
if cleaned is None:
|
|
81
79
|
continue
|
|
@@ -93,7 +91,7 @@ def run_cca_for_scores(
|
|
|
93
91
|
model.fit(x_matrix, y_matrix)
|
|
94
92
|
x_scores, y_scores = model.transform(x_matrix, y_matrix)
|
|
95
93
|
actual_components = min(
|
|
96
|
-
communication_params["
|
|
94
|
+
communication_params["cca_n_components"], x_scores.shape[1]
|
|
97
95
|
)
|
|
98
96
|
correlations = [
|
|
99
97
|
np.corrcoef(x_scores[:, index], y_scores[:, index])[0, 1]
|
|
@@ -111,7 +109,7 @@ def run_cca_for_scores(
|
|
|
111
109
|
shuffled_y = rng.permutation(y_matrix)
|
|
112
110
|
permutation_model = CCA(
|
|
113
111
|
n_components=min(
|
|
114
|
-
communication_params["
|
|
112
|
+
communication_params["cca_n_components"],
|
|
115
113
|
max_components,
|
|
116
114
|
),
|
|
117
115
|
scale=True,
|
|
@@ -143,7 +141,7 @@ def run_cca_for_scores(
|
|
|
143
141
|
row = {"CN_A": cn_a, "CN_B": cn_b, "n_samples": n_samples}
|
|
144
142
|
if condition is not None:
|
|
145
143
|
row["Condition"] = str(condition)
|
|
146
|
-
for component in range(communication_params["
|
|
144
|
+
for component in range(communication_params["cca_n_components"]):
|
|
147
145
|
row[f"rho{component + 1}"] = (
|
|
148
146
|
correlations[component] if component < len(correlations) else None
|
|
149
147
|
)
|
|
@@ -177,8 +175,8 @@ def run_cca_for_scores(
|
|
|
177
175
|
return {"results": results, "cache": cache}
|
|
178
176
|
|
|
179
177
|
|
|
180
|
-
def
|
|
181
|
-
results, cache, celltype_order,
|
|
178
|
+
def cca_loading_tables(
|
|
179
|
+
results, cache, celltype_order, n_components, condition=None
|
|
182
180
|
):
|
|
183
181
|
"""Convert fitted CCA scores into per-pair cell-type loading tables."""
|
|
184
182
|
tables = {}
|
|
@@ -198,47 +196,41 @@ def cca_coordinate_tables(
|
|
|
198
196
|
if cell_type in set(entry["X_ct"]) | set(entry["Y_ct"])
|
|
199
197
|
]
|
|
200
198
|
data = {"CellType": all_celltypes}
|
|
201
|
-
maximum = min(
|
|
199
|
+
maximum = min(n_components, entry["X_scores"].shape[1])
|
|
202
200
|
for component in range(maximum):
|
|
203
201
|
x_score = entry["X_scores"][:, component]
|
|
204
202
|
y_score = entry["Y_scores"][:, component]
|
|
205
203
|
x_loadings = {
|
|
206
204
|
cell_type: np.corrcoef(entry["X_mat"][:, index], x_score)[0, 1]
|
|
207
|
-
if len(x_score) > 1
|
|
208
|
-
else np.nan
|
|
209
205
|
for index, cell_type in enumerate(entry["X_ct"])
|
|
210
206
|
}
|
|
211
207
|
y_loadings = {
|
|
212
208
|
cell_type: np.corrcoef(entry["Y_mat"][:, index], y_score)[0, 1]
|
|
213
|
-
if len(y_score) > 1
|
|
214
|
-
else np.nan
|
|
215
209
|
for index, cell_type in enumerate(entry["Y_ct"])
|
|
216
210
|
}
|
|
217
|
-
data[f"
|
|
211
|
+
data[f"CN_A_loading{component + 1}"] = [
|
|
218
212
|
x_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
|
|
219
213
|
]
|
|
220
|
-
data[f"
|
|
214
|
+
data[f"CN_B_loading{component + 1}"] = [
|
|
221
215
|
y_loadings.get(cell_type, np.nan) for cell_type in all_celltypes
|
|
222
216
|
]
|
|
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
217
|
tables[(cn_a, cn_b)] = pd.DataFrame(data)
|
|
226
218
|
return tables
|
|
227
219
|
|
|
228
220
|
|
|
229
|
-
def
|
|
230
|
-
results, cache, output_dir, celltype_order,
|
|
221
|
+
def write_cca_loadings(
|
|
222
|
+
results, cache, output_dir, celltype_order, n_components, condition=None
|
|
231
223
|
):
|
|
232
|
-
"""Write
|
|
224
|
+
"""Write per-pair cell-type CCA loading tables."""
|
|
233
225
|
output_dir = Path(output_dir)
|
|
234
226
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
-
tables =
|
|
236
|
-
results, cache, celltype_order,
|
|
227
|
+
tables = cca_loading_tables(
|
|
228
|
+
results, cache, celltype_order, n_components, condition
|
|
237
229
|
)
|
|
238
230
|
prefix = f"{condition}_" if condition is not None else ""
|
|
239
231
|
for (cn_a, cn_b), table in tables.items():
|
|
240
232
|
table.to_csv(
|
|
241
|
-
output_dir / f"
|
|
233
|
+
output_dir / f"CCA_loadings_{prefix}CN{cn_a}_vs_CN{cn_b}.csv",
|
|
242
234
|
index=False,
|
|
243
235
|
)
|
|
244
236
|
return tables
|
|
@@ -268,13 +260,13 @@ def run_cca_analysis(
|
|
|
268
260
|
pooled = run_cca_for_scores(
|
|
269
261
|
build_score_list(frame, cn_list), cn_list, params, config.random_seed
|
|
270
262
|
)
|
|
271
|
-
pooled["results"].to_csv(output_dir / "
|
|
272
|
-
|
|
263
|
+
pooled["results"].to_csv(output_dir / "CCA_results.csv", index=False)
|
|
264
|
+
write_cca_loadings(
|
|
273
265
|
pooled["results"],
|
|
274
266
|
pooled["cache"],
|
|
275
267
|
output_dir,
|
|
276
268
|
celltype_order,
|
|
277
|
-
params["
|
|
269
|
+
params["cca_n_components"],
|
|
278
270
|
)
|
|
279
271
|
condition_results = []
|
|
280
272
|
for condition in conditions:
|
|
@@ -286,13 +278,12 @@ def run_cca_analysis(
|
|
|
286
278
|
config.random_seed,
|
|
287
279
|
condition,
|
|
288
280
|
)
|
|
289
|
-
|
|
290
|
-
write_cca_coordinates(
|
|
281
|
+
write_cca_loadings(
|
|
291
282
|
result["results"],
|
|
292
283
|
result["cache"],
|
|
293
284
|
output_dir,
|
|
294
285
|
celltype_order,
|
|
295
|
-
params["
|
|
286
|
+
params["cca_n_components"],
|
|
296
287
|
condition,
|
|
297
288
|
)
|
|
298
289
|
condition_results.append(result["results"])
|
|
@@ -310,16 +301,16 @@ def run_cca_analysis(
|
|
|
310
301
|
key=lambda values: abs(values) if values.name == "rho1" else values,
|
|
311
302
|
ascending=[True, False],
|
|
312
303
|
)
|
|
313
|
-
by_condition.to_csv(output_dir / "
|
|
304
|
+
by_condition.to_csv(output_dir / "CCA_results_by_condition.csv", index=False)
|
|
314
305
|
return {"pooled": pooled["results"], "by_condition": by_condition}
|
|
315
306
|
|
|
316
307
|
|
|
317
308
|
__all__ = [
|
|
318
309
|
"build_score_list",
|
|
319
|
-
"
|
|
310
|
+
"cca_loading_tables",
|
|
320
311
|
"clean_cca_data",
|
|
321
312
|
"pair_rng",
|
|
322
313
|
"run_cca_analysis",
|
|
323
314
|
"run_cca_for_scores",
|
|
324
|
-
"
|
|
315
|
+
"write_cca_loadings",
|
|
325
316
|
]
|
{cytocommunity2-1.0.1 → cytocommunity2-1.0.3}/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,
|
|
@@ -4,11 +4,11 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
|
-
from scipy import stats
|
|
8
7
|
|
|
9
8
|
from ..config import CytoCommunityConfig
|
|
10
9
|
from ..paths import build_paths
|
|
11
10
|
from .dominant_celltype import load_enrichment_matrices
|
|
11
|
+
from .wilcoxon import rank_sum_test
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def load_condition_enrichment(enrichment_dir, input_dir):
|
|
@@ -66,32 +66,26 @@ def build_label_comparisons(labels, mode):
|
|
|
66
66
|
raise ValueError(f"Unknown comparison mode: {mode}")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def composition_wilcoxon_tests(
|
|
70
|
-
|
|
69
|
+
def composition_wilcoxon_tests(
|
|
70
|
+
cell_type, target_values, reference_values, *, minimum_samples=3
|
|
71
|
+
):
|
|
72
|
+
"""Run one-sided Wilcoxon rank-sum tests, exact for combined N <= 30."""
|
|
71
73
|
target = np.asarray(target_values, dtype=float)
|
|
72
74
|
reference = np.asarray(reference_values, dtype=float)
|
|
73
|
-
if len(target) <
|
|
75
|
+
if len(target) < minimum_samples or len(reference) < minimum_samples:
|
|
74
76
|
return None
|
|
75
77
|
if not np.isfinite(target).all() or not np.isfinite(reference).all():
|
|
76
78
|
return None
|
|
77
|
-
|
|
78
|
-
target_rank_sum = float(pooled_ranks[: len(target)].sum())
|
|
79
|
+
p_values, target_rank_sum, test_name = rank_sum_test(target, reference)
|
|
79
80
|
|
|
80
81
|
rows = []
|
|
81
|
-
for alternative in (
|
|
82
|
-
test = stats.mannwhitneyu(
|
|
83
|
-
target,
|
|
84
|
-
reference,
|
|
85
|
-
alternative=alternative,
|
|
86
|
-
method="asymptotic",
|
|
87
|
-
use_continuity=True,
|
|
88
|
-
)
|
|
82
|
+
for alternative, p_value in p_values.items():
|
|
89
83
|
rows.append(
|
|
90
84
|
{
|
|
91
85
|
"CellType": cell_type,
|
|
92
86
|
"Alternative": alternative,
|
|
93
|
-
"p_value":
|
|
94
|
-
"Test":
|
|
87
|
+
"p_value": p_value,
|
|
88
|
+
"Test": test_name,
|
|
95
89
|
"W_statistic": target_rank_sum,
|
|
96
90
|
"N_target": len(target),
|
|
97
91
|
"N_reference": len(reference),
|
|
@@ -100,10 +94,12 @@ def composition_wilcoxon_tests(cell_type, target_values, reference_values):
|
|
|
100
94
|
return rows
|
|
101
95
|
|
|
102
96
|
|
|
103
|
-
def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
97
|
+
def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum_samples=3):
|
|
104
98
|
"""Calculate all directional composition comparisons in memory.
|
|
105
99
|
|
|
106
100
|
The result maps comparison names to maps of CN names and result tables.
|
|
101
|
+
Comparisons with fewer than ``minimum_samples`` samples on either side
|
|
102
|
+
have empty maps.
|
|
107
103
|
"""
|
|
108
104
|
normalized_records = [
|
|
109
105
|
{**record, "matrix": np.asarray(record["matrix"], dtype=float)}
|
|
@@ -114,8 +110,20 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
114
110
|
labels = sorted({record["condition"] for record in normalized_records})
|
|
115
111
|
comparisons = build_label_comparisons(labels, mode)
|
|
116
112
|
outputs = {comparison["name"]: {} for comparison in comparisons}
|
|
113
|
+
eligible = [
|
|
114
|
+
comparison
|
|
115
|
+
for comparison in comparisons
|
|
116
|
+
if sum(
|
|
117
|
+
record["condition"] in comparison["target_labels"]
|
|
118
|
+
for record in normalized_records
|
|
119
|
+
) >= minimum_samples
|
|
120
|
+
and sum(
|
|
121
|
+
record["condition"] in comparison["reference_labels"]
|
|
122
|
+
for record in normalized_records
|
|
123
|
+
) >= minimum_samples
|
|
124
|
+
]
|
|
117
125
|
for cn_index in range(expected_shape[1]):
|
|
118
|
-
for comparison in
|
|
126
|
+
for comparison in eligible:
|
|
119
127
|
rows = []
|
|
120
128
|
for cell_index, cell_type in enumerate(celltypes):
|
|
121
129
|
target = [
|
|
@@ -129,7 +137,7 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
129
137
|
if record["condition"] in comparison["reference_labels"]
|
|
130
138
|
]
|
|
131
139
|
result = composition_wilcoxon_tests(
|
|
132
|
-
cell_type, target, reference
|
|
140
|
+
cell_type, target, reference, minimum_samples=minimum_samples
|
|
133
141
|
)
|
|
134
142
|
if result is not None:
|
|
135
143
|
rows.extend(result)
|
|
@@ -141,104 +149,23 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
141
149
|
def run_differential_composition_analysis(
|
|
142
150
|
config: CytoCommunityConfig, *, output_dir=None
|
|
143
151
|
):
|
|
144
|
-
"""Run differential composition tests
|
|
152
|
+
"""Run and save condition-specific differential composition tests."""
|
|
145
153
|
paths = build_paths(config)
|
|
146
154
|
celltypes, records = load_condition_enrichment(
|
|
147
155
|
paths.enrichment, paths.input_dir
|
|
148
156
|
)
|
|
149
157
|
outputs = analyze_differential_composition(
|
|
150
|
-
records, celltypes, mode=config.downstream["
|
|
158
|
+
records, celltypes, mode=config.downstream["comparison_mode"],
|
|
159
|
+
minimum_samples=config.downstream["differential_composition"]["minimum_samples"],
|
|
151
160
|
)
|
|
152
161
|
output_dir = Path(output_dir or paths.downstream_data / "DifferentialComposed")
|
|
153
162
|
for comparison, cn_tables in outputs.items():
|
|
154
163
|
comparison_dir = output_dir / comparison
|
|
155
|
-
comparison_dir.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
if comparison_dir.exists():
|
|
165
|
+
for csv_file in comparison_dir.glob("CN*.csv"):
|
|
166
|
+
csv_file.unlink()
|
|
167
|
+
if cn_tables:
|
|
168
|
+
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
for cn, table in cn_tables.items():
|
|
170
|
+
table.to_csv(comparison_dir / f"{cn}_{comparison}.csv", index=False)
|
|
160
171
|
return outputs
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def comparison_label(record, condition_names):
|
|
164
|
-
target = condition_names.get(record["target"], record["target"])
|
|
165
|
-
reference = record["reference"]
|
|
166
|
-
if reference == "rest":
|
|
167
|
-
return f"{target} vs. other cancers"
|
|
168
|
-
return f"{target} vs. {condition_names.get(reference, reference)}"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def calculate_cn_variation_specificity(
|
|
172
|
-
records, celltypes, condition_names, *, p_value_threshold
|
|
173
|
-
):
|
|
174
|
-
"""Summarize significant differential-composition features (DCFs)."""
|
|
175
|
-
summary_rows = []
|
|
176
|
-
detail_rows = []
|
|
177
|
-
for record in records:
|
|
178
|
-
effect = np.asarray(record["effect"], dtype=float)
|
|
179
|
-
pvalues = np.asarray(record["pvalues"], dtype=float)
|
|
180
|
-
significant = np.isfinite(effect) & np.isfinite(pvalues) & (
|
|
181
|
-
pvalues < p_value_threshold
|
|
182
|
-
)
|
|
183
|
-
dcf_rows = [
|
|
184
|
-
{
|
|
185
|
-
"CellType": celltypes[celltype_index],
|
|
186
|
-
"Direction": "Up" if effect[celltype_index, cn_index] > 0 else "Down",
|
|
187
|
-
"CN": f"CN{cn_index + 1}",
|
|
188
|
-
"Significance": -np.log10(
|
|
189
|
-
np.clip(pvalues[celltype_index, cn_index], 1e-300, 1.0)
|
|
190
|
-
),
|
|
191
|
-
}
|
|
192
|
-
for celltype_index, cn_index in np.argwhere(significant)
|
|
193
|
-
]
|
|
194
|
-
dcf_table = pd.DataFrame(
|
|
195
|
-
dcf_rows, columns=["CellType", "Direction", "CN", "Significance"]
|
|
196
|
-
)
|
|
197
|
-
number_dcf = len(dcf_table)
|
|
198
|
-
number_patterns = (
|
|
199
|
-
dcf_table[["CellType", "Direction"]].drop_duplicates().shape[0]
|
|
200
|
-
if number_dcf
|
|
201
|
-
else 0
|
|
202
|
-
)
|
|
203
|
-
label = comparison_label(record, condition_names)
|
|
204
|
-
summary_rows.append(
|
|
205
|
-
{
|
|
206
|
-
"Comparison": record["name"],
|
|
207
|
-
"CancerComparison": label,
|
|
208
|
-
"NumberDCF": number_dcf,
|
|
209
|
-
"NumberCellTypeDirectionPatterns": number_patterns,
|
|
210
|
-
"SpecificityOfCNVariation": (
|
|
211
|
-
number_patterns / number_dcf if number_dcf else np.nan
|
|
212
|
-
),
|
|
213
|
-
"MeanDCFSignificance": (
|
|
214
|
-
dcf_table["Significance"].mean() if number_dcf else np.nan
|
|
215
|
-
),
|
|
216
|
-
}
|
|
217
|
-
)
|
|
218
|
-
if number_dcf:
|
|
219
|
-
patterns = (
|
|
220
|
-
dcf_table.groupby(["CellType", "Direction"], sort=False)
|
|
221
|
-
.agg(
|
|
222
|
-
NumberDCF=("CN", "size"),
|
|
223
|
-
MeanDCFSignificance=("Significance", "mean"),
|
|
224
|
-
)
|
|
225
|
-
.reset_index()
|
|
226
|
-
)
|
|
227
|
-
patterns.insert(0, "CancerComparison", label)
|
|
228
|
-
patterns.insert(0, "Comparison", record["name"])
|
|
229
|
-
detail_rows.extend(patterns.to_dict("records"))
|
|
230
|
-
return (
|
|
231
|
-
pd.DataFrame(summary_rows),
|
|
232
|
-
pd.DataFrame(
|
|
233
|
-
detail_rows,
|
|
234
|
-
columns=[
|
|
235
|
-
"Comparison",
|
|
236
|
-
"CancerComparison",
|
|
237
|
-
"CellType",
|
|
238
|
-
"Direction",
|
|
239
|
-
"NumberDCF",
|
|
240
|
-
"MeanDCFSignificance",
|
|
241
|
-
],
|
|
242
|
-
),
|
|
243
|
-
)
|
|
244
|
-
|