cytocommunity2 1.0.2__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.2/src/cytocommunity2.egg-info → cytocommunity2-1.0.3}/PKG-INFO +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/pyproject.toml +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/__init__.py +1 -1
- cytocommunity2-1.0.3/src/cytocommunity2/_io.py +11 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/colors.py +1 -6
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/config.py +9 -17
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/__init__.py +1 -5
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/coherence.py +5 -9
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/between.py +14 -24
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/cca.py +27 -36
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/within.py +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/differential_composition.py +11 -103
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/dominant_celltype.py +3 -1
- cytocommunity2-1.0.3/src/cytocommunity2/downstream/enrichment.py +166 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/moran.py +9 -16
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/cn_selection.py +1 -6
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/coherence.py +2 -2
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/communication.py +22 -22
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/dotplots.py +15 -15
- {cytocommunity2-1.0.2 → 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.2 → cytocommunity2-1.0.3}/src/cytocommunity2/ensemble.py +9 -25
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/api.py +2 -3
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/dataset.py +3 -14
- {cytocommunity2-1.0.2 → 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.2 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/training.py +2 -2
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/paths.py +2 -2
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/visualization/spatial.py +1 -16
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3/src/cytocommunity2.egg-info}/PKG-INFO +1 -1
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/SOURCES.txt +4 -2
- 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.3}/LICENSE +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/README.md +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/setup.cfg +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/plotting/dominant.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-1.0.2 → 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.2 → 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.2 → 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.2 → 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):
|
|
@@ -69,31 +69,23 @@ def build_label_comparisons(labels, mode):
|
|
|
69
69
|
def composition_wilcoxon_tests(
|
|
70
70
|
cell_type, target_values, reference_values, *, minimum_samples=3
|
|
71
71
|
):
|
|
72
|
-
"""Run one-sided
|
|
72
|
+
"""Run one-sided Wilcoxon rank-sum tests, exact for combined N <= 30."""
|
|
73
73
|
target = np.asarray(target_values, dtype=float)
|
|
74
74
|
reference = np.asarray(reference_values, dtype=float)
|
|
75
75
|
if len(target) < minimum_samples or len(reference) < minimum_samples:
|
|
76
76
|
return None
|
|
77
77
|
if not np.isfinite(target).all() or not np.isfinite(reference).all():
|
|
78
78
|
return None
|
|
79
|
-
|
|
80
|
-
target_rank_sum = float(pooled_ranks[: len(target)].sum())
|
|
79
|
+
p_values, target_rank_sum, test_name = rank_sum_test(target, reference)
|
|
81
80
|
|
|
82
81
|
rows = []
|
|
83
|
-
for alternative in (
|
|
84
|
-
test = stats.mannwhitneyu(
|
|
85
|
-
target,
|
|
86
|
-
reference,
|
|
87
|
-
alternative=alternative,
|
|
88
|
-
method="asymptotic",
|
|
89
|
-
use_continuity=True,
|
|
90
|
-
)
|
|
82
|
+
for alternative, p_value in p_values.items():
|
|
91
83
|
rows.append(
|
|
92
84
|
{
|
|
93
85
|
"CellType": cell_type,
|
|
94
86
|
"Alternative": alternative,
|
|
95
|
-
"p_value":
|
|
96
|
-
"Test":
|
|
87
|
+
"p_value": p_value,
|
|
88
|
+
"Test": test_name,
|
|
97
89
|
"W_statistic": target_rank_sum,
|
|
98
90
|
"N_target": len(target),
|
|
99
91
|
"N_reference": len(reference),
|
|
@@ -157,107 +149,23 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum
|
|
|
157
149
|
def run_differential_composition_analysis(
|
|
158
150
|
config: CytoCommunityConfig, *, output_dir=None
|
|
159
151
|
):
|
|
160
|
-
"""Run differential composition tests
|
|
152
|
+
"""Run and save condition-specific differential composition tests."""
|
|
161
153
|
paths = build_paths(config)
|
|
162
154
|
celltypes, records = load_condition_enrichment(
|
|
163
155
|
paths.enrichment, paths.input_dir
|
|
164
156
|
)
|
|
165
157
|
outputs = analyze_differential_composition(
|
|
166
|
-
records, celltypes, mode=config.downstream["
|
|
167
|
-
minimum_samples=config.downstream["
|
|
158
|
+
records, celltypes, mode=config.downstream["comparison_mode"],
|
|
159
|
+
minimum_samples=config.downstream["differential_composition"]["minimum_samples"],
|
|
168
160
|
)
|
|
169
161
|
output_dir = Path(output_dir or paths.downstream_data / "DifferentialComposed")
|
|
170
162
|
for comparison, cn_tables in outputs.items():
|
|
171
163
|
comparison_dir = output_dir / comparison
|
|
172
164
|
if comparison_dir.exists():
|
|
173
|
-
for
|
|
174
|
-
|
|
165
|
+
for csv_file in comparison_dir.glob("CN*.csv"):
|
|
166
|
+
csv_file.unlink()
|
|
175
167
|
if cn_tables:
|
|
176
168
|
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
177
169
|
for cn, table in cn_tables.items():
|
|
178
170
|
table.to_csv(comparison_dir / f"{cn}_{comparison}.csv", index=False)
|
|
179
171
|
return outputs
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def comparison_label(record, condition_names):
|
|
183
|
-
target = condition_names.get(record["target"], record["target"])
|
|
184
|
-
reference = record["reference"]
|
|
185
|
-
if reference == "rest":
|
|
186
|
-
return f"{target} vs. other cancers"
|
|
187
|
-
return f"{target} vs. {condition_names.get(reference, reference)}"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def calculate_cn_variation_specificity(
|
|
191
|
-
records, celltypes, condition_names, *, p_value_threshold
|
|
192
|
-
):
|
|
193
|
-
"""Summarize significant differential-composition features (DCFs)."""
|
|
194
|
-
summary_rows = []
|
|
195
|
-
detail_rows = []
|
|
196
|
-
for record in records:
|
|
197
|
-
effect = np.asarray(record["effect"], dtype=float)
|
|
198
|
-
pvalues = np.asarray(record["pvalues"], dtype=float)
|
|
199
|
-
significant = np.isfinite(effect) & np.isfinite(pvalues) & (
|
|
200
|
-
pvalues < p_value_threshold
|
|
201
|
-
)
|
|
202
|
-
dcf_rows = [
|
|
203
|
-
{
|
|
204
|
-
"CellType": celltypes[celltype_index],
|
|
205
|
-
"Direction": "Up" if effect[celltype_index, cn_index] > 0 else "Down",
|
|
206
|
-
"CN": f"CN{cn_index + 1}",
|
|
207
|
-
"Significance": -np.log10(
|
|
208
|
-
np.clip(pvalues[celltype_index, cn_index], 1e-300, 1.0)
|
|
209
|
-
),
|
|
210
|
-
}
|
|
211
|
-
for celltype_index, cn_index in np.argwhere(significant)
|
|
212
|
-
]
|
|
213
|
-
dcf_table = pd.DataFrame(
|
|
214
|
-
dcf_rows, columns=["CellType", "Direction", "CN", "Significance"]
|
|
215
|
-
)
|
|
216
|
-
number_dcf = len(dcf_table)
|
|
217
|
-
number_patterns = (
|
|
218
|
-
dcf_table[["CellType", "Direction"]].drop_duplicates().shape[0]
|
|
219
|
-
if number_dcf
|
|
220
|
-
else 0
|
|
221
|
-
)
|
|
222
|
-
label = comparison_label(record, condition_names)
|
|
223
|
-
summary_rows.append(
|
|
224
|
-
{
|
|
225
|
-
"Comparison": record["name"],
|
|
226
|
-
"CancerComparison": label,
|
|
227
|
-
"NumberDCF": number_dcf,
|
|
228
|
-
"NumberCellTypeDirectionPatterns": number_patterns,
|
|
229
|
-
"SpecificityOfCNVariation": (
|
|
230
|
-
number_patterns / number_dcf if number_dcf else np.nan
|
|
231
|
-
),
|
|
232
|
-
"MeanDCFSignificance": (
|
|
233
|
-
dcf_table["Significance"].mean() if number_dcf else np.nan
|
|
234
|
-
),
|
|
235
|
-
}
|
|
236
|
-
)
|
|
237
|
-
if number_dcf:
|
|
238
|
-
patterns = (
|
|
239
|
-
dcf_table.groupby(["CellType", "Direction"], sort=False)
|
|
240
|
-
.agg(
|
|
241
|
-
NumberDCF=("CN", "size"),
|
|
242
|
-
MeanDCFSignificance=("Significance", "mean"),
|
|
243
|
-
)
|
|
244
|
-
.reset_index()
|
|
245
|
-
)
|
|
246
|
-
patterns.insert(0, "CancerComparison", label)
|
|
247
|
-
patterns.insert(0, "Comparison", record["name"])
|
|
248
|
-
detail_rows.extend(patterns.to_dict("records"))
|
|
249
|
-
return (
|
|
250
|
-
pd.DataFrame(summary_rows),
|
|
251
|
-
pd.DataFrame(
|
|
252
|
-
detail_rows,
|
|
253
|
-
columns=[
|
|
254
|
-
"Comparison",
|
|
255
|
-
"CancerComparison",
|
|
256
|
-
"CellType",
|
|
257
|
-
"Direction",
|
|
258
|
-
"NumberDCF",
|
|
259
|
-
"MeanDCFSignificance",
|
|
260
|
-
],
|
|
261
|
-
),
|
|
262
|
-
)
|
|
263
|
-
|
{cytocommunity2-1.0.2 → cytocommunity2-1.0.3}/src/cytocommunity2/downstream/dominant_celltype.py
RENAMED
|
@@ -98,7 +98,9 @@ def run_dominant_celltype_analysis(config: CytoCommunityConfig, *, output_dir=No
|
|
|
98
98
|
)
|
|
99
99
|
output_dir = Path(output_dir or paths.downstream_data / "DominateCT")
|
|
100
100
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
-
result.frequency.to_csv(
|
|
101
|
+
result.frequency.to_csv(
|
|
102
|
+
output_dir / "CN_CT_Frequency.csv", index_label="CellType", float_format="%.4f"
|
|
103
|
+
)
|
|
102
104
|
result.dominant_cell_types.to_csv(output_dir / "DominantCT.csv", index=False)
|
|
103
105
|
return result
|
|
104
106
|
|