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.
Files changed (44) hide show
  1. {cytocommunity2-0.1.4/src/cytocommunity2.egg-info → cytocommunity2-0.1.6}/PKG-INFO +1 -1
  2. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/pyproject.toml +1 -1
  3. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/__init__.py +1 -1
  4. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/config.py +1 -6
  5. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/__init__.py +9 -11
  6. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/cca.py +6 -5
  7. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/within.py +3 -2
  8. cytocommunity2-0.1.4/src/cytocommunity2/downstream/composition.py → cytocommunity2-0.1.6/src/cytocommunity2/downstream/differential_composition.py +36 -173
  9. cytocommunity2-0.1.6/src/cytocommunity2/downstream/dominant_celltype.py +95 -0
  10. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/communication.py +1 -1
  11. cytocommunity2-0.1.6/src/cytocommunity2/downstream/plotting/dominant.py +78 -0
  12. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/dotplots.py +262 -256
  13. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/visualization/spatial.py +37 -29
  14. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6/src/cytocommunity2.egg-info}/PKG-INFO +1 -1
  15. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/SOURCES.txt +2 -1
  16. cytocommunity2-0.1.4/src/cytocommunity2/downstream/plotting/dominant.py +0 -142
  17. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/LICENSE +0 -0
  18. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/README.md +0 -0
  19. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/setup.cfg +0 -0
  20. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/colors.py +0 -0
  21. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/coherence.py +0 -0
  22. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
  23. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/between.py +0 -0
  24. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/communication/utils.py +0 -0
  25. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/moran.py +0 -0
  26. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
  27. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -0
  28. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/coherence.py +0 -0
  29. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/common.py +0 -0
  30. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/composition.py +0 -0
  31. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/plotting/recurrence.py +0 -0
  32. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/downstream/runtime.py +0 -0
  33. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/ensemble.py +0 -0
  34. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/__init__.py +0 -0
  35. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/api.py +0 -0
  36. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/dataset.py +0 -0
  37. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/model.py +0 -0
  38. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/selection.py +0 -0
  39. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/learning/training.py +0 -0
  40. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/paths.py +0 -0
  41. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2/visualization/__init__.py +0 -0
  42. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
  43. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/requires.txt +0 -0
  44. {cytocommunity2-0.1.4 → cytocommunity2-0.1.6}/src/cytocommunity2.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cytocommunity2
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Weakly supervised cellular-neighborhood learning for spatial omics
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cytocommunity2"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "Weakly supervised cellular-neighborhood learning for spatial omics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -10,7 +10,7 @@ from .learning import (
10
10
  )
11
11
  from .visualization import visualize_results
12
12
 
13
- __version__ = "0.1.4"
13
+ __version__ = "0.1.6"
14
14
 
15
15
  __all__ = [
16
16
  "CytoCommunityConfig",
@@ -75,12 +75,7 @@ class CytoCommunityConfig:
75
75
  visualization: VisualizationConfig
76
76
  downstream: Mapping[str, Any]
77
77
 
78
- @property
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 .composition import (
10
- run_differential_composition_analysis,
11
- run_dominant_celltype_analysis,
12
- )
13
-
14
- __all__ = [
15
- "run_coherence_analysis",
16
- "run_differential_composition_analysis",
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
+ ]
@@ -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
- config.celltype_order,
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
- config.celltype_order,
295
+ celltype_order,
295
296
  params["cca_canonical_components"],
296
297
  condition,
297
298
  )
@@ -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=config.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
- """Cell-type composition and CN-variation analyses."""
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
- SUBTYPE_NAMES = {
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, celltype_order
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 exact Wilcoxon rank-sum permutation tests."""
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
- if alternative == "greater":
219
- p_value = np.count_nonzero(null_rank_sums >= target_rank_sum) / len(
220
- null_rank_sums
221
- )
222
- else:
223
- p_value = np.count_nonzero(null_rank_sums <= target_rank_sum) / len(
224
- null_rank_sums
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(p_value),
231
- "Test": "Wilcoxon rank-sum exact permutation",
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, config.celltype_order
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 = [celltype for celltype in config.celltype_order if celltype in scores["CellType"].astype(str).unique()]
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}