cytocommunity2 0.1.3__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.
- {cytocommunity2-0.1.3/src/cytocommunity2.egg-info → cytocommunity2-0.1.4}/PKG-INFO +2 -2
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/pyproject.toml +2 -2
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/__init__.py +9 -9
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/config.py +7 -6
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/recurrence.py +1 -1
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/api.py +60 -40
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/selection.py +145 -232
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/training.py +15 -10
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4/src/cytocommunity2.egg-info}/PKG-INFO +2 -2
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/requires.txt +1 -1
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/LICENSE +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/README.md +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/setup.cfg +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/colors.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/__init__.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/coherence.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/between.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/cca.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/within.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/composition.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/moran.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/coherence.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/communication.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/composition.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dominant.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dotplots.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/runtime.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/ensemble.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/dataset.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/learning/model.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/paths.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/visualization/spatial.py +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/SOURCES.txt +0 -0
- {cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-0.1.3 → 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.
|
|
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.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cytocommunity2"
|
|
7
|
-
version = "0.1.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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()
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/recurrence.py
RENAMED
|
@@ -62,7 +62,7 @@ def plot_recurrence_results(config):
|
|
|
62
62
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
63
63
|
real = pd.read_csv(overlap_dir / "RealDistribution_sameCN.csv")
|
|
64
64
|
background = pd.read_csv(overlap_dir / "BackgroundDistribution_randomCN.csv")
|
|
65
|
-
tests = pd.read_csv(overlap_dir / "
|
|
65
|
+
tests = pd.read_csv(overlap_dir / "KSTest_Results.csv")
|
|
66
66
|
real["CN"] = real["CN"].astype(str).str.strip()
|
|
67
67
|
real["OverlapCoefficient"] = pd.to_numeric(real["OverlapCoefficient"], errors="coerce")
|
|
68
68
|
background["OverlapCoefficient"] = pd.to_numeric(
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import datetime
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
import shutil
|
|
7
|
-
|
|
8
|
-
import
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
9
10
|
|
|
10
11
|
from ..config import CytoCommunityConfig
|
|
11
12
|
from ..ensemble import ensemble_cn
|
|
@@ -133,7 +134,11 @@ def run_learning(
|
|
|
133
134
|
collect_hard_assignments=True,
|
|
134
135
|
)
|
|
135
136
|
|
|
136
|
-
|
|
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(
|
|
137
142
|
num_cn=num_cn,
|
|
138
143
|
dataset=input_dir,
|
|
139
144
|
celltype_file=unique_celltypes_file,
|
|
@@ -143,30 +148,35 @@ def run_learning(
|
|
|
143
148
|
recurrence_p_threshold=config.recurrence.recurrence_p_threshold,
|
|
144
149
|
n_random=config.recurrence.null_draws,
|
|
145
150
|
seed=config.random_seed,
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
del hard_assignments
|
|
151
|
+
cn_labels_by_sample=cn_labels_by_sample,
|
|
152
|
+
)
|
|
153
|
+
del hard_assignments, cn_labels_by_sample
|
|
149
154
|
selection_results.append(result)
|
|
150
|
-
cn_pvalues = cn_pvalues[
|
|
151
|
-
[
|
|
152
|
-
"CN",
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
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",
|
|
156
168
|
"SignificantRecurrence",
|
|
157
169
|
]
|
|
158
170
|
].copy()
|
|
159
171
|
cn_pvalues.insert(0, "NumCN", num_cn)
|
|
160
|
-
cn_pvalue_tables.append(cn_pvalues)
|
|
161
|
-
print(
|
|
162
|
-
f"K={num_cn}:
|
|
163
|
-
f"significantFraction={result['SignificantFraction']:.6f}, "
|
|
164
|
-
f"
|
|
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}"
|
|
165
177
|
)
|
|
166
178
|
|
|
167
|
-
pd.DataFrame(selection_results).
|
|
168
|
-
selection_summary_file, index=False
|
|
169
|
-
)
|
|
179
|
+
pd.DataFrame(selection_results).to_csv(selection_summary_file, index=False)
|
|
170
180
|
pd.concat(cn_pvalue_tables, ignore_index=True).to_csv(
|
|
171
181
|
cn_pvalues_file, index=False
|
|
172
182
|
)
|
|
@@ -175,9 +185,9 @@ def run_learning(
|
|
|
175
185
|
(output_dir / "SelectedCNNum.txt").write_text(
|
|
176
186
|
f"{selected_cn}\n", encoding="utf-8"
|
|
177
187
|
)
|
|
178
|
-
print(
|
|
179
|
-
f"\nSelected K={selected_cn} with
|
|
180
|
-
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}."
|
|
181
191
|
)
|
|
182
192
|
|
|
183
193
|
selected_config = _training_config(
|
|
@@ -185,22 +195,32 @@ def run_learning(
|
|
|
185
195
|
)
|
|
186
196
|
for run_index in range(1, config.main.final_num_runs + 1):
|
|
187
197
|
print(f"\n=== Selected K={selected_cn}: Run {run_index} ===")
|
|
188
|
-
train_one_run(
|
|
189
|
-
dataset,
|
|
190
|
-
selected_config,
|
|
191
|
-
run_index=run_index,
|
|
192
|
-
output_folder=selected_model_dir / f"Run{run_index}",
|
|
193
|
-
dynamic_pseudo=dataset.dynamic_pseudo,
|
|
194
|
-
base_seed=config.random_seed,
|
|
195
|
-
save_soft_assignments=True,
|
|
196
|
-
collect_hard_assignments=
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
)
|
|
204
224
|
return LearningResult(
|
|
205
225
|
selected_cn=selected_cn,
|
|
206
226
|
dataset=dataset,
|
|
@@ -5,15 +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,
|
|
8
|
+
from scipy.stats import false_discovery_control, hypergeom, ks_2samp
|
|
9
9
|
|
|
10
10
|
from ..paths import build_paths
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def read_nonempty_lines(path):
|
|
13
|
+
def read_nonempty_lines(path):
|
|
17
14
|
values = [
|
|
18
15
|
line.strip()
|
|
19
16
|
for line in Path(path).read_text(encoding="utf-8-sig").splitlines()
|
|
@@ -24,42 +21,14 @@ def read_nonempty_lines(path):
|
|
|
24
21
|
return values
|
|
25
22
|
|
|
26
23
|
|
|
27
|
-
def
|
|
28
|
-
pvalues = np.asarray(pvalues, dtype=np.float64)
|
|
29
|
-
flat = pvalues.ravel(order="F")
|
|
30
|
-
order = np.argsort(flat, kind="stable")
|
|
31
|
-
sorted_p = flat[order]
|
|
32
|
-
weighted = sorted_p.size * sorted_p / np.arange(1, sorted_p.size + 1)
|
|
33
|
-
adjusted_sorted = np.minimum.accumulate(weighted[::-1])[::-1]
|
|
34
|
-
adjusted = np.empty_like(flat)
|
|
35
|
-
adjusted[order] = adjusted_sorted
|
|
36
|
-
return adjusted.reshape(pvalues.shape, order="F")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def bh_adjust(pvalues):
|
|
40
|
-
"""Benjamini-Hochberg adjustment for one family of CN hypotheses."""
|
|
41
|
-
pvalues = np.asarray(pvalues, dtype=np.float64)
|
|
42
|
-
clean_pvalues = np.where(np.isfinite(pvalues), pvalues, 1.0)
|
|
43
|
-
clean_pvalues = np.clip(clean_pvalues, 0.0, 1.0)
|
|
44
|
-
order = np.argsort(clean_pvalues, kind="stable")
|
|
45
|
-
sorted_pvalues = clean_pvalues[order]
|
|
46
|
-
ranks = np.arange(1, sorted_pvalues.size + 1, dtype=np.float64)
|
|
47
|
-
adjusted_sorted = np.minimum.accumulate(
|
|
48
|
-
(sorted_pvalues.size * sorted_pvalues / ranks)[::-1]
|
|
49
|
-
)[::-1]
|
|
50
|
-
adjusted = np.empty_like(clean_pvalues)
|
|
51
|
-
adjusted[order] = np.clip(adjusted_sorted, 0.0, 1.0)
|
|
52
|
-
return adjusted
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def calculate_enrichment_matrix(
|
|
24
|
+
def calculate_enrichment_matrix(
|
|
56
25
|
cell_labels,
|
|
57
26
|
cn_labels,
|
|
58
27
|
cell_types,
|
|
59
28
|
num_cn,
|
|
60
29
|
minimum_adjusted_p,
|
|
61
30
|
):
|
|
62
|
-
"""
|
|
31
|
+
"""Calculate BH-adjusted cell-type enrichment scores for one sample."""
|
|
63
32
|
cell_labels = np.asarray(cell_labels, dtype=str)
|
|
64
33
|
cn_labels = np.asarray(cn_labels, dtype=np.int64)
|
|
65
34
|
if cell_labels.ndim != 1 or cn_labels.ndim != 1:
|
|
@@ -98,70 +67,47 @@ def calculate_enrichment_matrix(
|
|
|
98
67
|
int(cell_type_totals[cell_type_index]),
|
|
99
68
|
int(cn_totals[cn_index]),
|
|
100
69
|
)
|
|
101
|
-
raw_pvalues[cell_type_index, cn_index] =
|
|
70
|
+
raw_pvalues[cell_type_index, cn_index] = upper_tail
|
|
102
71
|
|
|
103
|
-
adjusted =
|
|
72
|
+
adjusted = false_discovery_control(
|
|
73
|
+
raw_pvalues.ravel(order="F"), method="bh"
|
|
74
|
+
).reshape(raw_pvalues.shape, order="F")
|
|
104
75
|
adjusted[adjusted < minimum_adjusted_p] = minimum_adjusted_p
|
|
105
76
|
return -np.log10(adjusted)
|
|
106
77
|
|
|
107
78
|
|
|
108
|
-
def calculate_enrichment(
|
|
109
|
-
dataset,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
output_dir / "CellTypeVec_List.csv", index=False
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
rows = []
|
|
133
|
-
for sample_index, sample in enumerate(samples):
|
|
134
|
-
cell_labels = (
|
|
135
|
-
pd.read_csv(
|
|
136
|
-
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",
|
|
137
100
|
sep="\t",
|
|
138
101
|
header=None,
|
|
139
102
|
dtype=str,
|
|
140
103
|
)
|
|
141
|
-
.iloc[:, 0]
|
|
142
|
-
.to_numpy()
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
cn_labels = np.asarray(
|
|
150
|
-
hard_assignments[sample_index], dtype=np.int64
|
|
151
|
-
)
|
|
152
|
-
else:
|
|
153
|
-
cn_labels = (
|
|
154
|
-
pd.read_csv(
|
|
155
|
-
cn_label_dir / f"{sample}_CNLabel_MajorityVoting.csv",
|
|
156
|
-
header=None,
|
|
157
|
-
dtype=np.int64,
|
|
158
|
-
)
|
|
159
|
-
.iloc[:, 0]
|
|
160
|
-
.to_numpy()
|
|
161
|
-
)
|
|
162
|
-
matrix = calculate_enrichment_matrix(
|
|
163
|
-
cell_labels,
|
|
164
|
-
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,
|
|
165
111
|
cell_types,
|
|
166
112
|
num_cn,
|
|
167
113
|
minimum_adjusted_p,
|
|
@@ -169,11 +115,25 @@ def calculate_enrichment(
|
|
|
169
115
|
np.savetxt(
|
|
170
116
|
output_dir / f"{sample}_EnrichScoreMatrix.csv",
|
|
171
117
|
matrix,
|
|
172
|
-
delimiter=",",
|
|
173
|
-
fmt="%.15g",
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
return
|
|
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
|
+
}
|
|
177
137
|
|
|
178
138
|
|
|
179
139
|
def export_enrichment_long(dataset, enrichment_dir, output_file):
|
|
@@ -223,82 +183,51 @@ def overlap_coefficient(first, second):
|
|
|
223
183
|
return len(first & second) / smaller_size if smaller_size else 0.0
|
|
224
184
|
|
|
225
185
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
for cn in range(matrix.shape[1])
|
|
248
|
-
]
|
|
249
|
-
return dominant_sets, num_cn
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _summarize_ttest(cn, real_values, background_values):
|
|
253
|
-
real_values = np.asarray(real_values, dtype=float)
|
|
254
|
-
background_values = np.asarray(background_values, dtype=float)
|
|
255
|
-
if len(real_values) >= 2 and len(background_values) >= 2:
|
|
256
|
-
result = ttest_ind(
|
|
257
|
-
real_values,
|
|
258
|
-
background_values,
|
|
259
|
-
equal_var=False,
|
|
260
|
-
alternative="greater",
|
|
261
|
-
)
|
|
262
|
-
t_statistic, p_value = float(result.statistic), float(result.pvalue)
|
|
263
|
-
else:
|
|
264
|
-
t_statistic, p_value = np.nan, np.nan
|
|
265
|
-
real_mean = np.mean(real_values) if len(real_values) else np.nan
|
|
266
|
-
background_mean = (
|
|
267
|
-
np.mean(background_values) if len(background_values) else np.nan
|
|
268
|
-
)
|
|
269
|
-
return {
|
|
270
|
-
"CN": cn,
|
|
271
|
-
"n_real": len(real_values),
|
|
272
|
-
"n_background": len(background_values),
|
|
273
|
-
"real_mean": real_mean,
|
|
274
|
-
"real_std": np.std(real_values, ddof=1) if len(real_values) > 1 else np.nan,
|
|
275
|
-
"background_mean": background_mean,
|
|
276
|
-
"background_std": (
|
|
277
|
-
np.std(background_values, ddof=1)
|
|
278
|
-
if len(background_values) > 1
|
|
279
|
-
else np.nan
|
|
280
|
-
),
|
|
281
|
-
"delta_mean": real_mean - background_mean,
|
|
282
|
-
"t_statistic": t_statistic,
|
|
283
|
-
"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),
|
|
284
207
|
}
|
|
285
208
|
|
|
286
209
|
|
|
287
|
-
def calculate_recurrence(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
output_dir,
|
|
210
|
+
def calculate_recurrence(
|
|
211
|
+
enrichment_matrices,
|
|
212
|
+
cell_types,
|
|
213
|
+
output_dir,
|
|
291
214
|
score_threshold,
|
|
292
215
|
recurrence_p_threshold,
|
|
293
|
-
n_random,
|
|
294
|
-
seed,
|
|
295
|
-
):
|
|
296
|
-
samples =
|
|
297
|
-
if len(samples) < 2:
|
|
298
|
-
raise ValueError("At least two samples are required for recurrence analysis")
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
231
|
sample_pairs = list(combinations(samples, 2))
|
|
303
232
|
real_df = pd.DataFrame(
|
|
304
233
|
[
|
|
@@ -324,35 +253,20 @@ def calculate_recurrence(
|
|
|
324
253
|
)
|
|
325
254
|
background_df = pd.DataFrame({"OverlapCoefficient": background_values})
|
|
326
255
|
|
|
327
|
-
|
|
256
|
+
ks_rows = []
|
|
328
257
|
for cn in [f"CN{index + 1}" for index in range(num_cn)]:
|
|
329
258
|
values = real_df.loc[
|
|
330
259
|
real_df["CN"] == cn, "OverlapCoefficient"
|
|
331
260
|
].to_numpy()
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
ttest_df.loc[cn_mask, "p_value_one_sided"].to_numpy()
|
|
342
|
-
)
|
|
343
|
-
ttest_df["p_adjusted_bh"] = np.nan
|
|
344
|
-
ttest_df["RecurrenceScore"] = np.nan
|
|
345
|
-
ttest_df["SignificantRecurrence"] = False
|
|
346
|
-
ttest_df.loc[cn_mask, "p_adjusted_bh"] = adjusted_pvalues
|
|
347
|
-
ttest_df.loc[cn_mask, "RecurrenceScore"] = -np.log10(
|
|
348
|
-
np.maximum(adjusted_pvalues, MIN_FLOAT_P)
|
|
349
|
-
)
|
|
350
|
-
ttest_df.loc[cn_mask, "SignificantRecurrence"] = (
|
|
351
|
-
(
|
|
352
|
-
ttest_df.loc[cn_mask, "p_adjusted_bh"]
|
|
353
|
-
< recurrence_p_threshold
|
|
354
|
-
)
|
|
355
|
-
& (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)
|
|
356
270
|
)
|
|
357
271
|
|
|
358
272
|
output_dir = Path(output_dir)
|
|
@@ -361,73 +275,72 @@ def calculate_recurrence(
|
|
|
361
275
|
background_df.to_csv(
|
|
362
276
|
output_dir / "BackgroundDistribution_randomCN.csv", index=False
|
|
363
277
|
)
|
|
364
|
-
|
|
365
|
-
return real_df, background_df,
|
|
278
|
+
ks_df.to_csv(output_dir / "KSTest_Results.csv", index=False)
|
|
279
|
+
return real_df, background_df, ks_df
|
|
366
280
|
|
|
367
281
|
|
|
368
|
-
def evaluate_cn_num(
|
|
282
|
+
def evaluate_cn_num(
|
|
369
283
|
num_cn,
|
|
370
284
|
dataset,
|
|
371
285
|
celltype_file,
|
|
372
286
|
output_dir,
|
|
373
287
|
enrichment_p_threshold,
|
|
374
288
|
minimum_adjusted_p,
|
|
375
|
-
recurrence_p_threshold,
|
|
376
|
-
n_random,
|
|
377
|
-
seed,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
):
|
|
289
|
+
recurrence_p_threshold,
|
|
290
|
+
n_random,
|
|
291
|
+
seed,
|
|
292
|
+
cn_labels_by_sample,
|
|
293
|
+
):
|
|
381
294
|
"""Calculate the recurrence-weighted CN resolution index and per-CN tests."""
|
|
382
295
|
output_dir = Path(output_dir)
|
|
383
296
|
enrichment_dir = output_dir / "EnrichScoreMatrix"
|
|
384
297
|
overlap_dir = output_dir / "OverlapCoefficient"
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
)
|
|
394
|
-
real_df, _,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
overlap_dir,
|
|
398
|
-
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),
|
|
399
312
|
recurrence_p_threshold=recurrence_p_threshold,
|
|
400
313
|
n_random=n_random,
|
|
401
314
|
seed=seed,
|
|
402
315
|
)
|
|
403
|
-
|
|
404
|
-
significant_mask = per_cn["SignificantRecurrence"].astype(bool)
|
|
316
|
+
significant_mask = per_cn["SignificantRecurrence"].astype(bool)
|
|
405
317
|
significant_cn = int(significant_mask.sum())
|
|
406
318
|
significant_fraction = significant_cn / num_cn
|
|
407
319
|
mean_rs = float(per_cn["RecurrenceScore"].mean())
|
|
408
320
|
mean_oc = float(real_df["OverlapCoefficient"].mean())
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"MeanOC": mean_oc,
|
|
418
|
-
"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,
|
|
419
329
|
}
|
|
420
330
|
pd.DataFrame([result]).to_csv(output_dir / "CNNumScore.csv", index=False)
|
|
421
|
-
return result, per_cn
|
|
331
|
+
return result, per_cn
|
|
422
332
|
|
|
423
333
|
|
|
424
334
|
def evaluate_selected_cn(result, config):
|
|
425
335
|
"""Evaluate the selected model using the configured project layout."""
|
|
426
336
|
paths = build_paths(config)
|
|
337
|
+
cn_labels_by_sample = _load_cn_labels(
|
|
338
|
+
result.dataset.sample_names, result.hard_assignment_dir
|
|
339
|
+
)
|
|
427
340
|
metrics, recurrence = evaluate_cn_num(
|
|
428
341
|
num_cn=result.selected_cn,
|
|
429
342
|
dataset=result.input_dir,
|
|
430
|
-
|
|
343
|
+
cn_labels_by_sample=cn_labels_by_sample,
|
|
431
344
|
celltype_file=result.unique_celltypes_file,
|
|
432
345
|
output_dir=paths.downstream_data,
|
|
433
346
|
enrichment_p_threshold=config.enrichment.p_value_threshold,
|
|
@@ -445,7 +358,7 @@ def evaluate_selected_cn(result, config):
|
|
|
445
358
|
|
|
446
359
|
|
|
447
360
|
def select_cn_num(results):
|
|
448
|
-
"""Select maximum
|
|
449
|
-
if not results:
|
|
450
|
-
raise ValueError("No CN-number results were supplied")
|
|
451
|
-
return min(results, key=lambda row: (-row["
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/between.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/cca.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/utils.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/communication/within.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/__init__.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/cn_selection.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/coherence.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/common.py
RENAMED
|
File without changes
|
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/composition.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dominant.py
RENAMED
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2/downstream/plotting/dotplots.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cytocommunity2-0.1.3 → cytocommunity2-0.1.4}/src/cytocommunity2.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|