cytocommunity2 0.1.2__tar.gz → 0.1.4__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 (42) hide show
  1. {cytocommunity2-0.1.2/src/cytocommunity2.egg-info → cytocommunity2-0.1.4}/PKG-INFO +2 -3
  2. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/README.md +0 -1
  3. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/pyproject.toml +2 -2
  4. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/__init__.py +9 -9
  5. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/config.py +7 -6
  6. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/api.py +67 -50
  7. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/dataset.py +9 -7
  8. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/selection.py +173 -235
  9. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/training.py +15 -10
  10. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4/src/cytocommunity2.egg-info}/PKG-INFO +2 -3
  11. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/requires.txt +1 -1
  12. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/LICENSE +0 -0
  13. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/setup.cfg +0 -0
  14. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/colors.py +0 -0
  15. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/__init__.py +0 -0
  16. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/coherence.py +0 -0
  17. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
  18. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/between.py +0 -0
  19. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/cca.py +0 -0
  20. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/utils.py +0 -0
  21. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/within.py +0 -0
  22. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/composition.py +0 -0
  23. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/moran.py +0 -0
  24. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
  25. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -0
  26. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/coherence.py +0 -0
  27. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/common.py +0 -0
  28. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/communication.py +0 -0
  29. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/composition.py +0 -0
  30. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dominant.py +0 -0
  31. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dotplots.py +0 -0
  32. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/recurrence.py +0 -0
  33. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/runtime.py +0 -0
  34. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/ensemble.py +0 -0
  35. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/__init__.py +0 -0
  36. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/model.py +0 -0
  37. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/paths.py +0 -0
  38. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/visualization/__init__.py +0 -0
  39. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2/visualization/spatial.py +0 -0
  40. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/SOURCES.txt +0 -0
  41. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
  42. {cytocommunity2-0.1.2 → cytocommunity2-0.1.4}/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.2
3
+ Version: 0.1.4
4
4
  Summary: Weakly supervised cellular-neighborhood learning for spatial omics
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -10,7 +10,7 @@ Requires-Dist: numpy<2,>=1.23
10
10
  Requires-Dist: pandas<3,>=1.5
11
11
  Requires-Dist: sci-palettes
12
12
  Requires-Dist: scikit-learn>=1.1
13
- Requires-Dist: scipy>=1.9
13
+ Requires-Dist: scipy>=1.11
14
14
  Requires-Dist: seaborn>=0.12
15
15
  Requires-Dist: torch-geometric>=2.0
16
16
  Provides-Extra: downstream
@@ -81,7 +81,6 @@ conda create -n cytocommunity2 python=3.10
81
81
  conda activate cytocommunity2
82
82
  ```
83
83
 
84
- Install PyTorch for your compute platform before installing CytoCommunity2.
85
84
  For an NVIDIA GPU on Windows or Linux, first install the appropriate
86
85
  CUDA-enabled PyTorch build from the
87
86
  [official PyTorch installation page](https://pytorch.org/get-started/locally/).
@@ -60,7 +60,6 @@ conda create -n cytocommunity2 python=3.10
60
60
  conda activate cytocommunity2
61
61
  ```
62
62
 
63
- Install PyTorch for your compute platform before installing CytoCommunity2.
64
63
  For an NVIDIA GPU on Windows or Linux, first install the appropriate
65
64
  CUDA-enabled PyTorch build from the
66
65
  [official PyTorch installation page](https://pytorch.org/get-started/locally/).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cytocommunity2"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Weakly supervised cellular-neighborhood learning for spatial omics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "pandas>=1.5,<3",
15
15
  "sci-palettes",
16
16
  "scikit-learn>=1.1",
17
- "scipy>=1.9",
17
+ "scipy>=1.11",
18
18
  "seaborn>=0.12",
19
19
  "torch-geometric>=2.0",
20
20
  ]
@@ -2,22 +2,22 @@
2
2
 
3
3
  from .config import CytoCommunityConfig
4
4
  from .learning import (
5
- LearningResult,
6
- SpatialGraphDataset,
7
- build_spatial_graphs,
8
- load_learning_result,
9
- run_learning,
5
+ LearningResult,
6
+ SpatialGraphDataset,
7
+ build_spatial_graphs,
8
+ load_learning_result,
9
+ run_learning,
10
10
  )
11
11
  from .visualization import visualize_results
12
12
 
13
- __version__ = "0.1.1"
13
+ __version__ = "0.1.4"
14
14
 
15
15
  __all__ = [
16
16
  "CytoCommunityConfig",
17
17
  "LearningResult",
18
18
  "SpatialGraphDataset",
19
- "build_spatial_graphs",
20
- "load_learning_result",
21
- "run_learning",
19
+ "build_spatial_graphs",
20
+ "load_learning_result",
21
+ "run_learning",
22
22
  "visualize_results",
23
23
  ]
@@ -8,7 +8,6 @@ from typing import Any, Mapping
8
8
 
9
9
 
10
10
  DEFAULT_CONFIG_FILE = "hyperparameters.json"
11
- INPUT_DIR_NAME = "TNBC_Input_pCR"
12
11
 
