cytocommunity2 0.1.4__tar.gz → 0.1.6__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-0.1.4/src/cytocommunity2.egg-info → cytocommunity2-0.1.6}/PKG-INFO +1 -1
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/pyproject.toml +1 -1
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/__init__.py +1 -1
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/config.py +1 -6
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/__init__.py +9 -11
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/cca.py +6 -5
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/within.py +3 -2
- cytocommunity2-0.1.4/src/cytocommunity2/downstream/composition.py → cytocommunity2-0.1.6/src/cytocommunity2/downstream/differential_composition.py +36 -173
- cytocommunity2-0.1.6/src/cytocommunity2/downstream/dominant_celltype.py +95 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/communication.py +1 -1
- cytocommunity2-0.1.6/src/cytocommunity2/downstream/plotting/dominant.py +78 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/dotplots.py +262 -256
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/visualization/spatial.py +37 -29
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6/src/cytocommunity2.egg-info}/PKG-INFO +1 -1
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/SOURCES.txt +2 -1
- cytocommunity2-0.1.4/src/cytocommunity2/downstream/plotting/dominant.py +0 -142
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/LICENSE +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/README.md +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/setup.cfg +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/colors.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/coherence.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/between.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/moran.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/coherence.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/composition.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/recurrence.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/runtime.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/ensemble.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/api.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/dataset.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/model.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/selection.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/training.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/paths.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/top_level.txt +0 -0
|
@@ -75,12 +75,7 @@ class CytoCommunityConfig:
|
|
|
75
75
|
visualization: VisualizationConfig
|
|
76
76
|
downstream: Mapping[str, Any]
|
|
77
77
|
|
|
78
|
-
@
|
|
79
|
-
def celltype_order(self):
|
|
80
|
-
groups = self.downstream["dominant_cell_type"]["cell_type_groups"]
|
|
81
|
-
return [cell_type for group in groups.values() for cell_type in group]
|
|
82
|
-
|
|
83
|
-
@classmethod
|
|
78
|
+
@classmethod
|
|
84
79
|
def from_json(cls, config_file=None):
|
|
85
80
|
"""Load and validate a JSON configuration file."""
|
|
86
81
|
return _load_config(config_file, config_class=cls)
|
|
@@ -5,14 +5,12 @@ functions live in a dedicated namespace because downstream analyses are
|
|
|
5
5
|
optional and evolve independently from model training.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .coherence import run_coherence_analysis
|
|
9
|
-
from .
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"run_dominant_celltype_analysis",
|
|
18
|
-
]
|
|
8
|
+
from .coherence import run_coherence_analysis
|
|
9
|
+
from .differential_composition import run_differential_composition_analysis
|
|
10
|
+
from .dominant_celltype import run_dominant_celltype_analysis
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"run_coherence_analysis",
|
|
14
|
+
"run_differential_composition_analysis",
|
|
15
|
+
"run_dominant_celltype_analysis",
|
|
16
|
+
]
|
{cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/cca.py
RENAMED
|
@@ -260,9 +260,10 @@ def run_cca_analysis(config, *, input_file=None, output_dir=None):
|
|
|
260
260
|
params = config.downstream["communication"]
|
|
261
261
|
output_dir = Path(output_dir or paths.communication_config)
|
|
262
262
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
263
|
-
frame = _normalize_scores(
|
|
264
|
-
pd.read_csv(input_file or output_dir / "EnrichScoreMatrix_long.csv")
|
|
265
|
-
)
|
|
263
|
+
frame = _normalize_scores(
|
|
264
|
+
pd.read_csv(input_file or output_dir / "EnrichScoreMatrix_long.csv")
|
|
265
|
+
)
|
|
266
|
+
celltype_order = frame["CellType"].astype(str).drop_duplicates().tolist()
|
|
266
267
|
conditions = sorted(frame["Condition"].unique(), key=condition_sort_key)
|
|
267
268
|
cn_list = sorted(frame["CN"].unique(), key=condition_sort_key)
|
|
268
269
|
pooled = run_cca_for_scores(
|
|
@@ -273,7 +274,7 @@ def run_cca_analysis(config, *, input_file=None, output_dir=None):
|
|
|
273
274
|
pooled["results"],
|
|
274
275
|
pooled["cache"],
|
|
275
276
|
output_dir,
|
|
276
|
-
|
|
277
|
+
celltype_order,
|
|
277
278
|
params["cca_canonical_components"],
|
|
278
279
|
)
|
|
279
280
|
condition_results = []
|
|
@@ -291,7 +292,7 @@ def run_cca_analysis(config, *, input_file=None, output_dir=None):
|
|
|
291
292
|
result["results"],
|
|
292
293
|
result["cache"],
|
|
293
294
|
output_dir,
|
|
294
|
-
|
|
295
|
+
celltype_order,
|
|
295
296
|
params["cca_canonical_components"],
|
|
296
297
|
condition,
|
|
297
298
|
)
|
{cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/within.py
RENAMED
|
@@ -224,7 +224,8 @@ def run_within_cn_analysis(config, *, input_file=None, output_dir=None):
|
|
|
224
224
|
output_dir = Path(output_dir or paths.communication_config)
|
|
225
225
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
226
226
|
input_file = Path(input_file or output_dir / "EnrichScoreMatrix_long.csv")
|
|
227
|
-
scores = pd.read_csv(input_file, dtype={"Condition": str, "CN": str})
|
|
227
|
+
scores = pd.read_csv(input_file, dtype={"Condition": str, "CN": str})
|
|
228
|
+
celltype_order = scores["CellType"].astype(str).drop_duplicates().tolist()
|
|
228
229
|
num_cn = int(paths.selected_cn_num.read_text(encoding="utf-8"))
|
|
229
230
|
cn_order = [str(index) for index in range(1, num_cn + 1)]
|
|
230
231
|
wide = prepare_wide_tables(scores, cn_order)
|
|
@@ -242,7 +243,7 @@ def run_within_cn_analysis(config, *, input_file=None, output_dir=None):
|
|
|
242
243
|
labels_a=comparison["labels_a"],
|
|
243
244
|
labels_b=comparison["labels_b"],
|
|
244
245
|
cn_order=cn_order,
|
|
245
|
-
celltype_order=
|
|
246
|
+
celltype_order=celltype_order,
|
|
246
247
|
min_samples=params["minimum_samples"],
|
|
247
248
|
n_permutations=params["n_permutations"],
|
|
248
249
|
max_attempt_multiplier=params["permutation_max_attempt_multiplier"],
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Condition-specific differential cell-type composition analysis."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from functools import lru_cache
|
|
5
|
-
from itertools import combinations
|
|
6
3
|
from pathlib import Path
|
|
7
4
|
|
|
8
5
|
import numpy as np
|
|
@@ -11,135 +8,13 @@ from scipy import stats
|
|
|
11
8
|
|
|
12
9
|
from ..config import CytoCommunityConfig
|
|
13
10
|
from ..paths import build_paths
|
|
11
|
+
from .dominant_celltype import load_enrichment_matrices
|
|
14
12
|
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
"ImmuneScore": "Immune cell-dominant",
|
|
18
|
-
"MalignantScore": "Malignant cell-dominant",
|
|
19
|
-
"StromalScore": "Stromal cell-dominant",
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@dataclass(frozen=True)
|
|
24
|
-
class DominantCellTypeResult:
|
|
25
|
-
"""In-memory outputs of dominant-cell-type analysis."""
|
|
26
|
-
|
|
27
|
-
frequency: pd.DataFrame
|
|
28
|
-
dominant_cell_types: pd.DataFrame
|
|
29
|
-
subtype_scores: pd.DataFrame
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def load_enrichment_matrices(enrichment_dir, input_dir, celltype_order):
|
|
33
|
-
"""Load and align per-sample enrichment matrices."""
|
|
34
|
-
enrichment_dir = Path(enrichment_dir)
|
|
35
|
-
input_dir = Path(input_dir)
|
|
36
|
-
source_celltypes = (
|
|
37
|
-
pd.read_csv(enrichment_dir / "CellTypeVec_List.csv")
|
|
38
|
-
.iloc[:, 0]
|
|
39
|
-
.astype(str)
|
|
40
|
-
.str.strip()
|
|
41
|
-
.tolist()
|
|
42
|
-
)
|
|
43
|
-
missing = [cell_type for cell_type in celltype_order if cell_type not in source_celltypes]
|
|
44
|
-
if missing:
|
|
45
|
-
raise ValueError(f"Cell types missing from enrichment matrices: {missing}")
|
|
46
|
-
samples = pd.read_csv(input_dir / "ImageNameList.txt", header=None).iloc[:, 0]
|
|
47
|
-
indices = [source_celltypes.index(cell_type) for cell_type in celltype_order]
|
|
48
|
-
matrices = [
|
|
49
|
-
pd.read_csv(
|
|
50
|
-
enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
|
|
51
|
-
).to_numpy(dtype=float)[indices]
|
|
52
|
-
for sample in samples
|
|
53
|
-
]
|
|
54
|
-
if not matrices:
|
|
55
|
-
raise ValueError("ImageNameList.txt does not contain any samples")
|
|
56
|
-
return list(celltype_order), np.stack(matrices, axis=0)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def build_subtype_scores(frequency, dominant_df, cell_type_groups):
|
|
60
|
-
"""Aggregate cell-type frequencies into configured biological groups."""
|
|
61
|
-
cn_by_celltype = frequency.T
|
|
62
|
-
available_groups = {
|
|
63
|
-
score_name: [cell_type for cell_type in group if cell_type in cn_by_celltype]
|
|
64
|
-
for score_name, group in cell_type_groups.items()
|
|
65
|
-
}
|
|
66
|
-
missing_groups = [name for name, group in available_groups.items() if not group]
|
|
67
|
-
if missing_groups:
|
|
68
|
-
raise ValueError(f"No input cell types found for: {missing_groups}")
|
|
69
|
-
|
|
70
|
-
scores = pd.DataFrame(
|
|
71
|
-
{
|
|
72
|
-
score_name: cn_by_celltype[cell_types].mean(axis=1)
|
|
73
|
-
for score_name, cell_types in available_groups.items()
|
|
74
|
-
}
|
|
75
|
-
)
|
|
76
|
-
names = {**{name: name for name in available_groups}, **SUBTYPE_NAMES}
|
|
77
|
-
scores["Subtype"] = scores.idxmax(axis=1).map(names)
|
|
78
|
-
scores = scores.reset_index().rename(columns={"index": "CN"})
|
|
79
|
-
return scores.merge(dominant_df, on="CN", how="left")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def analyze_dominant_celltypes(
|
|
83
|
-
enrichment_matrices,
|
|
84
|
-
celltypes,
|
|
85
|
-
*,
|
|
86
|
-
score_threshold,
|
|
87
|
-
frequency_threshold,
|
|
88
|
-
cell_type_groups,
|
|
89
|
-
):
|
|
90
|
-
"""Compute dominant cell types and subtype scores from aligned matrices."""
|
|
91
|
-
matrices = np.asarray(enrichment_matrices, dtype=float)
|
|
92
|
-
if matrices.ndim != 3:
|
|
93
|
-
raise ValueError("enrichment_matrices must have shape (samples, cell types, CNs)")
|
|
94
|
-
if matrices.shape[1] != len(celltypes):
|
|
95
|
-
raise ValueError("celltypes length does not match enrichment_matrices")
|
|
96
|
-
|
|
97
|
-
frequencies = (matrices > score_threshold).mean(axis=0)
|
|
98
|
-
cn_names = [f"CN{index}" for index in range(1, matrices.shape[2] + 1)]
|
|
99
|
-
frequency = pd.DataFrame(frequencies, index=celltypes, columns=cn_names)
|
|
100
|
-
dominant = pd.DataFrame(
|
|
101
|
-
{
|
|
102
|
-
"CN": cn_names,
|
|
103
|
-
"DominantCellTypes": [
|
|
104
|
-
", ".join(
|
|
105
|
-
np.asarray(celltypes)[frequencies[:, cn_index] > frequency_threshold]
|
|
106
|
-
)
|
|
107
|
-
for cn_index in range(matrices.shape[2])
|
|
108
|
-
],
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
subtype_scores = build_subtype_scores(frequency, dominant, cell_type_groups)
|
|
112
|
-
return DominantCellTypeResult(frequency, dominant, subtype_scores)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def run_dominant_celltype_analysis(config, *, output_dir=None):
|
|
116
|
-
"""Run dominant-cell-type analysis using the established repository layout."""
|
|
117
|
-
if not isinstance(config, CytoCommunityConfig):
|
|
118
|
-
config = CytoCommunityConfig.from_json(config)
|
|
119
|
-
paths = build_paths(config)
|
|
120
|
-
params = config.downstream["dominant_cell_type"]
|
|
121
|
-
celltypes, matrices = load_enrichment_matrices(
|
|
122
|
-
paths.enrichment, paths.input_dir, config.celltype_order
|
|
123
|
-
)
|
|
124
|
-
result = analyze_dominant_celltypes(
|
|
125
|
-
matrices,
|
|
126
|
-
celltypes,
|
|
127
|
-
score_threshold=-np.log10(config.enrichment.p_value_threshold),
|
|
128
|
-
frequency_threshold=params["frequency_threshold"],
|
|
129
|
-
cell_type_groups=params["cell_type_groups"],
|
|
130
|
-
)
|
|
131
|
-
output_dir = Path(output_dir or paths.downstream_data / "DominateCT")
|
|
132
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
-
result.frequency.to_csv(output_dir / "CN_CT_Frequency.csv", float_format="%.4f")
|
|
134
|
-
result.dominant_cell_types.to_csv(output_dir / "DominantCT.csv", index=False)
|
|
135
|
-
result.subtype_scores.to_csv(output_dir / "CN_Subtype_Scores.csv", index=False)
|
|
136
|
-
return result
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def load_condition_enrichment(enrichment_dir, input_dir, celltype_order):
|
|
14
|
+
def load_condition_enrichment(enrichment_dir, input_dir):
|
|
140
15
|
"""Load enrichment matrices together with each sample's graph label."""
|
|
141
|
-
celltypes, matrices = load_enrichment_matrices(
|
|
142
|
-
enrichment_dir, input_dir
|
|
16
|
+
celltypes, matrices = load_enrichment_matrices(
|
|
17
|
+
enrichment_dir, input_dir
|
|
143
18
|
)
|
|
144
19
|
input_dir = Path(input_dir)
|
|
145
20
|
samples = (
|
|
@@ -161,8 +36,8 @@ def load_condition_enrichment(enrichment_dir, input_dir, celltype_order):
|
|
|
161
36
|
return celltypes, records
|
|
162
37
|
|
|
163
38
|
|
|
164
|
-
def build_label_comparisons(labels, mode):
|
|
165
|
-
"""Build the directional comparisons used by composition tests."""
|
|
39
|
+
def build_label_comparisons(labels, mode):
|
|
40
|
+
"""Build the directional comparisons used by composition tests."""
|
|
166
41
|
labels = list(labels)
|
|
167
42
|
if mode == "pairwise":
|
|
168
43
|
return [
|
|
@@ -191,49 +66,37 @@ def build_label_comparisons(labels, mode):
|
|
|
191
66
|
raise ValueError(f"Unknown comparison mode: {mode}")
|
|
192
67
|
|
|
193
68
|
|
|
194
|
-
@lru_cache(maxsize=None)
|
|
195
|
-
def _target_index_combinations(n_total, n_target):
|
|
196
|
-
"""Return all target-group allocations for an exact permutation test."""
|
|
197
|
-
return np.asarray(
|
|
198
|
-
list(combinations(range(n_total), n_target)),
|
|
199
|
-
dtype=np.int16,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
|
|
203
69
|
def composition_wilcoxon_tests(cell_type, target_values, reference_values):
|
|
204
|
-
"""Run one-sided
|
|
205
|
-
target = np.asarray(target_values, dtype=float)
|
|
206
|
-
reference = np.asarray(reference_values, dtype=float)
|
|
70
|
+
"""Run one-sided asymptotic Wilcoxon rank-sum tests."""
|
|
71
|
+
target = np.asarray(target_values, dtype=float)
|
|
72
|
+
reference = np.asarray(reference_values, dtype=float)
|
|
207
73
|
if len(target) < 2 or len(reference) < 2:
|
|
208
74
|
return None
|
|
209
75
|
if not np.isfinite(target).all() or not np.isfinite(reference).all():
|
|
210
76
|
return None
|
|
211
77
|
pooled_ranks = stats.rankdata(np.concatenate([target, reference]), method="average")
|
|
212
78
|
target_rank_sum = float(pooled_ranks[: len(target)].sum())
|
|
213
|
-
allocations = _target_index_combinations(len(pooled_ranks), len(target))
|
|
214
|
-
null_rank_sums = pooled_ranks[allocations].sum(axis=1)
|
|
215
79
|
|
|
216
80
|
rows = []
|
|
217
81
|
for alternative in ("greater", "less"):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
82
|
+
test = stats.mannwhitneyu(
|
|
83
|
+
target,
|
|
84
|
+
reference,
|
|
85
|
+
alternative=alternative,
|
|
86
|
+
method="asymptotic",
|
|
87
|
+
use_continuity=True,
|
|
88
|
+
)
|
|
226
89
|
rows.append(
|
|
227
90
|
{
|
|
228
91
|
"CellType": cell_type,
|
|
229
92
|
"Alternative": alternative,
|
|
230
|
-
"p_value": float(
|
|
231
|
-
"Test": "Wilcoxon rank-sum
|
|
232
|
-
"W_statistic": target_rank_sum,
|
|
233
|
-
"N_target": len(target),
|
|
234
|
-
"N_reference": len(reference),
|
|
235
|
-
}
|
|
236
|
-
)
|
|
93
|
+
"p_value": float(test.pvalue),
|
|
94
|
+
"Test": "Wilcoxon rank-sum asymptotic",
|
|
95
|
+
"W_statistic": target_rank_sum,
|
|
96
|
+
"N_target": len(target),
|
|
97
|
+
"N_reference": len(reference),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
237
100
|
return rows
|
|
238
101
|
|
|
239
102
|
|
|
@@ -271,9 +134,9 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
271
134
|
for record in normalized_records
|
|
272
135
|
if record["condition"] in comparison["reference_labels"]
|
|
273
136
|
]
|
|
274
|
-
result = composition_wilcoxon_tests(
|
|
275
|
-
cell_type, target, reference
|
|
276
|
-
)
|
|
137
|
+
result = composition_wilcoxon_tests(
|
|
138
|
+
cell_type, target, reference
|
|
139
|
+
)
|
|
277
140
|
if result is not None:
|
|
278
141
|
rows.extend(result)
|
|
279
142
|
if rows:
|
|
@@ -286,20 +149,20 @@ def run_differential_composition_analysis(config, *, output_dir=None):
|
|
|
286
149
|
if not isinstance(config, CytoCommunityConfig):
|
|
287
150
|
config = CytoCommunityConfig.from_json(config)
|
|
288
151
|
paths = build_paths(config)
|
|
289
|
-
celltypes, records = load_condition_enrichment(
|
|
290
|
-
paths.enrichment, paths.input_dir
|
|
152
|
+
celltypes, records = load_condition_enrichment(
|
|
153
|
+
paths.enrichment, paths.input_dir
|
|
291
154
|
)
|
|
292
155
|
outputs = analyze_differential_composition(
|
|
293
156
|
records, celltypes, mode=config.downstream["ttest_mode"]
|
|
294
157
|
)
|
|
295
158
|
output_dir = Path(output_dir or paths.downstream_data / "DifferentialComposed")
|
|
296
|
-
for comparison, cn_tables in outputs.items():
|
|
297
|
-
comparison_dir = output_dir / comparison
|
|
298
|
-
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
299
|
-
for old_file in comparison_dir.glob("CN*.csv"):
|
|
300
|
-
old_file.unlink()
|
|
301
|
-
for cn, table in cn_tables.items():
|
|
302
|
-
table.to_csv(comparison_dir / f"{cn}_{comparison}.csv", index=False)
|
|
159
|
+
for comparison, cn_tables in outputs.items():
|
|
160
|
+
comparison_dir = output_dir / comparison
|
|
161
|
+
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
for old_file in comparison_dir.glob("CN*.csv"):
|
|
163
|
+
old_file.unlink()
|
|
164
|
+
for cn, table in cn_tables.items():
|
|
165
|
+
table.to_csv(comparison_dir / f"{cn}_{comparison}.csv", index=False)
|
|
303
166
|
return outputs
|
|
304
167
|
|
|
305
168
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Dominant cell-type analysis."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ..config import CytoCommunityConfig
|
|
10
|
+
from ..paths import build_paths
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class DominantCellTypeResult:
|
|
15
|
+
"""In-memory outputs of dominant-cell-type analysis."""
|
|
16
|
+
|
|
17
|
+
frequency: pd.DataFrame
|
|
18
|
+
dominant_cell_types: pd.DataFrame
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_enrichment_matrices(enrichment_dir, input_dir):
|
|
22
|
+
"""Load per-sample enrichment matrices in their stored cell-type order."""
|
|
23
|
+
enrichment_dir = Path(enrichment_dir)
|
|
24
|
+
input_dir = Path(input_dir)
|
|
25
|
+
source_celltypes = (
|
|
26
|
+
pd.read_csv(enrichment_dir / "CellTypeVec_List.csv")
|
|
27
|
+
.iloc[:, 0]
|
|
28
|
+
.astype(str)
|
|
29
|
+
.str.strip()
|
|
30
|
+
.tolist()
|
|
31
|
+
)
|
|
32
|
+
samples = pd.read_csv(input_dir / "ImageNameList.txt", header=None).iloc[:, 0]
|
|
33
|
+
matrices = [
|
|
34
|
+
pd.read_csv(
|
|
35
|
+
enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
|
|
36
|
+
).to_numpy(dtype=float)
|
|
37
|
+
for sample in samples
|
|
38
|
+
]
|
|
39
|
+
if not matrices:
|
|
40
|
+
raise ValueError("ImageNameList.txt does not contain any samples")
|
|
41
|
+
return source_celltypes, np.stack(matrices, axis=0)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def analyze_dominant_celltypes(
|
|
45
|
+
enrichment_matrices,
|
|
46
|
+
celltypes,
|
|
47
|
+
*,
|
|
48
|
+
score_threshold,
|
|
49
|
+
frequency_threshold,
|
|
50
|
+
):
|
|
51
|
+
"""Compute dominant cell types from aligned enrichment matrices."""
|
|
52
|
+
matrices = np.asarray(enrichment_matrices, dtype=float)
|
|
53
|
+
if matrices.ndim != 3:
|
|
54
|
+
raise ValueError("enrichment_matrices must have shape (samples, cell types, CNs)")
|
|
55
|
+
if matrices.shape[1] != len(celltypes):
|
|
56
|
+
raise ValueError("celltypes length does not match enrichment_matrices")
|
|
57
|
+
|
|
58
|
+
frequencies = (matrices > score_threshold).mean(axis=0)
|
|
59
|
+
cn_names = [f"CN{index}" for index in range(1, matrices.shape[2] + 1)]
|
|
60
|
+
frequency = pd.DataFrame(frequencies, index=celltypes, columns=cn_names)
|
|
61
|
+
dominant = pd.DataFrame(
|
|
62
|
+
{
|
|
63
|
+
"CN": cn_names,
|
|
64
|
+
"DominantCellTypes": [
|
|
65
|
+
", ".join(
|
|
66
|
+
np.asarray(celltypes)[frequencies[:, cn_index] > frequency_threshold]
|
|
67
|
+
)
|
|
68
|
+
for cn_index in range(matrices.shape[2])
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
return DominantCellTypeResult(frequency, dominant)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_dominant_celltype_analysis(config, *, output_dir=None):
|
|
76
|
+
"""Run dominant-cell-type analysis using the established repository layout."""
|
|
77
|
+
if not isinstance(config, CytoCommunityConfig):
|
|
78
|
+
config = CytoCommunityConfig.from_json(config)
|
|
79
|
+
paths = build_paths(config)
|
|
80
|
+
params = config.downstream["dominant_cell_type"]
|
|
81
|
+
celltypes, matrices = load_enrichment_matrices(
|
|
82
|
+
paths.enrichment, paths.input_dir
|
|
83
|
+
)
|
|
84
|
+
result = analyze_dominant_celltypes(
|
|
85
|
+
matrices,
|
|
86
|
+
celltypes,
|
|
87
|
+
score_threshold=-np.log10(config.enrichment.p_value_threshold),
|
|
88
|
+
frequency_threshold=params["frequency_threshold"],
|
|
89
|
+
)
|
|
90
|
+
output_dir = Path(output_dir or paths.downstream_data / "DominateCT")
|
|
91
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
result.frequency.to_csv(output_dir / "CN_CT_Frequency.csv", float_format="%.4f")
|
|
93
|
+
result.dominant_cell_types.to_csv(output_dir / "DominantCT.csv", index=False)
|
|
94
|
+
return result
|
|
95
|
+
|
|
@@ -319,7 +319,7 @@ def plot_communication_results(
|
|
|
319
319
|
figure_dir, table_dir = stack_root / "figures", stack_root / "tables"
|
|
320
320
|
figure_dir.mkdir(parents=True, exist_ok=True)
|
|
321
321
|
table_dir.mkdir(parents=True, exist_ok=True)
|
|
322
|
-
present_celltypes =
|
|
322
|
+
present_celltypes = scores["CellType"].astype(str).drop_duplicates().tolist()
|
|
323
323
|
colors = _color_map(present_celltypes)
|
|
324
324
|
pd.DataFrame({"CellType": list(colors), "Color": [mpl.colors.to_hex(color) for color in colors.values()]}).to_csv(table_dir / "CellType_ColorMap.csv", index=False)
|
|
325
325
|
outputs["stackplot"].extend(_save_stack_legends(colors, figure_dir, params["permutation_p_threshold"]))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Dominant-cell-type frequency heatmap."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from ...paths import build_paths
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _prepare(result):
|
|
10
|
+
frequency = result.frequency.copy()
|
|
11
|
+
frequency.columns = frequency.columns.astype(str).str.strip()
|
|
12
|
+
frequency.index = frequency.index.astype(str).str.strip()
|
|
13
|
+
frequency = frequency.apply(pd.to_numeric, errors="coerce")
|
|
14
|
+
cn_numbers = pd.to_numeric(
|
|
15
|
+
frequency.columns.to_series().str.extract(r"(\d+)", expand=False),
|
|
16
|
+
errors="coerce",
|
|
17
|
+
)
|
|
18
|
+
cn_order = cn_numbers.sort_values(kind="stable").index.tolist()
|
|
19
|
+
return frequency.loc[:, cn_order]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def plot_dominant_celltype_results(result, config):
|
|
23
|
+
"""Save the frequency heatmap and its colorbar."""
|
|
24
|
+
import matplotlib as mpl
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
|
|
27
|
+
mpl.rcParams.update({
|
|
28
|
+
"font.family": "Arial",
|
|
29
|
+
"font.size": 12,
|
|
30
|
+
"axes.linewidth": 0.8,
|
|
31
|
+
"lines.linewidth": 1.2,
|
|
32
|
+
"xtick.labelsize": 10,
|
|
33
|
+
"ytick.labelsize": 10,
|
|
34
|
+
"pdf.fonttype": 42,
|
|
35
|
+
})
|
|
36
|
+
output_dir = build_paths(config).downstream_plot / "DominateCT"
|
|
37
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
frequency = _prepare(result)
|
|
39
|
+
|
|
40
|
+
rows, columns = frequency.shape
|
|
41
|
+
heat_height = 0.68 * 10 * rows / (8 * columns)
|
|
42
|
+
figure = plt.figure(figsize=(10, 8))
|
|
43
|
+
heat_axis = figure.add_axes([0.10, 0.10, 0.68, heat_height])
|
|
44
|
+
heat_axis.imshow(
|
|
45
|
+
frequency.to_numpy(), cmap="RdYlBu_r", vmin=0, vmax=1,
|
|
46
|
+
aspect="auto", interpolation="nearest", origin="upper",
|
|
47
|
+
)
|
|
48
|
+
heat_axis.set_xticks(np.arange(columns))
|
|
49
|
+
heat_axis.set_xticklabels(frequency.columns, rotation=90, fontsize=10)
|
|
50
|
+
heat_axis.set_yticks(np.arange(rows))
|
|
51
|
+
heat_axis.set_yticklabels(frequency.index, fontsize=10)
|
|
52
|
+
for label in heat_axis.get_yticklabels():
|
|
53
|
+
label.set_horizontalalignment("right")
|
|
54
|
+
heat_axis.tick_params(axis="x", length=0, bottom=True, top=False, labelbottom=True)
|
|
55
|
+
heat_axis.tick_params(axis="y", length=0, pad=6)
|
|
56
|
+
heat_axis.set_xticks(np.arange(-0.5, columns, 1), minor=True)
|
|
57
|
+
heat_axis.set_yticks(np.arange(-0.5, rows, 1), minor=True)
|
|
58
|
+
heat_axis.grid(which="minor", color="white", linewidth=0.35)
|
|
59
|
+
heat_axis.tick_params(which="minor", bottom=False, left=False)
|
|
60
|
+
for spine in heat_axis.spines.values():
|
|
61
|
+
spine.set_visible(False)
|
|
62
|
+
heatmap_output = output_dir / "heatmap_main.pdf"
|
|
63
|
+
figure.savefig(heatmap_output, bbox_inches="tight", dpi=1200)
|
|
64
|
+
figure.savefig(heatmap_output.with_suffix(".png"), bbox_inches="tight", dpi=300)
|
|
65
|
+
plt.close(figure)
|
|
66
|
+
|
|
67
|
+
figure = plt.figure(figsize=(1.2, 3.2))
|
|
68
|
+
axis = figure.add_axes([0.45, 0.08, 0.22, 0.84])
|
|
69
|
+
scalar_map = mpl.cm.ScalarMappable(norm=mpl.colors.Normalize(vmin=0, vmax=1), cmap="RdYlBu_r")
|
|
70
|
+
scalar_map.set_array([])
|
|
71
|
+
colorbar = figure.colorbar(scalar_map, cax=axis)
|
|
72
|
+
colorbar.set_ticks([0.0, 0.5, 1.0])
|
|
73
|
+
colorbar.ax.tick_params(labelsize=10, length=3, pad=2)
|
|
74
|
+
colorbar.outline.set_visible(False)
|
|
75
|
+
colorbar_output = output_dir / "colorbar_only.pdf"
|
|
76
|
+
figure.savefig(colorbar_output, bbox_inches="tight", dpi=1200)
|
|
77
|
+
plt.close(figure)
|
|
78
|
+
return {"heatmap": heatmap_output, "colorbar": colorbar_output}
|