cytocommunity2 1.0.7__tar.gz → 1.0.8__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.7/src/cytocommunity2.egg-info → cytocommunity2-1.0.8}/PKG-INFO +1 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/pyproject.toml +1 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/__init__.py +1 -1
- cytocommunity2-1.0.8/src/cytocommunity2/_display.py +23 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/colors.py +9 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/config.py +3 -2
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/between.py +23 -10
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/cca.py +92 -95
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/utils.py +1 -12
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/within.py +4 -10
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/differential_composition.py +21 -5
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/enrichment.py +18 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/moran.py +2 -2
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/__init__.py +2 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/cn_selection.py +2 -2
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/coherence.py +1 -1
- cytocommunity2-1.0.8/src/cytocommunity2/downstream/plotting/communication.py +123 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/communication_cca.py +44 -51
- cytocommunity2-1.0.8/src/cytocommunity2/downstream/plotting/communication_dumbbell.py +278 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/dominant.py +14 -4
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/dotplots.py +84 -45
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/recurrence.py +18 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/ensemble.py +6 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/api.py +10 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/dataset.py +8 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/selection.py +1 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/training.py +88 -65
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/paths.py +18 -2
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/visualization/spatial.py +7 -6
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8/src/cytocommunity2.egg-info}/PKG-INFO +1 -1
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2.egg-info/SOURCES.txt +2 -1
- cytocommunity2-1.0.7/src/cytocommunity2/downstream/plotting/communication.py +0 -121
- cytocommunity2-1.0.7/src/cytocommunity2/downstream/plotting/communication_stack.py +0 -217
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/LICENSE +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/README.md +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/setup.cfg +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/_io.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/__init__.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/coherence.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/dominant_celltype.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/recurrence.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/wilcoxon.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/learning/model.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Dataset-specific display labels, separate from statistical identifiers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def display_label_map(config, name, overrides=None):
|
|
5
|
+
visualization = getattr(config, "visualization", None)
|
|
6
|
+
labels = dict(getattr(visualization, name, {}) or {})
|
|
7
|
+
labels.update(overrides or {})
|
|
8
|
+
return {str(key): str(value) for key, value in labels.items()}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def celltype_label(value, config=None, *, labels=None, mathtext=True):
|
|
12
|
+
text = str(value)
|
|
13
|
+
text = display_label_map(config, "celltype_labels", labels).get(text, text)
|
|
14
|
+
# Arial lacks the superscript-plus glyph; mathtext keeps it readable.
|
|
15
|
+
return text.replace("⁺", r"$^{+}$") if mathtext else text
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def condition_label(value, config=None, *, labels=None):
|
|
19
|
+
text = str(value)
|
|
20
|
+
key = text.removeprefix("label")
|
|
21
|
+
mapping = display_label_map(config, "condition_labels", labels)
|
|
22
|
+
fallback = text if text == "rest" or text.startswith("label") else f"label{text}"
|
|
23
|
+
return mapping.get(text, mapping.get(key, fallback))
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
CN_PALETTE = "d3_category20"
|
|
5
|
+
TARGET_COLOR = "#CB3027"
|
|
6
|
+
REST_COLOR = "#466988"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def condition_comparison_colors(condition_a, condition_b):
|
|
10
|
+
"""Color the target red and rest/reference blue, independent of rho order."""
|
|
11
|
+
if str(condition_a).lower() == "rest":
|
|
12
|
+
return REST_COLOR, TARGET_COLOR
|
|
13
|
+
return TARGET_COLOR, REST_COLOR
|
|
5
14
|
|
|
6
15
|
|
|
7
16
|
def cn_color_map(cns):
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import math
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, Mapping
|
|
8
8
|
|
|
@@ -21,7 +21,6 @@ DOWNSTREAM_PARAMETERS = {
|
|
|
21
21
|
"communication": {
|
|
22
22
|
"minimum_samples": 3,
|
|
23
23
|
"permutation_max_attempt_multiplier": 20,
|
|
24
|
-
"spearman_p_threshold": 0.05,
|
|
25
24
|
"permutation_p_threshold": 0.05,
|
|
26
25
|
"cca_p_threshold": 0.05,
|
|
27
26
|
"cca_minimum_common_samples": 3,
|
|
@@ -62,6 +61,8 @@ class RecurrenceConfig:
|
|
|
62
61
|
@dataclass(frozen=True)
|
|
63
62
|
class VisualizationConfig:
|
|
64
63
|
point_size: float = 2.0
|
|
64
|
+
celltype_labels: Mapping[str, str] = field(default_factory=dict)
|
|
65
|
+
condition_labels: Mapping[str, str] = field(default_factory=dict)
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
@dataclass(frozen=True)
|
{cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/between.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
6
7
|
|
|
7
8
|
from ...config import CytoCommunityConfig
|
|
8
9
|
from ...paths import build_paths
|
|
@@ -24,6 +25,7 @@ def select_cca_pairs(cca_pairs, top_n):
|
|
|
24
25
|
pairs = cca_pairs.copy()
|
|
25
26
|
pairs["CN_A"] = pairs["CN_A"].astype(str).str.strip()
|
|
26
27
|
pairs["CN_B"] = pairs["CN_B"].astype(str).str.strip()
|
|
28
|
+
pairs = pairs.loc[np.isfinite(pairs["rho1"])].copy()
|
|
27
29
|
return pairs.reindex(
|
|
28
30
|
pairs["rho1"].abs().sort_values(ascending=False).index
|
|
29
31
|
).head(top_n)
|
|
@@ -42,8 +44,10 @@ def analyze_between_cn(
|
|
|
42
44
|
cn_a, cn_b = str(pair["CN_A"]), str(pair["CN_B"])
|
|
43
45
|
loadings = loading_tables[(cn_a, cn_b)].copy()
|
|
44
46
|
loadings["CellType"] = loadings["CellType"].astype(str).str.strip()
|
|
45
|
-
loadings_a = loadings.
|
|
46
|
-
loadings_b = loadings.
|
|
47
|
+
loadings_a = loadings.loc[np.isfinite(loadings["CN_A_loading1"])]
|
|
48
|
+
loadings_b = loadings.loc[np.isfinite(loadings["CN_B_loading1"])]
|
|
49
|
+
if loadings_a.empty or loadings_b.empty:
|
|
50
|
+
raise ValueError(f"No finite first canonical loadings for CN{cn_a} vs CN{cn_b}")
|
|
47
51
|
top_a_index = loadings_a["CN_A_loading1"].abs().idxmax()
|
|
48
52
|
top_b_index = loadings_b["CN_B_loading1"].abs().idxmax()
|
|
49
53
|
top_a = loadings_a.loc[top_a_index, "CellType"]
|
|
@@ -87,19 +91,24 @@ def analyze_between_cn(
|
|
|
87
91
|
return result.drop(columns="rho1_CCA")
|
|
88
92
|
|
|
89
93
|
|
|
90
|
-
def run_between_cn_statistics(
|
|
94
|
+
def run_between_cn_statistics(
|
|
95
|
+
cca_dir, minimum_samples, top_n_cca_pairs, *, input_file=None, output_dir=None
|
|
96
|
+
):
|
|
91
97
|
"""Calculate and save Spearman statistics for top CCA pairs."""
|
|
92
|
-
|
|
98
|
+
cca_dir = Path(cca_dir)
|
|
99
|
+
output_dir = Path(output_dir or cca_dir.parent)
|
|
100
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
93
101
|
scores = read_scores_long(
|
|
94
|
-
|
|
102
|
+
input_file or cca_dir.parent.parent / "EnrichScoreMatrix_long.csv",
|
|
103
|
+
require_condition=False,
|
|
95
104
|
)
|
|
96
105
|
pairs = pd.read_csv(
|
|
97
|
-
|
|
106
|
+
cca_dir / "CCA_results.csv", dtype={"CN_A": str, "CN_B": str}
|
|
98
107
|
)
|
|
99
108
|
selected = select_cca_pairs(pairs, top_n_cca_pairs)
|
|
100
109
|
tables = {
|
|
101
110
|
(str(row.CN_A), str(row.CN_B)): pd.read_csv(
|
|
102
|
-
|
|
111
|
+
cca_dir / f"CCA_loadings_CN{row.CN_A}_vs_CN{row.CN_B}.csv",
|
|
103
112
|
converters={"CellType": str.strip},
|
|
104
113
|
)
|
|
105
114
|
for row in selected.itertuples(index=False)
|
|
@@ -111,17 +120,21 @@ def run_between_cn_statistics(config_dir, minimum_samples, top_n_cca_pairs):
|
|
|
111
120
|
minimum_samples=minimum_samples,
|
|
112
121
|
top_n_cca_pairs=top_n_cca_pairs,
|
|
113
122
|
)
|
|
114
|
-
result.to_csv(
|
|
123
|
+
result.to_csv(output_dir / "Spearman_TopCCA_Pairs.csv", index=False)
|
|
115
124
|
return result
|
|
116
125
|
|
|
117
126
|
|
|
118
|
-
def run_between_cn_analysis(
|
|
127
|
+
def run_between_cn_analysis(
|
|
128
|
+
config: CytoCommunityConfig, *, input_file=None, output_dir=None, cca_dir=None
|
|
129
|
+
):
|
|
119
130
|
paths = build_paths(config)
|
|
120
131
|
params = config.downstream["communication"]
|
|
121
132
|
return run_between_cn_statistics(
|
|
122
|
-
|
|
133
|
+
cca_dir or paths.communication_cca,
|
|
123
134
|
params["minimum_samples"],
|
|
124
135
|
params["top_n_cca_pairs"],
|
|
136
|
+
input_file=input_file or paths.communication_scores,
|
|
137
|
+
output_dir=output_dir or paths.communication_between,
|
|
125
138
|
)
|
|
126
139
|
|
|
127
140
|
|
{cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/cca.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from itertools import combinations
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import warnings
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -12,8 +13,6 @@ from ...paths import build_paths
|
|
|
12
13
|
from .utils import (
|
|
13
14
|
build_score_tables,
|
|
14
15
|
condition_sort_key,
|
|
15
|
-
normalize_scores_long,
|
|
16
|
-
prune_csv_outputs,
|
|
17
16
|
read_scores_long,
|
|
18
17
|
)
|
|
19
18
|
|
|
@@ -22,8 +21,8 @@ def _result_columns(n_components, *, by_condition=False):
|
|
|
22
21
|
columns = ["CN_A", "CN_B", "n_samples"]
|
|
23
22
|
if by_condition:
|
|
24
23
|
columns.append("Condition")
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
columns.extend(["rho1", "pval1", "invalid_perm1"])
|
|
25
|
+
columns.extend(f"rho{component}" for component in range(2, n_components + 1))
|
|
27
26
|
return columns
|
|
28
27
|
|
|
29
28
|
|
|
@@ -42,8 +41,8 @@ def clean_cca_data(x, y, minimum_common_samples, min_std):
|
|
|
42
41
|
common = x.index.intersection(y.index)
|
|
43
42
|
if len(common) < minimum_common_samples:
|
|
44
43
|
return None
|
|
45
|
-
x = x.loc[common]
|
|
46
|
-
y = y.loc[common]
|
|
44
|
+
x = x.loc[common].replace([np.inf, -np.inf], np.nan)
|
|
45
|
+
y = y.loc[common].replace([np.inf, -np.inf], np.nan)
|
|
47
46
|
x = x.loc[:, x.std(axis=0) > min_std]
|
|
48
47
|
y = y.loc[:, y.std(axis=0) > min_std]
|
|
49
48
|
if x.shape[1] == 0 or y.shape[1] == 0:
|
|
@@ -52,6 +51,11 @@ def clean_cca_data(x, y, minimum_common_samples, min_std):
|
|
|
52
51
|
x, y = x.loc[complete], y.loc[complete]
|
|
53
52
|
if x.shape[0] < minimum_common_samples:
|
|
54
53
|
return None
|
|
54
|
+
# Removing incomplete samples can make a previously variable column constant.
|
|
55
|
+
x = x.loc[:, x.std(axis=0) > min_std]
|
|
56
|
+
y = y.loc[:, y.std(axis=0) > min_std]
|
|
57
|
+
if x.shape[1] == 0 or y.shape[1] == 0:
|
|
58
|
+
return None
|
|
55
59
|
return {
|
|
56
60
|
"X": x.values,
|
|
57
61
|
"Y": y.values,
|
|
@@ -66,20 +70,41 @@ def build_score_list(frame, cn_list):
|
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def select_top_features(matrix, top_n=5):
|
|
69
|
-
"""Select cell types by
|
|
70
|
-
if matrix is None or matrix.shape[1] == 0:
|
|
73
|
+
"""Select the five highest-mean cell types by default, before filtering."""
|
|
74
|
+
if top_n is None or matrix is None or matrix.shape[1] == 0:
|
|
71
75
|
return matrix
|
|
76
|
+
if isinstance(top_n, bool) or not isinstance(top_n, (int, np.integer)) or top_n < 1:
|
|
77
|
+
raise ValueError("cca_top_celltypes must be a positive integer or null")
|
|
72
78
|
means = matrix.mean(axis=0).fillna(-np.inf)
|
|
73
79
|
selected = means.sort_values(ascending=False, kind="stable").head(top_n).index
|
|
74
80
|
return matrix.loc[:, selected]
|
|
75
81
|
|
|
76
82
|
|
|
83
|
+
def _fit_cca_scores(x_matrix, y_matrix, n_components, max_iterations):
|
|
84
|
+
"""Fit finite canonical scores, retrying a degenerate second component."""
|
|
85
|
+
for count in range(n_components, 0, -1):
|
|
86
|
+
try:
|
|
87
|
+
model = CCA(n_components=count, scale=True, max_iter=max_iterations)
|
|
88
|
+
with np.errstate(invalid="raise", divide="raise", over="raise"):
|
|
89
|
+
model.fit(x_matrix, y_matrix)
|
|
90
|
+
x_scores, y_scores = model.transform(x_matrix, y_matrix)
|
|
91
|
+
if not (np.isfinite(x_scores).all() and np.isfinite(y_scores).all()):
|
|
92
|
+
continue
|
|
93
|
+
if any(np.ptp(scores[:, index]) == 0 for scores in (x_scores, y_scores) for index in range(count)):
|
|
94
|
+
continue
|
|
95
|
+
return x_scores, y_scores
|
|
96
|
+
except (ValueError, np.linalg.LinAlgError, FloatingPointError):
|
|
97
|
+
continue
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
77
101
|
def run_cca_for_scores(
|
|
78
102
|
score_list, cn_list, communication_params, random_seed, condition=None
|
|
79
103
|
):
|
|
80
|
-
"""
|
|
104
|
+
"""Fit up to two pooled components, or just one within a condition."""
|
|
105
|
+
n_components = 1 if condition is not None else communication_params["cca_n_components"]
|
|
81
106
|
score_list = {
|
|
82
|
-
cn: select_top_features(matrix, communication_params
|
|
107
|
+
cn: select_top_features(matrix, communication_params.get("cca_top_celltypes", 5))
|
|
83
108
|
for cn, matrix in score_list.items()
|
|
84
109
|
}
|
|
85
110
|
result_rows = []
|
|
@@ -97,78 +122,69 @@ def run_cca_for_scores(
|
|
|
97
122
|
continue
|
|
98
123
|
x_matrix, y_matrix = cleaned["X"], cleaned["Y"]
|
|
99
124
|
n_samples = x_matrix.shape[0]
|
|
100
|
-
|
|
125
|
+
# Column count alone does not detect proportional cell-type profiles.
|
|
126
|
+
max_components = min(
|
|
127
|
+
n_components,
|
|
128
|
+
np.linalg.matrix_rank(x_matrix - x_matrix.mean(axis=0)),
|
|
129
|
+
np.linalg.matrix_rank(y_matrix - y_matrix.mean(axis=0)),
|
|
130
|
+
n_samples - 2,
|
|
131
|
+
)
|
|
101
132
|
if max_components < 1:
|
|
102
133
|
continue
|
|
103
134
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
max_iter=communication_params["cca_max_iterations"],
|
|
108
|
-
)
|
|
109
|
-
model.fit(x_matrix, y_matrix)
|
|
110
|
-
x_scores, y_scores = model.transform(x_matrix, y_matrix)
|
|
111
|
-
actual_components = min(
|
|
112
|
-
communication_params["cca_n_components"], x_scores.shape[1]
|
|
135
|
+
scores = _fit_cca_scores(
|
|
136
|
+
x_matrix, y_matrix, max_components,
|
|
137
|
+
communication_params["cca_max_iterations"],
|
|
113
138
|
)
|
|
139
|
+
if scores is None:
|
|
140
|
+
warnings.warn(
|
|
141
|
+
f"CCA could not estimate finite scores for CN{cn_a} vs CN{cn_b} "
|
|
142
|
+
f"(condition={condition}); pair omitted.", RuntimeWarning,
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
x_scores, y_scores = scores
|
|
146
|
+
actual_components = min(n_components, x_scores.shape[1])
|
|
114
147
|
correlations = [
|
|
115
148
|
np.corrcoef(x_scores[:, index], y_scores[:, index])[0, 1]
|
|
116
149
|
for index in range(actual_components)
|
|
117
150
|
]
|
|
151
|
+
if not np.isfinite(correlations[0]):
|
|
152
|
+
continue
|
|
118
153
|
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
# Only the first canonical correlation is used for inference. rho2 and
|
|
155
|
+
# loading2 are descriptive coordinates for the pooled loading diagram.
|
|
156
|
+
observed = correlations[0]
|
|
157
|
+
extreme = 0
|
|
158
|
+
valid_count = 0
|
|
159
|
+
invalid_count = 0
|
|
121
160
|
rng = pair_rng(random_seed, condition, cn_a, cn_b)
|
|
122
|
-
for
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
shuffled_y = rng.permutation(y_matrix)
|
|
128
|
-
permutation_model = CCA(
|
|
129
|
-
n_components=min(
|
|
130
|
-
communication_params["cca_n_components"],
|
|
131
|
-
max_components,
|
|
132
|
-
),
|
|
133
|
-
scale=True,
|
|
134
|
-
max_iter=communication_params["cca_max_iterations"],
|
|
135
|
-
)
|
|
136
|
-
try:
|
|
137
|
-
permutation_model.fit(x_matrix, shuffled_y)
|
|
138
|
-
permuted_x, permuted_y = permutation_model.transform(
|
|
139
|
-
x_matrix, shuffled_y
|
|
140
|
-
)
|
|
141
|
-
except (ValueError, np.linalg.LinAlgError, FloatingPointError):
|
|
142
|
-
invalid_count += 1
|
|
143
|
-
continue
|
|
144
|
-
if permuted_x.shape[1] <= component:
|
|
145
|
-
continue
|
|
146
|
-
permuted_rho = abs(
|
|
147
|
-
np.corrcoef(
|
|
148
|
-
permuted_x[:, component], permuted_y[:, component]
|
|
149
|
-
)[0, 1]
|
|
150
|
-
)
|
|
151
|
-
valid_count += 1
|
|
152
|
-
if permuted_rho >= abs(observed) - 1e-12:
|
|
153
|
-
extreme += 1
|
|
154
|
-
p_values.append(
|
|
155
|
-
(extreme + 1) / (valid_count + 1) if valid_count else np.nan
|
|
161
|
+
for _ in range(communication_params["cca_n_permutations"]):
|
|
162
|
+
shuffled_y = rng.permutation(y_matrix)
|
|
163
|
+
permuted = _fit_cca_scores(
|
|
164
|
+
x_matrix, shuffled_y, x_scores.shape[1],
|
|
165
|
+
communication_params["cca_max_iterations"],
|
|
156
166
|
)
|
|
157
|
-
|
|
167
|
+
if permuted is None:
|
|
168
|
+
invalid_count += 1
|
|
169
|
+
continue
|
|
170
|
+
permuted_x, permuted_y = permuted
|
|
171
|
+
permuted_rho = abs(np.corrcoef(permuted_x[:, 0], permuted_y[:, 0])[0, 1])
|
|
172
|
+
if not np.isfinite(permuted_rho):
|
|
173
|
+
invalid_count += 1
|
|
174
|
+
continue
|
|
175
|
+
valid_count += 1
|
|
176
|
+
if permuted_rho >= abs(observed) - 1e-12:
|
|
177
|
+
extreme += 1
|
|
158
178
|
|
|
159
179
|
row = {"CN_A": cn_a, "CN_B": cn_b, "n_samples": n_samples}
|
|
160
180
|
if condition is not None:
|
|
161
181
|
row["Condition"] = str(condition)
|
|
162
|
-
|
|
182
|
+
row["pval1"] = (extreme + 1) / (valid_count + 1) if valid_count else np.nan
|
|
183
|
+
row["invalid_perm1"] = invalid_count
|
|
184
|
+
for component in range(n_components):
|
|
163
185
|
row[f"rho{component + 1}"] = (
|
|
164
186
|
correlations[component] if component < len(correlations) else None
|
|
165
187
|
)
|
|
166
|
-
row[f"pval{component + 1}"] = (
|
|
167
|
-
p_values[component] if component < len(p_values) else None
|
|
168
|
-
)
|
|
169
|
-
row[f"invalid_perm{component + 1}"] = (
|
|
170
|
-
invalid_counts[component] if component < len(invalid_counts) else None
|
|
171
|
-
)
|
|
172
188
|
result_rows.append(row)
|
|
173
189
|
key = "__".join(
|
|
174
190
|
filter(
|
|
@@ -187,9 +203,7 @@ def run_cca_for_scores(
|
|
|
187
203
|
|
|
188
204
|
results = pd.DataFrame(
|
|
189
205
|
result_rows,
|
|
190
|
-
columns=_result_columns(
|
|
191
|
-
communication_params["cca_n_components"], by_condition=condition is not None
|
|
192
|
-
),
|
|
206
|
+
columns=_result_columns(n_components, by_condition=condition is not None),
|
|
193
207
|
)
|
|
194
208
|
if not results.empty:
|
|
195
209
|
results = results.sort_values(
|
|
@@ -263,19 +277,15 @@ def write_cca_loadings(
|
|
|
263
277
|
return tables
|
|
264
278
|
|
|
265
279
|
|
|
266
|
-
def _normalize_scores(frame):
|
|
267
|
-
return normalize_scores_long(frame, require_condition=True)
|
|
268
|
-
|
|
269
|
-
|
|
270
280
|
def run_cca_analysis(
|
|
271
281
|
config: CytoCommunityConfig, *, input_file=None, output_dir=None
|
|
272
282
|
):
|
|
273
283
|
"""Run pooled and condition-specific CCA using configured paths."""
|
|
274
284
|
paths = build_paths(config)
|
|
275
285
|
params = config.downstream["communication"]
|
|
276
|
-
output_dir = Path(output_dir or paths.
|
|
286
|
+
output_dir = Path(output_dir or paths.communication_cca)
|
|
277
287
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
278
|
-
frame = read_scores_long(input_file or
|
|
288
|
+
frame = read_scores_long(input_file or paths.communication_scores)
|
|
279
289
|
celltype_order = frame["CellType"].astype(str).drop_duplicates().tolist()
|
|
280
290
|
conditions = sorted(frame["Condition"].unique(), key=condition_sort_key)
|
|
281
291
|
cn_list = sorted(frame["CN"].unique(), key=condition_sort_key)
|
|
@@ -283,7 +293,6 @@ def run_cca_analysis(
|
|
|
283
293
|
build_score_list(frame, cn_list), cn_list, params, config.random_seed
|
|
284
294
|
)
|
|
285
295
|
condition_results = []
|
|
286
|
-
fitted_conditions = []
|
|
287
296
|
for condition in conditions:
|
|
288
297
|
subset = frame[frame["Condition"] == condition]
|
|
289
298
|
result = run_cca_for_scores(
|
|
@@ -293,14 +302,12 @@ def run_cca_analysis(
|
|
|
293
302
|
config.random_seed,
|
|
294
303
|
condition,
|
|
295
304
|
)
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
if not result["results"].empty:
|
|
306
|
+
condition_results.append(result["results"])
|
|
298
307
|
by_condition = (
|
|
299
308
|
pd.concat(condition_results, ignore_index=True)
|
|
300
309
|
if condition_results
|
|
301
|
-
else pd.DataFrame(
|
|
302
|
-
columns=_result_columns(params["cca_n_components"], by_condition=True)
|
|
303
|
-
)
|
|
310
|
+
else pd.DataFrame(columns=_result_columns(1, by_condition=True))
|
|
304
311
|
)
|
|
305
312
|
if not by_condition.empty:
|
|
306
313
|
by_condition["Condition"] = pd.Categorical(
|
|
@@ -311,24 +318,14 @@ def run_cca_analysis(
|
|
|
311
318
|
key=lambda values: abs(values) if values.name == "rho1" else values,
|
|
312
319
|
ascending=[True, False],
|
|
313
320
|
)
|
|
314
|
-
#
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
params["cca_n_components"], condition,
|
|
321
|
-
)
|
|
322
|
-
prefix = f"{condition}_" if condition is not None else ""
|
|
323
|
-
current_loadings.extend(
|
|
324
|
-
f"CCA_loadings_{prefix}CN{cn_a}_vs_CN{cn_b}.csv"
|
|
325
|
-
for cn_a, cn_b in tables
|
|
326
|
-
)
|
|
321
|
+
# The circle plot consumes only the condition summary. Loading tables are
|
|
322
|
+
# needed for pooled CCA plots and selection of between-CN cell-type pairs.
|
|
323
|
+
write_cca_loadings(
|
|
324
|
+
pooled["results"], pooled["cache"], output_dir, celltype_order,
|
|
325
|
+
params["cca_n_components"],
|
|
326
|
+
)
|
|
327
327
|
pooled["results"].to_csv(output_dir / "CCA_results.csv", index=False)
|
|
328
328
|
by_condition.to_csv(output_dir / "CCA_results_by_condition.csv", index=False)
|
|
329
|
-
prune_csv_outputs(
|
|
330
|
-
output_dir, "CCA_loadings_*CN*_vs_CN*.csv", current_loadings
|
|
331
|
-
)
|
|
332
329
|
return {"pooled": pooled["results"], "by_condition": by_condition}
|
|
333
330
|
|
|
334
331
|
|
{cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/utils.py
RENAMED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Shared communication table and statistical utilities."""
|
|
2
2
|
|
|
3
3
|
import itertools
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
4
|
import numpy as np
|
|
7
5
|
import pandas as pd
|
|
8
6
|
from scipy.stats import spearmanr
|
|
@@ -72,14 +70,6 @@ def build_score_tables(
|
|
|
72
70
|
return tables
|
|
73
71
|
|
|
74
72
|
|
|
75
|
-
def prune_csv_outputs(output_dir, pattern, current_names):
|
|
76
|
-
"""Remove only obsolete files from one analysis-owned CSV filename pattern."""
|
|
77
|
-
current_names = set(current_names)
|
|
78
|
-
for path in Path(output_dir).glob(pattern):
|
|
79
|
-
if path.is_file() and path.name not in current_names:
|
|
80
|
-
path.unlink()
|
|
81
|
-
|
|
82
|
-
|
|
83
73
|
def condition_label(condition):
|
|
84
74
|
condition = str(condition)
|
|
85
75
|
return condition if condition == "rest" else f"label{condition}"
|
|
@@ -114,7 +104,7 @@ def stable_seed(base_seed, *parts):
|
|
|
114
104
|
def safe_spearman(x, y, min_samples):
|
|
115
105
|
x = np.asarray(x, dtype=float)
|
|
116
106
|
y = np.asarray(y, dtype=float)
|
|
117
|
-
valid =
|
|
107
|
+
valid = np.isfinite(x) & np.isfinite(y)
|
|
118
108
|
x = x[valid]
|
|
119
109
|
y = y[valid]
|
|
120
110
|
if len(x) < min_samples or len(np.unique(x)) < 2 or len(np.unique(y)) < 2:
|
|
@@ -133,7 +123,6 @@ __all__ = [
|
|
|
133
123
|
"condition_pairs",
|
|
134
124
|
"condition_sort_key",
|
|
135
125
|
"normalize_scores_long",
|
|
136
|
-
"prune_csv_outputs",
|
|
137
126
|
"read_scores_long",
|
|
138
127
|
"safe_spearman",
|
|
139
128
|
"stable_seed",
|
{cytocommunity2-1.0.7 → cytocommunity2-1.0.8}/src/cytocommunity2/downstream/communication/within.py
RENAMED
|
@@ -12,7 +12,6 @@ from .utils import (
|
|
|
12
12
|
available_conditions,
|
|
13
13
|
build_score_tables,
|
|
14
14
|
condition_pair_label,
|
|
15
|
-
prune_csv_outputs,
|
|
16
15
|
read_scores_long,
|
|
17
16
|
safe_spearman,
|
|
18
17
|
stable_seed,
|
|
@@ -88,7 +87,7 @@ def permutation_pvalue(
|
|
|
88
87
|
x = np.asarray(x, dtype=float)
|
|
89
88
|
y = np.asarray(y, dtype=float)
|
|
90
89
|
labels = np.asarray(labels)
|
|
91
|
-
valid =
|
|
90
|
+
valid = np.isfinite(x) & np.isfinite(y) & np.isin(
|
|
92
91
|
labels, [condition_a, condition_b]
|
|
93
92
|
)
|
|
94
93
|
x, y, labels = x[valid], y[valid], labels[valid]
|
|
@@ -222,9 +221,9 @@ def run_within_cn_analysis(
|
|
|
222
221
|
"""Run and save pooled plus condition-specific within-CN analyses."""
|
|
223
222
|
paths = build_paths(config)
|
|
224
223
|
params = config.downstream["communication"]
|
|
225
|
-
output_dir = Path(output_dir or paths.
|
|
224
|
+
output_dir = Path(output_dir or paths.communication_within)
|
|
226
225
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
227
|
-
input_file = Path(input_file or
|
|
226
|
+
input_file = Path(input_file or paths.communication_scores)
|
|
228
227
|
scores = read_scores_long(input_file)
|
|
229
228
|
celltype_order = scores["CellType"].astype(str).drop_duplicates().tolist()
|
|
230
229
|
num_cn = int(paths.selected_cn_num.read_text(encoding="utf-8"))
|
|
@@ -251,17 +250,12 @@ def run_within_cn_analysis(
|
|
|
251
250
|
)
|
|
252
251
|
label = condition_pair_label(comparison["cond_a"], comparison["cond_b"])
|
|
253
252
|
outputs[label] = result
|
|
254
|
-
# Publish only after all comparisons compute; retired comparison modes and
|
|
255
|
-
# conditions must not leave tables that a later run can mistake for current.
|
|
256
253
|
outputs["pooled"].to_csv(output_dir / "SpearmanWithinCNs.csv", index=False)
|
|
257
|
-
current_names = []
|
|
258
254
|
for label, result in outputs.items():
|
|
259
|
-
if label == "pooled":
|
|
255
|
+
if label == "pooled" or result.empty:
|
|
260
256
|
continue
|
|
261
257
|
name = f"CommunicationPair_{label}.csv"
|
|
262
258
|
result.to_csv(output_dir / name, index=False)
|
|
263
|
-
current_names.append(name)
|
|
264
|
-
prune_csv_outputs(output_dir, "CommunicationPair_label*_vs_*.csv", current_names)
|
|
265
259
|
return outputs
|
|
266
260
|
|
|
267
261
|
|
|
@@ -92,7 +92,8 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum
|
|
|
92
92
|
|
|
93
93
|
The result maps comparison names to maps of CN names and result tables.
|
|
94
94
|
Comparisons with fewer than ``minimum_samples`` samples on either side
|
|
95
|
-
have empty maps.
|
|
95
|
+
have empty maps. Pairwise tests are computed once per unordered pair;
|
|
96
|
+
the reverse comparison swaps alternatives, sample counts, and rank sums.
|
|
96
97
|
"""
|
|
97
98
|
normalized_records = [
|
|
98
99
|
{**record, "matrix": np.asarray(record["matrix"], dtype=float)}
|
|
@@ -116,8 +117,12 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum
|
|
|
116
117
|
) >= minimum_samples
|
|
117
118
|
]
|
|
118
119
|
for cn_index in range(expected_shape[1]):
|
|
120
|
+
cn_name = f"CN{cn_index + 1}"
|
|
119
121
|
for comparison in eligible:
|
|
122
|
+
if mode == "pairwise" and cn_name in outputs[comparison["name"]]:
|
|
123
|
+
continue
|
|
120
124
|
rows = []
|
|
125
|
+
reverse_rows = []
|
|
121
126
|
for cell_index, cell_type in enumerate(celltypes):
|
|
122
127
|
target = [
|
|
123
128
|
record["matrix"][cell_index, cn_index]
|
|
@@ -134,8 +139,22 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum
|
|
|
134
139
|
)
|
|
135
140
|
if result is not None:
|
|
136
141
|
rows.extend(result)
|
|
142
|
+
if mode == "pairwise":
|
|
143
|
+
total_n = len(target) + len(reference)
|
|
144
|
+
# Reversing the two rows keeps greater before less.
|
|
145
|
+
for row in reversed(result):
|
|
146
|
+
reverse_rows.append({
|
|
147
|
+
**row,
|
|
148
|
+
"Alternative": "greater" if row["Alternative"] == "less" else "less",
|
|
149
|
+
"W_statistic": total_n * (total_n + 1) / 2 - row["W_statistic"],
|
|
150
|
+
"N_target": row["N_reference"],
|
|
151
|
+
"N_reference": row["N_target"],
|
|
152
|
+
})
|
|
137
153
|
if rows:
|
|
138
|
-
outputs[comparison["name"]][
|
|
154
|
+
outputs[comparison["name"]][cn_name] = pd.DataFrame(rows)
|
|
155
|
+
if mode == "pairwise":
|
|
156
|
+
reverse_name = f"{comparison['reference_name']}_gt_{comparison['target_name']}"
|
|
157
|
+
outputs[reverse_name][cn_name] = pd.DataFrame(reverse_rows)
|
|
139
158
|
return outputs
|
|
140
159
|
|
|
141
160
|
|
|
@@ -154,9 +173,6 @@ def run_differential_composition_analysis(
|
|
|
154
173
|
output_dir = Path(output_dir or paths.downstream_data / "DifferentialComposed")
|
|
155
174
|
for comparison, cn_tables in outputs.items():
|
|
156
175
|
comparison_dir = output_dir / comparison
|
|
157
|
-
if comparison_dir.exists():
|
|
158
|
-
for csv_file in comparison_dir.glob("CN*.csv"):
|
|
159
|
-
csv_file.unlink()
|
|
160
176
|
if cn_tables:
|
|
161
177
|
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
162
178
|
for cn, table in cn_tables.items():
|
|
@@ -19,13 +19,31 @@ def load_enrichment_by_sample(enrichment_dir, input_dir):
|
|
|
19
19
|
.str.strip()
|
|
20
20
|
.tolist()
|
|
21
21
|
)
|
|
22
|
+
if not celltypes or any(not celltype for celltype in celltypes):
|
|
23
|
+
raise ValueError(f"{enrichment_dir}: cell-type names must be nonempty")
|
|
24
|
+
if len(set(celltypes)) != len(celltypes):
|
|
25
|
+
raise ValueError(f"{enrichment_dir}: cell-type names must be unique")
|
|
22
26
|
samples = read_nonempty_lines(Path(input_dir) / "ImageNameList.txt")
|
|
27
|
+
if not samples or len(set(samples)) != len(samples):
|
|
28
|
+
raise ValueError(f"{input_dir}: sample names must be nonempty and unique")
|
|
23
29
|
matrices = {
|
|
24
30
|
sample: pd.read_csv(
|
|
25
31
|
enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
|
|
26
32
|
).to_numpy(dtype=float)
|
|
27
33
|
for sample in samples
|
|
28
34
|
}
|
|
35
|
+
expected_shape = next(iter(matrices.values())).shape
|
|
36
|
+
if expected_shape[0] != len(celltypes) or expected_shape[1] == 0:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"{enrichment_dir}: enrichment rows must match cell types and include at least one CN"
|
|
39
|
+
)
|
|
40
|
+
for sample, matrix in matrices.items():
|
|
41
|
+
if matrix.shape != expected_shape:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"{sample}: enrichment shape={matrix.shape}, expected {expected_shape}"
|
|
44
|
+
)
|
|
45
|
+
if not np.isfinite(matrix).all():
|
|
46
|
+
raise ValueError(f"{sample}: enrichment scores must be finite")
|
|
29
47
|
return celltypes, matrices
|
|
30
48
|
|
|
31
49
|
|