13
12
  # Established analysis rules stay stable across runs; only experimental choices
14
13
  # belong in hyperparameters.json.
@@ -94,13 +93,15 @@ def _require_keys(values, required, section):
94
93
 
95
94
 
96
95
  def _load_config(config_file=None, config_class=CytoCommunityConfig):
97
- """Load one configuration file and use the adjacent TNBC input folder."""
98
- config_file = Path(config_file or DEFAULT_CONFIG_FILE).expanduser().resolve()
99
- raw = json.loads(config_file.read_text(encoding="utf-8"))
100
- _require_keys(raw, ["random_seed", "main", "downstream"], "root")
96
+ """Load one configuration file and resolve its configured input folder."""
97
+ config_file = Path(config_file or DEFAULT_CONFIG_FILE).expanduser().resolve()
98
+ raw = json.loads(config_file.read_text(encoding="utf-8"))
99
+ _require_keys(raw, ["input_dir", "random_seed", "main", "downstream"], "root")
101
100
 
102
101
  project_root = config_file.parent
103
- input_dir = project_root / INPUT_DIR_NAME
102
+ input_dir = Path(raw["input_dir"]).expanduser()
103
+ if not input_dir.is_absolute():
104
+ input_dir = project_root / input_dir
104
105
 
105
106
  main = MainConfig(**raw["main"])
106
107
  enrichment = EnrichmentConfig()
@@ -3,12 +3,14 @@
3
3
  import datetime
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
- import shutil
7
-
8
- import pandas as pd
6
+ import shutil
7
+
8
+ import numpy as np
9
+ import pandas as pd
9
10
 
10
- from ..config import CytoCommunityConfig
11
- from ..ensemble import ensemble_cn
11
+ from ..config import CytoCommunityConfig
12
+ from ..ensemble import ensemble_cn
13
+ from ..paths import build_paths
12
14
  from .dataset import SpatialGraphDataset
13
15
  from .model import TrainingConfig
14
16
  from .selection import evaluate_cn_num, select_cn_num
@@ -33,11 +35,9 @@ class LearningResult:
33
35
  def load_learning_result(
34
36
  dataset: SpatialGraphDataset,
35
37
  config: CytoCommunityConfig,
36
- *,
37
- output_dir,
38
38
  ) -> LearningResult:
39
39
  """Restore a :class:`LearningResult` from a completed learning run."""
40
- output_dir = Path(output_dir).expanduser().resolve()
40
+ output_dir = build_paths(config).learning_output
41
41
  paths = {
42
42
  "selected CN number": output_dir / "SelectedCNNum.txt",
43
43
  "cell-type list": output_dir / "UniqueCellTypeList.txt",
@@ -86,13 +86,11 @@ def _training_config(config, num_cn, num_class):
86
86
 
87
87
 
88
88
  def run_learning(
89
- dataset: SpatialGraphDataset,
90
- config: CytoCommunityConfig,
91
- *,
92
- output_dir,
89
+ dataset: SpatialGraphDataset,
90
+ config: CytoCommunityConfig,
93
91
  ) -> LearningResult:
94
92
  """Select the CN number, train final runs, and export consensus labels."""
95
- output_dir = Path(output_dir).expanduser().resolve()
93
+ output_dir = build_paths(config).learning_output
96
94
  input_dir = config.input_dir
97
95
  unique_celltypes_file = output_dir / "UniqueCellTypeList.txt"
98
96
  selected_model_dir = output_dir / "SelectedModel"
@@ -136,7 +134,11 @@ def run_learning(
136
134
  collect_hard_assignments=True,
137
135
  )
138
136
 
139
- result, cn_pvalues = evaluate_cn_num(
137
+ cn_labels_by_sample = {
138
+ sample: hard_assignments[sample_index]
139
+ for sample_index, sample in enumerate(dataset.sample_names)
140
+ }
141
+ result, cn_pvalues = evaluate_cn_num(
140
142
  num_cn=num_cn,
141
143
  dataset=input_dir,
142
144
  celltype_file=unique_celltypes_file,
@@ -146,30 +148,35 @@ def run_learning(
146
148
  recurrence_p_threshold=config.recurrence.recurrence_p_threshold,
147
149
  n_random=config.recurrence.null_draws,
148
150
  seed=config.random_seed,
149
- hard_assignments=hard_assignments,
150
- )
151
- del hard_assignments
151
+ cn_labels_by_sample=cn_labels_by_sample,
152
+ )
153
+ del hard_assignments, cn_labels_by_sample
152
154
  selection_results.append(result)
153
- cn_pvalues = cn_pvalues[
154
- [
155
- "CN",
156
- "p_value_one_sided",
157
- "p_adjusted_bh",
158
- "RecurrenceScore",
155
+ cn_pvalues = cn_pvalues[
156
+ [
157
+ "CN",
158
+ "n_real",
159
+ "n_background",
160
+ "real_mean",
161
+ "background_mean",
162
+ "delta_mean",
163
+ "KSStatistic",
164
+ "KSLocation",
165
+ "p_value_one_sided",
166
+ "p_adjusted_bh",
167
+ "RecurrenceScore",
159
168
  "SignificantRecurrence",
160
169
  ]
161
170
  ].copy()
162
171
  cn_pvalues.insert(0, "NumCN", num_cn)
163
- cn_pvalue_tables.append(cn_pvalues)
164
- print(
165
- f"K={num_cn}: significantCN={result['SignificantCN']}, "
166
- f"significantFraction={result['SignificantFraction']:.6f}, "
167
- f"meanRS={result['MeanRS']:.6f}, score={result['Score']:.6f}"
172
+ cn_pvalue_tables.append(cn_pvalues)
173
+ print(
174
+ f"K={num_cn}: Ksig={result['Ksig']}, "
175
+ f"significantFraction={result['SignificantFraction']:.6f}, "
176
+ f"MeanRS={result['MeanRS']:.6f}, CRI={result['CRI']:.6f}"
168
177
  )
169
178
 
170
- pd.DataFrame(selection_results).sort_values("NumCN").to_csv(
171
- selection_summary_file, index=False
172
- )
179
+ pd.DataFrame(selection_results).to_csv(selection_summary_file, index=False)
173
180
  pd.concat(cn_pvalue_tables, ignore_index=True).to_csv(
174
181
  cn_pvalues_file, index=False
175
182
  )
@@ -178,9 +185,9 @@ def run_learning(
178
185
  (output_dir / "SelectedCNNum.txt").write_text(
179
186
  f"{selected_cn}\n", encoding="utf-8"
180
187
  )
181
- print(
182
- f"\nSelected K={selected_cn} with score={selected['Score']:.6f}. "
183
- f"Training a fresh set of Runs 1-{config.main.final_num_runs}."
188
+ print(
189
+ f"\nSelected K={selected_cn} with CRI={selected['CRI']:.6f}. "
190
+ f"Training a fresh set of Runs 1-{config.main.final_num_runs}."
184
191
  )
185
192
 
186
193
  selected_config = _training_config(
@@ -188,22 +195,32 @@ def run_learning(
188
195
  )
189
196
  for run_index in range(1, config.main.final_num_runs + 1):
190
197
  print(f"\n=== Selected K={selected_cn}: Run {run_index} ===")
191
- train_one_run(
192
- dataset,
193
- selected_config,
194
- run_index=run_index,
195
- output_folder=selected_model_dir / f"Run{run_index}",
196
- dynamic_pseudo=dataset.dynamic_pseudo,
197
- base_seed=config.random_seed,
198
- save_soft_assignments=True,
199
- collect_hard_assignments=False,
200
- )
201
-
202
- ensemble_cn(
203
- input_dir=input_dir,
204
- selected_model_dir=selected_model_dir,
205
- hard_assignment_dir=hard_assignment_dir,
206
- )
198
+ _, hard_assignments = train_one_run(
199
+ dataset,
200
+ selected_config,
201
+ run_index=run_index,
202
+ output_folder=selected_model_dir / f"Run{run_index}",
203
+ dynamic_pseudo=dataset.dynamic_pseudo,
204
+ base_seed=config.random_seed,
205
+ save_soft_assignments=True,
206
+ collect_hard_assignments=config.main.final_num_runs == 1,
207
+ )
208
+
209
+ if config.main.final_num_runs == 1:
210
+ hard_assignment_dir.mkdir()
211
+ for sample_index, sample in enumerate(dataset.sample_names):
212
+ np.savetxt(
213
+ hard_assignment_dir / f"{sample}_CNLabel_MajorityVoting.csv",
214
+ hard_assignments[sample_index],
215
+ delimiter=",",
216
+ fmt="%d",
217
+ )
218
+ else:
219
+ ensemble_cn(
220
+ input_dir=input_dir,
221
+ selected_model_dir=selected_model_dir,
222
+ hard_assignment_dir=hard_assignment_dir,
223
+ )
207
224
  return LearningResult(
208
225
  selected_cn=selected_cn,
209
226
  dataset=dataset,
@@ -5,9 +5,11 @@ from pathlib import Path
5
5
  import numpy as np
6
6
  from scipy.sparse import csr_matrix
7
7
  from sklearn.neighbors import NearestNeighbors
8
- import torch
9
- from torch_geometric.data import Data
10
-
8
+ import torch
9
+ from torch_geometric.data import Data
10
+
11
+ from ..config import CytoCommunityConfig
12
+
11
13
 
12
14
  class SpatialGraphDataset(torch.utils.data.Dataset):
13
15
  """In-memory collection of real spatial graphs."""
@@ -64,9 +66,9 @@ def _build_knn_edge_index(coordinates, knn_k):
64
66
  ).contiguous()
65
67
 
66
68
 
67
- def build_spatial_graphs(input_dir, *, knn_k):
68
- """Read real samples and construct their coordinate-based KNN graphs once."""
69
- input_folder = Path(input_dir)
69
+ def build_spatial_graphs(config: CytoCommunityConfig):
70
+ """Build spatial graphs from the input folder associated with ``config``."""
71
+ input_folder = config.input_dir
70
72
  sample_names = _read_lines(input_folder / "ImageNameList.txt")
71
73
  labels_by_sample = {
72
74
  sample: _read_lines(input_folder / f"{sample}_CellTypeLabel.txt")
@@ -125,7 +127,7 @@ def build_spatial_graphs(input_dir, *, knn_k):
125
127
  data_list.append(Data(
126
128
  x=x,
127
129
  y=torch.tensor([training_label], dtype=torch.long),
128
- edge_index=_build_knn_edge_index(coordinates, knn_k),
130
+ edge_index=_build_knn_edge_index(coordinates, config.main.knn_k),
129
131
  ))
130
132
 
131
133
  return SpatialGraphDataset(
@@ -5,13 +5,12 @@ from pathlib import Path
5
5
 
6
6
  import numpy as np
7
7
  import pandas as pd
8
- from scipy.stats import hypergeom, ttest_ind
8
+ from scipy.stats import false_discovery_control, hypergeom, ks_2samp
9
+
10
+ from ..paths import build_paths
11
+
9
12
 
10
-
11
- MIN_FLOAT_P = np.finfo(np.float64).tiny
12
-
13
-
14
- def read_nonempty_lines(path):
13
+ def read_nonempty_lines(path):
15
14
  values = [
16
15
  line.strip()
17
16
  for line in Path(path).read_text(encoding="utf-8-sig").splitlines()
@@ -22,42 +21,14 @@ def read_nonempty_lines(path):
22
21
  return values
23
22
 
24
23
 
25
- def _bh_adjust_matlab(pvalues):
26
- pvalues = np.asarray(pvalues, dtype=np.float64)
27
- flat = pvalues.ravel(order="F")
28
- order = np.argsort(flat, kind="stable")
29
- sorted_p = flat[order]
30
- weighted = sorted_p.size * sorted_p / np.arange(1, sorted_p.size + 1)
31
- adjusted_sorted = np.minimum.accumulate(weighted[::-1])[::-1]
32
- adjusted = np.empty_like(flat)
33
- adjusted[order] = adjusted_sorted
34
- return adjusted.reshape(pvalues.shape, order="F")
35
-
36
-
37
- def bh_adjust(pvalues):
38
- """Benjamini-Hochberg adjustment for one family of CN hypotheses."""
39
- pvalues = np.asarray(pvalues, dtype=np.float64)
40
- clean_pvalues = np.where(np.isfinite(pvalues), pvalues, 1.0)
41
- clean_pvalues = np.clip(clean_pvalues, 0.0, 1.0)
42
- order = np.argsort(clean_pvalues, kind="stable")
43
- sorted_pvalues = clean_pvalues[order]
44
- ranks = np.arange(1, sorted_pvalues.size + 1, dtype=np.float64)
45
- adjusted_sorted = np.minimum.accumulate(
46
- (sorted_pvalues.size * sorted_pvalues / ranks)[::-1]
47
- )[::-1]
48
- adjusted = np.empty_like(clean_pvalues)
49
- adjusted[order] = np.clip(adjusted_sorted, 0.0, 1.0)
50
- return adjusted
51
-
52
-
53
- def calculate_enrichment_matrix(
24
+ def calculate_enrichment_matrix(
54
25
  cell_labels,
55
26
  cn_labels,
56
27
  cell_types,
57
28
  num_cn,
58
29
  minimum_adjusted_p,
59
30
  ):
60
- """Reproduce CellTypeEnrichmentMatrix.m, including its tail precision."""
31
+ """Calculate BH-adjusted cell-type enrichment scores for one sample."""
61
32
  cell_labels = np.asarray(cell_labels, dtype=str)
62
33
  cn_labels = np.asarray(cn_labels, dtype=np.int64)
63
34
  if cell_labels.ndim != 1 or cn_labels.ndim != 1:
@@ -96,70 +67,47 @@ def calculate_enrichment_matrix(
96
67
  int(cell_type_totals[cell_type_index]),
97
68
  int(cn_totals[cn_index]),
98
69
  )
99
- raw_pvalues[cell_type_index, cn_index] = 1.0 - (1.0 - upper_tail)
70
+ raw_pvalues[cell_type_index, cn_index] = upper_tail
100
71
 
101
- adjusted = _bh_adjust_matlab(raw_pvalues)
72
+ adjusted = false_discovery_control(
73
+ raw_pvalues.ravel(order="F"), method="bh"
74
+ ).reshape(raw_pvalues.shape, order="F")
102
75
  adjusted[adjusted < minimum_adjusted_p] = minimum_adjusted_p
103
76
  return -np.log10(adjusted)
104
77
 
105
78
 
106
- def calculate_enrichment(
107
- dataset,
108
- celltype_file,
109
- output_dir,
110
- num_cn,
111
- minimum_adjusted_p,
112
- cn_label_dir=None,
113
- hard_assignments=None,
114
- ):
115
- dataset = Path(dataset)
116
- if (cn_label_dir is None) == (hard_assignments is None):
117
- raise ValueError(
118
- "Provide exactly one of cn_label_dir or hard_assignments"
119
- )
120
- if cn_label_dir is not None:
121
- cn_label_dir = Path(cn_label_dir)
122
- output_dir = Path(output_dir)
123
- samples = read_nonempty_lines(dataset / "ImageNameList.txt")
124
- cell_types = read_nonempty_lines(celltype_file)
125
- output_dir.mkdir(parents=True, exist_ok=True)
126
- pd.DataFrame({"Var1": cell_types}).to_csv(
127
- output_dir / "CellTypeVec_List.csv", index=False
128
- )
129
-
130
- rows = []
131
- for sample_index, sample in enumerate(samples):
132
- cell_labels = (
133
- pd.read_csv(
134
- dataset / f"{sample}_CellTypeLabel.txt",
79
+ def calculate_enrichment(
80
+ dataset,
81
+ cell_types,
82
+ cn_labels_by_sample,
83
+ output_dir,
84
+ num_cn,
85
+ minimum_adjusted_p,
86
+ ):
87
+ dataset = Path(dataset)
88
+ output_dir = Path(output_dir)
89
+ samples = read_nonempty_lines(dataset / "ImageNameList.txt")
90
+ output_dir.mkdir(parents=True, exist_ok=True)
91
+ pd.DataFrame({"Var1": cell_types}).to_csv(
92
+ output_dir / "CellTypeVec_List.csv", index=False
93
+ )
94
+
95
+ matrices = {}
96
+ for sample in samples:
97
+ cell_labels = (
98
+ pd.read_csv(
99
+ dataset / f"{sample}_CellTypeLabel.txt",
135
100
  sep="\t",
136
101
  header=None,
137
102
  dtype=str,
138
103
  )
139
- .iloc[:, 0]
140
- .to_numpy()
141
- )
142
- if hard_assignments is not None:
143
- if sample_index not in hard_assignments:
144
- raise KeyError(
145
- f"Missing in-memory CN labels for sample index {sample_index}"
146
- )
147
- cn_labels = np.asarray(
148
- hard_assignments[sample_index], dtype=np.int64
149
- )
150
- else:
151
- cn_labels = (
152
- pd.read_csv(
153
- cn_label_dir / f"{sample}_CNLabel_MajorityVoting.csv",
154
- header=None,
155
- dtype=np.int64,
156
- )
157
- .iloc[:, 0]
158
- .to_numpy()
159
- )
160
- matrix = calculate_enrichment_matrix(
161
- cell_labels,
162
- cn_labels,
104
+ .iloc[:, 0]
105
+ .to_numpy()
106
+ )
107
+ cn_labels = np.asarray(cn_labels_by_sample[sample], dtype=np.int64)
108
+ matrix = calculate_enrichment_matrix(
109
+ cell_labels,
110
+ cn_labels,
163
111
  cell_types,
164
112
  num_cn,
165
113
  minimum_adjusted_p,
@@ -167,11 +115,25 @@ def calculate_enrichment(
167
115
  np.savetxt(
168
116
  output_dir / f"{sample}_EnrichScoreMatrix.csv",
169
117
  matrix,
170
- delimiter=",",
171
- fmt="%.15g",
172
- )
173
- rows.append({"Sample": sample, "Cells": len(cell_labels), "NumCN": num_cn})
174
- return pd.DataFrame(rows)
118
+ delimiter=",",
119
+ fmt="%.15g",
120
+ )
121
+ matrices[sample] = matrix
122
+ return matrices
123
+
124
+
125
+ def _load_cn_labels(samples, cn_label_dir):
126
+ cn_label_dir = Path(cn_label_dir)
127
+ return {
128
+ sample: pd.read_csv(
129
+ cn_label_dir / f"{sample}_CNLabel_MajorityVoting.csv",
130
+ header=None,
131
+ dtype=np.int64,
132
+ )
133
+ .iloc[:, 0]
134
+ .to_numpy()
135
+ for sample in samples
136
+ }
175
137
 
176
138
 
177
139
  def export_enrichment_long(dataset, enrichment_dir, output_file):
@@ -221,82 +183,51 @@ def overlap_coefficient(first, second):
221
183
  return len(first & second) / smaller_size if smaller_size else 0.0
222
184
 
223
185
 
224
- def _load_dominant_sets(samples, score_dir, score_threshold):
225
- celltypes = (
226
- pd.read_csv(Path(score_dir) / "CellTypeVec_List.csv")
227
- .iloc[:, 0]
228
- .astype(str)
229
- .to_numpy()
230
- )
231
- dominant_sets = {}
232
- num_cn = None
233
- for sample in samples:
234
- matrix = pd.read_csv(
235
- Path(score_dir) / f"{sample}_EnrichScoreMatrix.csv", header=None
236
- ).to_numpy(dtype=float)
237
- if matrix.shape[0] != len(celltypes):
238
- raise ValueError(f"{sample}: enrichment matrix has the wrong row count")
239
- if num_cn is None:
240
- num_cn = matrix.shape[1]
241
- elif matrix.shape[1] != num_cn:
242
- raise ValueError(f"{sample}: inconsistent number of CN columns")
243
- dominant_sets[sample] = [
244
- set(celltypes[matrix[:, cn] > score_threshold])
245
- for cn in range(matrix.shape[1])
246
- ]
247
- return dominant_sets, num_cn
248
-
249
-
250
- def _summarize_ttest(cn, real_values, background_values):
251
- real_values = np.asarray(real_values, dtype=float)
252
- background_values = np.asarray(background_values, dtype=float)
253
- if len(real_values) >= 2 and len(background_values) >= 2:
254
- result = ttest_ind(
255
- real_values,
256
- background_values,
257
- equal_var=False,
258
- alternative="greater",
259
- )
260
- t_statistic, p_value = float(result.statistic), float(result.pvalue)
261
- else:
262
- t_statistic, p_value = np.nan, np.nan
263
- real_mean = np.mean(real_values) if len(real_values) else np.nan
264
- background_mean = (
265
- np.mean(background_values) if len(background_values) else np.nan
266
- )
267
- return {
268
- "CN": cn,
269
- "n_real": len(real_values),
270
- "n_background": len(background_values),
271
- "real_mean": real_mean,
272
- "real_std": np.std(real_values, ddof=1) if len(real_values) > 1 else np.nan,
273
- "background_mean": background_mean,
274
- "background_std": (
275
- np.std(background_values, ddof=1)
276
- if len(background_values) > 1
277
- else np.nan
278
- ),
279
- "delta_mean": real_mean - background_mean,
280
- "t_statistic": t_statistic,
281
- "p_value_one_sided": p_value,
186
+ def _summarize_ks(cn, real_values, background_values):
187
+ real_values = np.asarray(real_values, dtype=float)
188
+ background_values = np.asarray(background_values, dtype=float)
189
+ result = ks_2samp(
190
+ real_values,
191
+ background_values,
192
+ alternative="less",
193
+ method="asymp",
194
+ )
195
+ real_mean = float(np.mean(real_values))
196
+ background_mean = float(np.mean(background_values))
197
+ return {
198
+ "CN": cn,
199
+ "n_real": len(real_values),
200
+ "n_background": len(background_values),
201
+ "real_mean": real_mean,
202
+ "background_mean": background_mean,
203
+ "delta_mean": real_mean - background_mean,
204
+ "KSStatistic": float(result.statistic),
205
+ "KSLocation": float(result.statistic_location),
206
+ "p_value_one_sided": float(result.pvalue),
282
207
  }
283
208
 
284
209
 
285
- def calculate_recurrence(
286
- score_dir,
287
- image_list_file,
288
- output_dir,
210
+ def calculate_recurrence(
211
+ enrichment_matrices,
212
+ cell_types,
213
+ output_dir,
289
214
  score_threshold,
290
215
  recurrence_p_threshold,
291
- n_random,
292
- seed,
293
- ):
294
- samples = read_nonempty_lines(image_list_file)
295
- if len(samples) < 2:
296
- raise ValueError("At least two samples are required for recurrence analysis")
297
- dominant_sets, num_cn = _load_dominant_sets(
298
- samples, score_dir, score_threshold
299
- )
216
+ n_random,
217
+ seed,
218
+ ):
219
+ samples = list(enrichment_matrices)
220
+ if len(samples) < 2:
221
+ raise ValueError("At least two samples are required for recurrence analysis")
222
+ cell_types = np.asarray(cell_types, dtype=str)
223
+ num_cn = enrichment_matrices[samples[0]].shape[1]
224
+ dominant_sets = {
225
+ sample: [
226
+ set(cell_types[matrix[:, cn] > score_threshold])
227
+ for cn in range(num_cn)
228
+ ]
229
+ for sample, matrix in enrichment_matrices.items()
230
+ }
300
231
  sample_pairs = list(combinations(samples, 2))
301
232
  real_df = pd.DataFrame(
302
233
  [
@@ -322,35 +253,20 @@ def calculate_recurrence(
322
253
  )
323
254
  background_df = pd.DataFrame({"OverlapCoefficient": background_values})
324
255
 
325
- ttest_rows = []
256
+ ks_rows = []
326
257
  for cn in [f"CN{index + 1}" for index in range(num_cn)]:
327
258
  values = real_df.loc[
328
259
  real_df["CN"] == cn, "OverlapCoefficient"
329
260
  ].to_numpy()
330
- ttest_rows.append(_summarize_ttest(cn, values, background_values))
331
- ttest_rows.append(
332
- _summarize_ttest(
333
- "Overall", real_df["OverlapCoefficient"].to_numpy(), background_values
334
- )
335
- )
336
- ttest_df = pd.DataFrame(ttest_rows)
337
- cn_mask = ttest_df["CN"] != "Overall"
338
- adjusted_pvalues = bh_adjust(
339
- ttest_df.loc[cn_mask, "p_value_one_sided"].to_numpy()
340
- )
341
- ttest_df["p_adjusted_bh"] = np.nan
342
- ttest_df["RecurrenceScore"] = np.nan
343
- ttest_df["SignificantRecurrence"] = False
344
- ttest_df.loc[cn_mask, "p_adjusted_bh"] = adjusted_pvalues
345
- ttest_df.loc[cn_mask, "RecurrenceScore"] = -np.log10(
346
- np.maximum(adjusted_pvalues, MIN_FLOAT_P)
347
- )
348
- ttest_df.loc[cn_mask, "SignificantRecurrence"] = (
349
- (
350
- ttest_df.loc[cn_mask, "p_adjusted_bh"]
351
- < recurrence_p_threshold
352
- )
353
- & (ttest_df.loc[cn_mask, "delta_mean"] > 0)
261
+ ks_rows.append(_summarize_ks(cn, values, background_values))
262
+ ks_df = pd.DataFrame(ks_rows)
263
+ ks_df["p_adjusted_bh"] = false_discovery_control(
264
+ ks_df["p_value_one_sided"].to_numpy(), method="bh"
265
+ )
266
+ ks_df["RecurrenceScore"] = ks_df["KSStatistic"] * 100.0
267
+ ks_df["SignificantRecurrence"] = (
268
+ (ks_df["p_adjusted_bh"] < recurrence_p_threshold)
269
+ & (ks_df["delta_mean"] > 0)
354
270
  )
355
271
 
356
272
  output_dir = Path(output_dir)
@@ -359,68 +275,90 @@ def calculate_recurrence(
359
275
  background_df.to_csv(
360
276
  output_dir / "BackgroundDistribution_randomCN.csv", index=False
361
277
  )
362
- ttest_df.to_csv(output_dir / "TTest_Results.csv", index=False)
363
- return real_df, background_df, ttest_df
278
+ ks_df.to_csv(output_dir / "KSTest_Results.csv", index=False)
279
+ return real_df, background_df, ks_df
364
280
 
365
281
 
366
- def evaluate_cn_num(
282
+ def evaluate_cn_num(
367
283
  num_cn,
368
284
  dataset,
369
285
  celltype_file,
370
286
  output_dir,
371
287
  enrichment_p_threshold,
372
288
  minimum_adjusted_p,
373
- recurrence_p_threshold,
374
- n_random,
375
- seed,
376
- cn_label_dir=None,
377
- hard_assignments=None,
378
- ):
289
+ recurrence_p_threshold,
290
+ n_random,
291
+ seed,
292
+ cn_labels_by_sample,
293
+ ):
379
294
  """Calculate the recurrence-weighted CN resolution index and per-CN tests."""
380
295
  output_dir = Path(output_dir)
381
296
  enrichment_dir = output_dir / "EnrichScoreMatrix"
382
297
  overlap_dir = output_dir / "OverlapCoefficient"
383
- calculate_enrichment(
384
- dataset=dataset,
385
- celltype_file=celltype_file,
386
- output_dir=enrichment_dir,
387
- num_cn=num_cn,
388
- minimum_adjusted_p=minimum_adjusted_p,
389
- cn_label_dir=cn_label_dir,
390
- hard_assignments=hard_assignments,
391
- )
392
- real_df, _, ttest_df = calculate_recurrence(
393
- enrichment_dir,
394
- Path(dataset) / "ImageNameList.txt",
395
- overlap_dir,
396
- score_threshold=-np.log10(enrichment_p_threshold),
298
+ cell_types = read_nonempty_lines(celltype_file)
299
+ enrichment_matrices = calculate_enrichment(
300
+ dataset=dataset,
301
+ cell_types=cell_types,
302
+ cn_labels_by_sample=cn_labels_by_sample,
303
+ output_dir=enrichment_dir,
304
+ num_cn=num_cn,
305
+ minimum_adjusted_p=minimum_adjusted_p,
306
+ )
307
+ real_df, _, per_cn = calculate_recurrence(
308
+ enrichment_matrices=enrichment_matrices,
309
+ cell_types=cell_types,
310
+ output_dir=overlap_dir,
311
+ score_threshold=-np.log10(enrichment_p_threshold),
397
312
  recurrence_p_threshold=recurrence_p_threshold,
398
313
  n_random=n_random,
399
314
  seed=seed,
400
315
  )
401
- per_cn = ttest_df[ttest_df["CN"] != "Overall"]
402
- significant_mask = per_cn["SignificantRecurrence"].astype(bool)
316
+ significant_mask = per_cn["SignificantRecurrence"].astype(bool)
403
317
  significant_cn = int(significant_mask.sum())
404
318
  significant_fraction = significant_cn / num_cn
405
319
  mean_rs = float(per_cn["RecurrenceScore"].mean())
406
320
  mean_oc = float(real_df["OverlapCoefficient"].mean())
407
- score = float(
408
- significant_fraction**2 * significant_cn * mean_rs
409
- )
410
- result = {
411
- "NumCN": int(num_cn),
412
- "SignificantCN": significant_cn,
413
- "SignificantFraction": significant_fraction,
414
- "MeanRS": mean_rs,
415
- "MeanOC": mean_oc,
416
- "Score": score,
321
+ cri = float(significant_fraction**2 * mean_rs)
322
+ result = {
323
+ "NumCN": int(num_cn),
324
+ "Ksig": significant_cn,
325
+ "SignificantFraction": significant_fraction,
326
+ "MeanOC": mean_oc,
327
+ "MeanRS": mean_rs,
328
+ "CRI": cri,
417
329
  }
418
330
  pd.DataFrame([result]).to_csv(output_dir / "CNNumScore.csv", index=False)
419
- return result, per_cn.copy()
420
-
421
-
422
- def select_cn_num(results):
423
- """Select maximum score, using smaller K as the deterministic tie-break."""
424
- if not results:
425
- raise ValueError("No CN-number results were supplied")
426
- return min(results, key=lambda row: (-row["Score"], row["NumCN"]))
331
+ return result, per_cn
332
+
333
+
334
+ def evaluate_selected_cn(result, config):
335
+ """Evaluate the selected model using the configured project layout."""
336
+ paths = build_paths(config)
337
+ cn_labels_by_sample = _load_cn_labels(
338
+ result.dataset.sample_names, result.hard_assignment_dir
339
+ )
340
+ metrics, recurrence = evaluate_cn_num(
341
+ num_cn=result.selected_cn,
342
+ dataset=result.input_dir,
343
+ cn_labels_by_sample=cn_labels_by_sample,
344
+ celltype_file=result.unique_celltypes_file,
345
+ output_dir=paths.downstream_data,
346
+ enrichment_p_threshold=config.enrichment.p_value_threshold,
347
+ minimum_adjusted_p=config.enrichment.minimum_adjusted_p,
348
+ recurrence_p_threshold=config.recurrence.recurrence_p_threshold,
349
+ n_random=config.recurrence.null_draws,
350
+ seed=config.random_seed,
351
+ )
352
+ export_enrichment_long(
353
+ dataset=result.input_dir,
354
+ enrichment_dir=paths.enrichment,
355
+ output_file=paths.communication_config / "EnrichScoreMatrix_long.csv",
356
+ )
357
+ return metrics, recurrence
358
+
359
+
360
+ def select_cn_num(results):
361
+ """Select maximum CRI, using smaller K as the deterministic tie-break."""
362
+ if not results:
363
+ raise ValueError("No CN-number results were supplied")
364
+ return min(results, key=lambda row: (-row["CRI"], row["NumCN"]))
@@ -212,14 +212,19 @@ def train_one_run(
212
212
  _, _, _, assignment_logits, _ = model(
213
213
  data.x, data.edge_index, data.batch
214
214
  )
215
- if collect_hard_assignments:
216
- hard_assignments[index] = (
217
- torch.argmax(assignment_logits, dim=-1).cpu().numpy() + 1
218
- )
219
- if save_soft_assignments:
220
- np.savetxt(
221
- output_folder / f"ClusterAssignMatrix1_{index}.csv",
222
- torch.softmax(assignment_logits, dim=-1).cpu().numpy(),
223
- delimiter=",",
224
- )
215
+ if save_soft_assignments:
216
+ soft_assignments = torch.softmax(
217
+ assignment_logits, dim=-1
218
+ ).cpu().numpy()
219
+ np.savetxt(
220
+ output_folder / f"ClusterAssignMatrix1_{index}.csv",
221
+ soft_assignments,
222
+ delimiter=",",
223
+ )
224
+ if collect_hard_assignments:
225
+ hard_assignments[index] = (
226
+ np.argmax(soft_assignments, axis=-1) + 1
227
+ if save_soft_assignments
228
+ else torch.argmax(assignment_logits, dim=-1).cpu().numpy() + 1
229
+ )
225
230
  return summary, hard_assignments
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cytocommunity2
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Weakly supervised cellular-neighborhood learning for spatial omics
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -10,7 +10,7 @@ Requires-Dist: numpy<2,>=1.23
10
10
  Requires-Dist: pandas<3,>=1.5
11
11
  Requires-Dist: sci-palettes
12
12
  Requires-Dist: scikit-learn>=1.1
13
- Requires-Dist: scipy>=1.9
13
+ Requires-Dist: scipy>=1.11
14
14
  Requires-Dist: seaborn>=0.12
15
15
  Requires-Dist: torch-geometric>=2.0
16
16
  Provides-Extra: downstream
@@ -81,7 +81,6 @@ conda create -n cytocommunity2 python=3.10
81
81
  conda activate cytocommunity2
82
82
  ```
83
83
 
84
- Install PyTorch for your compute platform before installing CytoCommunity2.
85
84
  For an NVIDIA GPU on Windows or Linux, first install the appropriate
86
85
  CUDA-enabled PyTorch build from the
87
86
  [official PyTorch installation page](https://pytorch.org/get-started/locally/).
@@ -3,7 +3,7 @@ numpy<2,>=1.23
3
3
  pandas<3,>=1.5
4
4
  sci-palettes
5
5
  scikit-learn>=1.1
6
- scipy>=1.9
6
+ scipy>=1.11
7
7
  seaborn>=0.12
8
8
  torch-geometric>=2.0
9
9
 
File without changes
File without changes