cytocommunity2 1.0.0__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cytocommunity2-1.0.0/src/cytocommunity2.egg-info → cytocommunity2-1.0.2}/PKG-INFO +19 -3
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/README.md +18 -2
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/pyproject.toml +1 -1
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/__init__.py +1 -1
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/config.py +1 -1
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/differential_composition.py +30 -11
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/dominant_celltype.py +19 -2
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/moran.py +16 -19
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/dominant.py +31 -5
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/dotplots.py +83 -15
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/selection.py +39 -12
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2/src/cytocommunity2.egg-info}/PKG-INFO +19 -3
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/LICENSE +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/setup.cfg +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/colors.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/__init__.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/coherence.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/between.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/cca.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/utils.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/within.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/cn_selection.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/coherence.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/common.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/communication.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/composition.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/recurrence.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/runtime.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/ensemble.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/__init__.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/api.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/dataset.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/model.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/learning/training.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/paths.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/visualization/__init__.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/visualization/spatial.py +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2.egg-info/SOURCES.txt +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2.egg-info/requires.txt +0 -0
- {cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/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: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Weakly supervised cellular-neighborhood learning for spatial omics
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -120,10 +120,26 @@ pseudo-samples for weakly-supervised learning.
|
|
|
120
120
|
|
|
121
121
|
Before running CytoCommunity2, set `input_dir` and review all
|
|
122
122
|
model and downstream parameters in
|
|
123
|
-
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/hyperparameters.json). The Python workflow will use this configuration file.
|
|
123
|
+
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/tutorial/hyperparameters.json). The Python workflow will use this configuration file.
|
|
124
124
|
|
|
125
125
|
### 3. Run CytoCommunity2
|
|
126
126
|
|
|
127
|
+
With `hyperparameters.json` in the working directory, run the core workflow:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pathlib import Path
|
|
131
|
+
import cytocommunity2 as cc2
|
|
132
|
+
|
|
133
|
+
CONFIG_FILE = Path("hyperparameters.json").resolve()
|
|
134
|
+
|
|
135
|
+
config = cc2.CytoCommunityConfig.from_json(CONFIG_FILE)
|
|
136
|
+
dataset = cc2.build_spatial_graphs(config)
|
|
137
|
+
learning_result = cc2.run_learning(dataset, config)
|
|
138
|
+
cc2.visualize_results(learning_result, config)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 4.Tutorial
|
|
142
|
+
|
|
127
143
|
The tutorial uses a TNBC dataset as a worked example and demonstrates the
|
|
128
144
|
entire CytoCommunity2 workflow, from model training and CN identification to
|
|
129
145
|
downstream analysis:
|
|
@@ -132,7 +148,7 @@ downstream analysis:
|
|
|
132
148
|
jupyter notebook tutorial/TNBC_workflow.ipynb
|
|
133
149
|
```
|
|
134
150
|
|
|
135
|
-
###
|
|
151
|
+
### 5. Output files
|
|
136
152
|
|
|
137
153
|
The main output directories are:
|
|
138
154
|
|
|
@@ -100,10 +100,26 @@ pseudo-samples for weakly-supervised learning.
|
|
|
100
100
|
|
|
101
101
|
Before running CytoCommunity2, set `input_dir` and review all
|
|
102
102
|
model and downstream parameters in
|
|
103
|
-
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/hyperparameters.json). The Python workflow will use this configuration file.
|
|
103
|
+
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/tutorial/hyperparameters.json). The Python workflow will use this configuration file.
|
|
104
104
|
|
|
105
105
|
### 3. Run CytoCommunity2
|
|
106
106
|
|
|
107
|
+
With `hyperparameters.json` in the working directory, run the core workflow:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from pathlib import Path
|
|
111
|
+
import cytocommunity2 as cc2
|
|
112
|
+
|
|
113
|
+
CONFIG_FILE = Path("hyperparameters.json").resolve()
|
|
114
|
+
|
|
115
|
+
config = cc2.CytoCommunityConfig.from_json(CONFIG_FILE)
|
|
116
|
+
dataset = cc2.build_spatial_graphs(config)
|
|
117
|
+
learning_result = cc2.run_learning(dataset, config)
|
|
118
|
+
cc2.visualize_results(learning_result, config)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4.Tutorial
|
|
122
|
+
|
|
107
123
|
The tutorial uses a TNBC dataset as a worked example and demonstrates the
|
|
108
124
|
entire CytoCommunity2 workflow, from model training and CN identification to
|
|
109
125
|
downstream analysis:
|
|
@@ -112,7 +128,7 @@ downstream analysis:
|
|
|
112
128
|
jupyter notebook tutorial/TNBC_workflow.ipynb
|
|
113
129
|
```
|
|
114
130
|
|
|
115
|
-
###
|
|
131
|
+
### 5. Output files
|
|
116
132
|
|
|
117
133
|
The main output directories are:
|
|
118
134
|
|
|
@@ -13,7 +13,7 @@ DEFAULT_CONFIG_FILE = "hyperparameters.json"
|
|
|
13
13
|
# belong in hyperparameters.json.
|
|
14
14
|
FIXED_DOWNSTREAM = {
|
|
15
15
|
"dominant_cell_type": {"frequency_threshold": 0.5},
|
|
16
|
-
"differential_composed": {"p_value_threshold": 0.05},
|
|
16
|
+
"differential_composed": {"p_value_threshold": 0.05, "minimum_samples": 3},
|
|
17
17
|
"moran": {
|
|
18
18
|
"minimum_positive_count": 2,
|
|
19
19
|
"minimum_cn_cells": 10,
|
|
@@ -66,11 +66,13 @@ def build_label_comparisons(labels, mode):
|
|
|
66
66
|
raise ValueError(f"Unknown comparison mode: {mode}")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def composition_wilcoxon_tests(
|
|
69
|
+
def composition_wilcoxon_tests(
|
|
70
|
+
cell_type, target_values, reference_values, *, minimum_samples=3
|
|
71
|
+
):
|
|
70
72
|
"""Run one-sided asymptotic Wilcoxon rank-sum tests."""
|
|
71
73
|
target = np.asarray(target_values, dtype=float)
|
|
72
74
|
reference = np.asarray(reference_values, dtype=float)
|
|
73
|
-
if len(target) <
|
|
75
|
+
if len(target) < minimum_samples or len(reference) < minimum_samples:
|
|
74
76
|
return None
|
|
75
77
|
if not np.isfinite(target).all() or not np.isfinite(reference).all():
|
|
76
78
|
return None
|
|
@@ -100,10 +102,12 @@ def composition_wilcoxon_tests(cell_type, target_values, reference_values):
|
|
|
100
102
|
return rows
|
|
101
103
|
|
|
102
104
|
|
|
103
|
-
def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
105
|
+
def analyze_differential_composition(matrix_records, celltypes, *, mode, minimum_samples=3):
|
|
104
106
|
"""Calculate all directional composition comparisons in memory.
|
|
105
107
|
|
|
106
108
|
The result maps comparison names to maps of CN names and result tables.
|
|
109
|
+
Comparisons with fewer than ``minimum_samples`` samples on either side
|
|
110
|
+
have empty maps.
|
|
107
111
|
"""
|
|
108
112
|
normalized_records = [
|
|
109
113
|
{**record, "matrix": np.asarray(record["matrix"], dtype=float)}
|
|
@@ -114,8 +118,20 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
114
118
|
labels = sorted({record["condition"] for record in normalized_records})
|
|
115
119
|
comparisons = build_label_comparisons(labels, mode)
|
|
116
120
|
outputs = {comparison["name"]: {} for comparison in comparisons}
|
|
121
|
+
eligible = [
|
|
122
|
+
comparison
|
|
123
|
+
for comparison in comparisons
|
|
124
|
+
if sum(
|
|
125
|
+
record["condition"] in comparison["target_labels"]
|
|
126
|
+
for record in normalized_records
|
|
127
|
+
) >= minimum_samples
|
|
128
|
+
and sum(
|
|
129
|
+
record["condition"] in comparison["reference_labels"]
|
|
130
|
+
for record in normalized_records
|
|
131
|
+
) >= minimum_samples
|
|
132
|
+
]
|
|
117
133
|
for cn_index in range(expected_shape[1]):
|
|
118
|
-
for comparison in
|
|
134
|
+
for comparison in eligible:
|
|
119
135
|
rows = []
|
|
120
136
|
for cell_index, cell_type in enumerate(celltypes):
|
|
121
137
|
target = [
|
|
@@ -129,7 +145,7 @@ def analyze_differential_composition(matrix_records, celltypes, *, mode):
|
|
|
129
145
|
if record["condition"] in comparison["reference_labels"]
|
|
130
146
|
]
|
|
131
147
|
result = composition_wilcoxon_tests(
|
|
132
|
-
cell_type, target, reference
|
|
148
|
+
cell_type, target, reference, minimum_samples=minimum_samples
|
|
133
149
|
)
|
|
134
150
|
if result is not None:
|
|
135
151
|
rows.extend(result)
|
|
@@ -147,16 +163,19 @@ def run_differential_composition_analysis(
|
|
|
147
163
|
paths.enrichment, paths.input_dir
|
|
148
164
|
)
|
|
149
165
|
outputs = analyze_differential_composition(
|
|
150
|
-
records, celltypes, mode=config.downstream["ttest_mode"]
|
|
166
|
+
records, celltypes, mode=config.downstream["ttest_mode"],
|
|
167
|
+
minimum_samples=config.downstream["differential_composed"]["minimum_samples"],
|
|
151
168
|
)
|
|
152
169
|
output_dir = Path(output_dir or paths.downstream_data / "DifferentialComposed")
|
|
153
170
|
for comparison, cn_tables in outputs.items():
|
|
154
171
|
comparison_dir = output_dir / comparison
|
|
155
|
-
comparison_dir.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
if comparison_dir.exists():
|
|
173
|
+
for old_file in comparison_dir.glob("CN*.csv"):
|
|
174
|
+
old_file.unlink()
|
|
175
|
+
if cn_tables:
|
|
176
|
+
comparison_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
for cn, table in cn_tables.items():
|
|
178
|
+
table.to_csv(comparison_dir / f"{cn}_{comparison}.csv", index=False)
|
|
160
179
|
return outputs
|
|
161
180
|
|
|
162
181
|
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/dominant_celltype.py
RENAMED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
|
+
from scipy.cluster.hierarchy import leaves_list, linkage
|
|
8
9
|
|
|
9
10
|
from ..config import CytoCommunityConfig
|
|
10
11
|
from ..paths import build_paths
|
|
@@ -16,6 +17,8 @@ class DominantCellTypeResult:
|
|
|
16
17
|
|
|
17
18
|
frequency: pd.DataFrame
|
|
18
19
|
dominant_cell_types: pd.DataFrame
|
|
20
|
+
cn_order: tuple[str, ...] = ()
|
|
21
|
+
cn_linkage: np.ndarray | None = None
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def load_enrichment_matrices(enrichment_dir, input_dir):
|
|
@@ -46,12 +49,26 @@ def analyze_dominant_celltypes(
|
|
|
46
49
|
score_threshold,
|
|
47
50
|
frequency_threshold,
|
|
48
51
|
):
|
|
49
|
-
"""Compute dominant cell types
|
|
52
|
+
"""Compute dominant cell types and cluster CNs by enrichment frequency."""
|
|
50
53
|
matrices = np.asarray(enrichment_matrices, dtype=float)
|
|
51
54
|
|
|
52
55
|
frequencies = (matrices > score_threshold).mean(axis=0)
|
|
53
56
|
cn_names = [f"CN{index}" for index in range(1, matrices.shape[2] + 1)]
|
|
54
57
|
frequency = pd.DataFrame(frequencies, index=celltypes, columns=cn_names)
|
|
58
|
+
# Each CN is represented by its across-sample enrichment frequency for
|
|
59
|
+
# every cell type, the same values displayed in the heatmap.
|
|
60
|
+
cn_linkage = None
|
|
61
|
+
if len(cn_names) > 1:
|
|
62
|
+
cn_linkage = linkage(
|
|
63
|
+
frequency.to_numpy().T,
|
|
64
|
+
method="ward",
|
|
65
|
+
metric="euclidean",
|
|
66
|
+
optimal_ordering=True,
|
|
67
|
+
)
|
|
68
|
+
cn_order = (
|
|
69
|
+
tuple(cn_names[index] for index in leaves_list(cn_linkage))
|
|
70
|
+
if cn_linkage is not None else tuple(cn_names)
|
|
71
|
+
)
|
|
55
72
|
dominant = pd.DataFrame(
|
|
56
73
|
{
|
|
57
74
|
"CN": cn_names,
|
|
@@ -63,7 +80,7 @@ def analyze_dominant_celltypes(
|
|
|
63
80
|
],
|
|
64
81
|
}
|
|
65
82
|
)
|
|
66
|
-
return DominantCellTypeResult(frequency, dominant)
|
|
83
|
+
return DominantCellTypeResult(frequency, dominant, cn_order, cn_linkage)
|
|
67
84
|
|
|
68
85
|
|
|
69
86
|
def run_dominant_celltype_analysis(config: CytoCommunityConfig, *, output_dir=None):
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Moran's I calculation and between-condition statistical comparisons."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
import warnings
|
|
5
4
|
|
|
6
5
|
from esda.moran import Moran
|
|
7
6
|
from libpysal.weights import DistanceBand
|
|
@@ -161,8 +160,8 @@ def build_comparisons(graph_labels, mode):
|
|
|
161
160
|
raise ValueError(f"Unknown comparison mode: {mode}")
|
|
162
161
|
|
|
163
162
|
|
|
164
|
-
def
|
|
165
|
-
"""Return both one-sided
|
|
163
|
+
def wilcoxon_one_sided_tests(target_values, reference_values):
|
|
164
|
+
"""Return both one-sided Wilcoxon rank-sum tests and descriptive statistics."""
|
|
166
165
|
target = np.asarray(target_values, dtype=float)
|
|
167
166
|
reference = np.asarray(reference_values, dtype=float)
|
|
168
167
|
target = target[np.isfinite(target)]
|
|
@@ -171,19 +170,16 @@ def welch_one_sided_tests(target_values, reference_values):
|
|
|
171
170
|
reference_mean = float(np.mean(reference)) if len(reference) else np.nan
|
|
172
171
|
p_values = {"greater": np.nan, "less": np.nan}
|
|
173
172
|
if len(target) >= 2 and len(reference) >= 2:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
alternative=alternative,
|
|
185
|
-
).pvalue
|
|
186
|
-
)
|
|
173
|
+
for alternative in p_values:
|
|
174
|
+
p_values[alternative] = float(
|
|
175
|
+
stats.mannwhitneyu(
|
|
176
|
+
target,
|
|
177
|
+
reference,
|
|
178
|
+
alternative=alternative,
|
|
179
|
+
method="asymptotic",
|
|
180
|
+
use_continuity=True,
|
|
181
|
+
).pvalue
|
|
182
|
+
)
|
|
187
183
|
return {
|
|
188
184
|
"target_n": len(target),
|
|
189
185
|
"reference_n": len(reference),
|
|
@@ -209,7 +205,7 @@ def test_comparison(frame, comparison):
|
|
|
209
205
|
reference = group.loc[
|
|
210
206
|
group["GraphLabel"] == comparison["reference"], "Global_Moran_I"
|
|
211
207
|
].to_numpy(dtype=float)
|
|
212
|
-
result =
|
|
208
|
+
result = wilcoxon_one_sided_tests(target, reference)
|
|
213
209
|
for alternative, p_value in result["p_values"].items():
|
|
214
210
|
rows.append(
|
|
215
211
|
{
|
|
@@ -224,6 +220,7 @@ def test_comparison(frame, comparison):
|
|
|
224
220
|
"ReferenceMeanMoran": result["reference_mean"],
|
|
225
221
|
"DeltaMeanMoran": result["delta"],
|
|
226
222
|
"p_value": p_value,
|
|
223
|
+
"Test": "Wilcoxon rank-sum asymptotic",
|
|
227
224
|
}
|
|
228
225
|
)
|
|
229
226
|
return pd.DataFrame(rows)
|
|
@@ -232,7 +229,7 @@ def test_comparison(frame, comparison):
|
|
|
232
229
|
def run_moran_comparisons(
|
|
233
230
|
config: CytoCommunityConfig, *, input_dir=None, output_dir=None
|
|
234
231
|
):
|
|
235
|
-
"""Run configured
|
|
232
|
+
"""Run configured Wilcoxon rank-sum comparisons over saved Moran tables."""
|
|
236
233
|
paths = build_paths(config)
|
|
237
234
|
input_dir = Path(input_dir or paths.downstream_data / "globalMoran")
|
|
238
235
|
output_dir = Path(output_dir or input_dir / "t_test")
|
|
@@ -261,5 +258,5 @@ __all__ = [
|
|
|
261
258
|
"run_moran_analysis",
|
|
262
259
|
"run_moran_comparisons",
|
|
263
260
|
"test_comparison",
|
|
264
|
-
"
|
|
261
|
+
"wilcoxon_one_sided_tests",
|
|
265
262
|
]
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/dominant.py
RENAMED
|
@@ -9,13 +9,25 @@ def _prepare(result):
|
|
|
9
9
|
frequency = result.frequency.copy()
|
|
10
10
|
frequency.columns = frequency.columns.astype(str).str.strip()
|
|
11
11
|
frequency.index = frequency.index.astype(str).str.strip()
|
|
12
|
-
cn_order = sorted(
|
|
12
|
+
cn_order = result.cn_order or sorted(
|
|
13
13
|
frequency.columns,
|
|
14
14
|
key=lambda column: int(column.removeprefix("CN")),
|
|
15
15
|
)
|
|
16
16
|
return frequency.loc[:, cn_order]
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _equal_level_linkage(linkage):
|
|
20
|
+
"""Give each dendrogram merge level equal display height."""
|
|
21
|
+
display_linkage = linkage.copy()
|
|
22
|
+
n_leaves = len(linkage) + 1
|
|
23
|
+
levels = np.zeros(2 * n_leaves - 1, dtype=int)
|
|
24
|
+
for index, (left, right, _, _) in enumerate(linkage):
|
|
25
|
+
level = max(levels[int(left)], levels[int(right)]) + 1
|
|
26
|
+
levels[n_leaves + index] = level
|
|
27
|
+
display_linkage[index, 2] = level
|
|
28
|
+
return display_linkage
|
|
29
|
+
|
|
30
|
+
|
|
19
31
|
def plot_dominant_celltype_results(result, config):
|
|
20
32
|
"""Save the frequency heatmap and its colorbar."""
|
|
21
33
|
import matplotlib as mpl
|
|
@@ -35,12 +47,26 @@ def plot_dominant_celltype_results(result, config):
|
|
|
35
47
|
frequency = _prepare(result)
|
|
36
48
|
|
|
37
49
|
rows, columns = frequency.shape
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
cell_fraction = min(0.68 / columns, 0.68 / rows)
|
|
51
|
+
heat_width = columns * cell_fraction
|
|
52
|
+
heat_height = rows * cell_fraction
|
|
53
|
+
figure = plt.figure(figsize=(8, 8))
|
|
54
|
+
heat_axis = figure.add_axes([0.10, 0.10, heat_width, heat_height])
|
|
55
|
+
if result.cn_linkage is not None:
|
|
56
|
+
from scipy.cluster.hierarchy import dendrogram
|
|
57
|
+
|
|
58
|
+
tree_axis = figure.add_axes([0.10, 0.108 + heat_height, heat_width, 0.075])
|
|
59
|
+
tree = dendrogram(_equal_level_linkage(result.cn_linkage), no_plot=True)
|
|
60
|
+
for x, y in zip(tree["icoord"], tree["dcoord"]):
|
|
61
|
+
tree_axis.plot(
|
|
62
|
+
[(value - 5) / 10 for value in x], y,
|
|
63
|
+
color="#4c5966", linewidth=0.8,
|
|
64
|
+
)
|
|
65
|
+
tree_axis.set_xlim(-0.5, columns - 0.5)
|
|
66
|
+
tree_axis.axis("off")
|
|
41
67
|
heat_axis.imshow(
|
|
42
68
|
frequency.to_numpy(), cmap="RdYlBu_r", vmin=0, vmax=1,
|
|
43
|
-
aspect="
|
|
69
|
+
aspect="equal", interpolation="nearest", origin="upper",
|
|
44
70
|
)
|
|
45
71
|
heat_axis.set_xticks(np.arange(columns))
|
|
46
72
|
heat_axis.set_xticklabels(frequency.columns, rotation=90, fontsize=10)
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/dotplots.py
RENAMED
|
@@ -8,6 +8,9 @@ import pandas as pd
|
|
|
8
8
|
from .common import dot_size, legend_size
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
DCF_STAR_THRESHOLDS = (0.1, 0.05)
|
|
12
|
+
|
|
13
|
+
|
|
11
14
|
def _configure_style():
|
|
12
15
|
import matplotlib as mpl
|
|
13
16
|
|
|
@@ -22,7 +25,10 @@ def _configure_style():
|
|
|
22
25
|
})
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
def _draw_panel(
|
|
28
|
+
def _draw_panel(
|
|
29
|
+
axis, record, celltypes, cn_order, norm, threshold, rotation, title,
|
|
30
|
+
star_thresholds=None,
|
|
31
|
+
):
|
|
26
32
|
effect, pvalues = record["effect"].T, record["pvalues"].T
|
|
27
33
|
x_grid, y_grid = np.meshgrid(np.arange(len(celltypes)), np.arange(len(cn_order)))
|
|
28
34
|
tested = np.isfinite(effect) & np.isfinite(pvalues)
|
|
@@ -37,14 +43,31 @@ def _draw_panel(axis, record, celltypes, cn_order, norm, threshold, rotation, ti
|
|
|
37
43
|
c=effect[mask], cmap="coolwarm", norm=norm, alpha=alpha,
|
|
38
44
|
edgecolors=edgecolor, linewidths=linewidth, zorder=zorder,
|
|
39
45
|
)
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if star_thresholds is None:
|
|
47
|
+
if np.any(significant):
|
|
48
|
+
axis.scatter(
|
|
49
|
+
x_grid[significant], y_grid[significant], s=60.0, marker="*",
|
|
50
|
+
c="#1d2630", edgecolors="none", zorder=4,
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
weak_threshold, strong_threshold = star_thresholds
|
|
54
|
+
one_star = tested & (pvalues < weak_threshold) & (pvalues >= strong_threshold)
|
|
55
|
+
two_stars = tested & (pvalues < strong_threshold)
|
|
56
|
+
if np.any(one_star):
|
|
57
|
+
axis.scatter(
|
|
58
|
+
x_grid[one_star], y_grid[one_star], s=60.0, marker="*",
|
|
59
|
+
c="#1d2630", edgecolors="none", zorder=4,
|
|
60
|
+
)
|
|
61
|
+
if np.any(two_stars):
|
|
62
|
+
for x_offset in (-0.075, 0.075):
|
|
63
|
+
axis.scatter(
|
|
64
|
+
x_grid[two_stars] + x_offset, y_grid[two_stars],
|
|
65
|
+
s=45.0, marker="*", c="#1d2630", edgecolors="none",
|
|
66
|
+
zorder=4,
|
|
67
|
+
)
|
|
45
68
|
axis.set_title(title, fontsize=13, fontweight="bold", pad=10)
|
|
46
69
|
if not np.any(tested):
|
|
47
|
-
axis.text(0.5, 0.5, "Not tested
|
|
70
|
+
axis.text(0.5, 0.5, "Not tested", transform=axis.transAxes, ha="center", va="center", color="#6d7781")
|
|
48
71
|
axis.set_xticks(np.arange(len(celltypes)))
|
|
49
72
|
axis.set_xticklabels(celltypes, rotation=rotation, ha="right" if rotation != 90 else "center", fontsize=8.5)
|
|
50
73
|
axis.set_yticks(np.arange(len(cn_order)))
|
|
@@ -58,9 +81,13 @@ def _draw_panel(axis, record, celltypes, cn_order, norm, threshold, rotation, ti
|
|
|
58
81
|
spine.set_visible(False)
|
|
59
82
|
|
|
60
83
|
|
|
61
|
-
def _add_legends(
|
|
84
|
+
def _add_legends(
|
|
85
|
+
figure, axis, norm, threshold, colorbar_title, star_thresholds=None,
|
|
86
|
+
significance_title="-log10(p-value)",
|
|
87
|
+
):
|
|
62
88
|
import matplotlib as mpl
|
|
63
89
|
import matplotlib.pyplot as plt
|
|
90
|
+
from matplotlib.legend_handler import HandlerTuple
|
|
64
91
|
|
|
65
92
|
axis.axis("off")
|
|
66
93
|
scalar_map = mpl.cm.ScalarMappable(norm=norm, cmap=plt.get_cmap("coolwarm"))
|
|
@@ -70,12 +97,31 @@ def _add_legends(figure, axis, norm, threshold, colorbar_title):
|
|
|
70
97
|
colorbar.ax.tick_params(labelsize=9)
|
|
71
98
|
values = (1.3, 2.0, 3.0)
|
|
72
99
|
handles = [axis.scatter([], [], s=legend_size(value), color="#b9414b") for value in values]
|
|
73
|
-
|
|
100
|
+
labels = [f"{value:g}" for value in values]
|
|
101
|
+
handler_map = None
|
|
102
|
+
if star_thresholds is None:
|
|
103
|
+
handles.append(axis.scatter([], [], s=60.0, marker="*", color="#1d2630"))
|
|
104
|
+
labels.append(f"p-value < {threshold:g}")
|
|
105
|
+
else:
|
|
106
|
+
weak_threshold, strong_threshold = star_thresholds
|
|
107
|
+
handles.extend([
|
|
108
|
+
axis.scatter([], [], s=60.0, marker="*", color="#1d2630"),
|
|
109
|
+
(
|
|
110
|
+
axis.scatter([], [], s=45.0, marker="*", color="#1d2630"),
|
|
111
|
+
axis.scatter([], [], s=45.0, marker="*", color="#1d2630"),
|
|
112
|
+
),
|
|
113
|
+
])
|
|
114
|
+
labels.extend([
|
|
115
|
+
f"{strong_threshold:g} <= p-value < {weak_threshold:g}",
|
|
116
|
+
f"p-value < {strong_threshold:g}",
|
|
117
|
+
])
|
|
118
|
+
handler_map = {tuple: HandlerTuple(ndivide=None, pad=0.0)}
|
|
74
119
|
axis.legend(
|
|
75
120
|
handles,
|
|
76
|
-
|
|
77
|
-
title=
|
|
121
|
+
labels,
|
|
122
|
+
title=significance_title, frameon=False, loc="center left",
|
|
78
123
|
bbox_to_anchor=(0.36, 0.50), labelspacing=1.0, handletextpad=0.75,
|
|
124
|
+
handler_map=handler_map,
|
|
79
125
|
)
|
|
80
126
|
|
|
81
127
|
|
|
@@ -97,7 +143,8 @@ def _normalization(records, *, cap=None):
|
|
|
97
143
|
|
|
98
144
|
def _save_dotplots(
|
|
99
145
|
records, celltypes, cn_order, norm, threshold, output_dir, prefix,
|
|
100
|
-
colorbar_title, *, grid_name=None, grid_title=None,
|
|
146
|
+
colorbar_title, *, grid_name=None, grid_title=None, star_thresholds=None,
|
|
147
|
+
significance_title="-log10(p-value)",
|
|
101
148
|
):
|
|
102
149
|
import matplotlib.pyplot as plt
|
|
103
150
|
|
|
@@ -109,6 +156,7 @@ def _save_dotplots(
|
|
|
109
156
|
_draw_panel(
|
|
110
157
|
axis, record, celltypes, cn_order, norm, threshold, 90,
|
|
111
158
|
f"{record['target']} vs {record['reference']}",
|
|
159
|
+
star_thresholds,
|
|
112
160
|
)
|
|
113
161
|
output = output_dir / f"{prefix}{record['name']}.pdf"
|
|
114
162
|
figure.savefig(output, bbox_inches="tight", dpi=1200)
|
|
@@ -116,7 +164,10 @@ def _save_dotplots(
|
|
|
116
164
|
plt.close(figure)
|
|
117
165
|
outputs.append(output)
|
|
118
166
|
figure, axis = plt.subplots(figsize=(4.6, 3.6))
|
|
119
|
-
_add_legends(
|
|
167
|
+
_add_legends(
|
|
168
|
+
figure, axis, norm, threshold, colorbar_title, star_thresholds,
|
|
169
|
+
significance_title,
|
|
170
|
+
)
|
|
120
171
|
legend = output_dir / "LEGEND.pdf"
|
|
121
172
|
figure.savefig(legend, bbox_inches="tight", dpi=1200)
|
|
122
173
|
plt.close(figure)
|
|
@@ -130,8 +181,12 @@ def _save_dotplots(
|
|
|
130
181
|
_draw_panel(
|
|
131
182
|
axis, record, celltypes, cn_order, norm, threshold, 55,
|
|
132
183
|
grid_title(record) if grid_title else record["target"],
|
|
184
|
+
star_thresholds,
|
|
133
185
|
)
|
|
134
|
-
_add_legends(
|
|
186
|
+
_add_legends(
|
|
187
|
+
figure, axes[len(records)], norm, threshold, colorbar_title,
|
|
188
|
+
star_thresholds, significance_title,
|
|
189
|
+
)
|
|
135
190
|
for axis in axes[len(records) + 1:]:
|
|
136
191
|
axis.set_visible(False)
|
|
137
192
|
figure.subplots_adjust(left=0.07, right=0.98, bottom=0.10, top=0.96, hspace=0.60, wspace=0.25)
|
|
@@ -170,6 +225,8 @@ def _differential_records(config, results):
|
|
|
170
225
|
celltype_index = {celltype: index for index, celltype in enumerate(celltypes)}
|
|
171
226
|
records = []
|
|
172
227
|
for name in names:
|
|
228
|
+
if not results.get(name):
|
|
229
|
+
continue
|
|
173
230
|
target, reference = name.split("_gt_", 1)
|
|
174
231
|
reference_mean = means[reference] if reference != "rest" else np.nanmean(np.stack([
|
|
175
232
|
matrix for condition, matrices in condition_matrices.items()
|
|
@@ -208,14 +265,25 @@ def plot_differential_composition_results(config, results, output_dir=None):
|
|
|
208
265
|
"""Save the original differential-composition dot plots."""
|
|
209
266
|
_configure_style()
|
|
210
267
|
celltypes, records = _differential_records(config, results)
|
|
268
|
+
output_dir = Path(output_dir or config.project_root / "plot" / "DifferentialComposed")
|
|
269
|
+
plotted_names = {record["name"] for record in records}
|
|
270
|
+
for name in results:
|
|
271
|
+
if name not in plotted_names:
|
|
272
|
+
for old_plot in (output_dir / f"{name}.pdf", output_dir / f"{name}.png"):
|
|
273
|
+
old_plot.unlink(missing_ok=True)
|
|
274
|
+
if not records:
|
|
275
|
+
for old_plot in (output_dir / "LEGEND.pdf", output_dir / "DotPlot_all.pdf"):
|
|
276
|
+
old_plot.unlink(missing_ok=True)
|
|
277
|
+
return {"plots": [], "legend": None, "grid": None}
|
|
211
278
|
threshold = config.downstream["differential_composed"]["p_value_threshold"]
|
|
212
279
|
return _save_dotplots(
|
|
213
280
|
records, celltypes, range(1, records[0]["effect"].shape[1] + 1),
|
|
214
281
|
_normalization(records, cap=4.0), threshold,
|
|
215
|
-
output_dir
|
|
282
|
+
output_dir,
|
|
216
283
|
"", "log2FC",
|
|
217
284
|
grid_name="DotPlot_all.pdf" if config.downstream["ttest_mode"] == "one_vs_rest" else None,
|
|
218
285
|
grid_title=lambda record: f"{record['target']} (n={record['sample_count']})",
|
|
286
|
+
star_thresholds=DCF_STAR_THRESHOLDS,
|
|
219
287
|
)
|
|
220
288
|
|
|
221
289
|
|
|
@@ -28,16 +28,42 @@ def calculate_enrichment_matrix(
|
|
|
28
28
|
num_cn,
|
|
29
29
|
minimum_adjusted_p,
|
|
30
30
|
):
|
|
31
|
-
"""Calculate
|
|
31
|
+
"""Calculate enrichment while retaining cells with an unassigned CN."""
|
|
32
32
|
cell_labels = np.asarray(cell_labels, dtype=str)
|
|
33
|
-
cn_labels =
|
|
33
|
+
cn_labels = pd.to_numeric(
|
|
34
|
+
pd.Series(np.asarray(cn_labels)).replace("NA", np.nan),
|
|
35
|
+
errors="raise",
|
|
36
|
+
).to_numpy(dtype=float)
|
|
37
|
+
if cell_labels.size != cn_labels.size:
|
|
38
|
+
raise ValueError("Cell-type and CN-label vectors must have equal lengths")
|
|
34
39
|
|
|
35
40
|
categories = pd.Categorical(cell_labels, categories=cell_types)
|
|
36
41
|
cell_type_codes = categories.codes
|
|
42
|
+
if np.any(cell_type_codes < 0):
|
|
43
|
+
unknown = sorted(np.unique(cell_labels[cell_type_codes < 0]).tolist())
|
|
44
|
+
raise ValueError(f"Unknown cell types: {unknown}")
|
|
45
|
+
|
|
46
|
+
assigned = np.isfinite(cn_labels)
|
|
47
|
+
assigned_labels = cn_labels[assigned]
|
|
48
|
+
if np.any(assigned_labels != np.floor(assigned_labels)) or np.any(
|
|
49
|
+
(assigned_labels < 1) | (assigned_labels > num_cn)
|
|
50
|
+
):
|
|
51
|
+
raise ValueError(f"CN labels must be integers from 1 to {num_cn}, or NA")
|
|
37
52
|
|
|
38
53
|
counts = np.zeros((len(cell_types), num_cn), dtype=np.int64)
|
|
39
|
-
np.add.at(
|
|
40
|
-
|
|
54
|
+
np.add.at(
|
|
55
|
+
counts,
|
|
56
|
+
(
|
|
57
|
+
cell_type_codes[assigned],
|
|
58
|
+
assigned_labels.astype(np.int64) - 1,
|
|
59
|
+
),
|
|
60
|
+
1,
|
|
61
|
+
)
|
|
62
|
+
# Cells without a CN assignment remain part of the sample and their cell
|
|
63
|
+
# type totals, but they do not contribute to any CN-specific overlap.
|
|
64
|
+
cell_type_totals = np.bincount(
|
|
65
|
+
cell_type_codes, minlength=len(cell_types)
|
|
66
|
+
)
|
|
41
67
|
cn_totals = counts.sum(axis=0)
|
|
42
68
|
raw_pvalues = np.ones(counts.shape, dtype=np.float64)
|
|
43
69
|
|
|
@@ -88,7 +114,7 @@ def calculate_enrichment(
|
|
|
88
114
|
.iloc[:, 0]
|
|
89
115
|
.to_numpy()
|
|
90
116
|
)
|
|
91
|
-
cn_labels = np.asarray(cn_labels_by_sample[sample]
|
|
117
|
+
cn_labels = np.asarray(cn_labels_by_sample[sample])
|
|
92
118
|
matrix = calculate_enrichment_matrix(
|
|
93
119
|
cell_labels,
|
|
94
120
|
cn_labels,
|
|
@@ -109,13 +135,14 @@ def calculate_enrichment(
|
|
|
109
135
|
def _load_cn_labels(samples, result_table_dir):
|
|
110
136
|
result_table_dir = Path(result_table_dir)
|
|
111
137
|
return {
|
|
112
|
-
sample: pd.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
138
|
+
sample: pd.to_numeric(
|
|
139
|
+
pd.read_csv(
|
|
140
|
+
result_table_dir / f"ResultTable_{sample}.csv",
|
|
141
|
+
usecols=["CN_Label"],
|
|
142
|
+
na_values=["NA"],
|
|
143
|
+
)["CN_Label"],
|
|
144
|
+
errors="raise",
|
|
145
|
+
).to_numpy(dtype=float)
|
|
119
146
|
for sample in samples
|
|
120
147
|
}
|
|
121
148
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cytocommunity2
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Weakly supervised cellular-neighborhood learning for spatial omics
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -120,10 +120,26 @@ pseudo-samples for weakly-supervised learning.
|
|
|
120
120
|
|
|
121
121
|
Before running CytoCommunity2, set `input_dir` and review all
|
|
122
122
|
model and downstream parameters in
|
|
123
|
-
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/hyperparameters.json). The Python workflow will use this configuration file.
|
|
123
|
+
[`hyperparameters.json`](https://github.com/LiukangWu/CytoCommunity2/blob/main/tutorial/hyperparameters.json). The Python workflow will use this configuration file.
|
|
124
124
|
|
|
125
125
|
### 3. Run CytoCommunity2
|
|
126
126
|
|
|
127
|
+
With `hyperparameters.json` in the working directory, run the core workflow:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pathlib import Path
|
|
131
|
+
import cytocommunity2 as cc2
|
|
132
|
+
|
|
133
|
+
CONFIG_FILE = Path("hyperparameters.json").resolve()
|
|
134
|
+
|
|
135
|
+
config = cc2.CytoCommunityConfig.from_json(CONFIG_FILE)
|
|
136
|
+
dataset = cc2.build_spatial_graphs(config)
|
|
137
|
+
learning_result = cc2.run_learning(dataset, config)
|
|
138
|
+
cc2.visualize_results(learning_result, config)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 4.Tutorial
|
|
142
|
+
|
|
127
143
|
The tutorial uses a TNBC dataset as a worked example and demonstrates the
|
|
128
144
|
entire CytoCommunity2 workflow, from model training and CN identification to
|
|
129
145
|
downstream analysis:
|
|
@@ -132,7 +148,7 @@ downstream analysis:
|
|
|
132
148
|
jupyter notebook tutorial/TNBC_workflow.ipynb
|
|
133
149
|
```
|
|
134
150
|
|
|
135
|
-
###
|
|
151
|
+
### 5. Output files
|
|
136
152
|
|
|
137
153
|
The main output directories are:
|
|
138
154
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/between.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/cca.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/utils.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/communication/within.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/__init__.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/cn_selection.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/coherence.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/common.py
RENAMED
|
File without changes
|
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/composition.py
RENAMED
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2/downstream/plotting/recurrence.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
|
|
File without changes
|
|
File without changes
|
{cytocommunity2-1.0.0 → cytocommunity2-1.0.2}/src/cytocommunity2.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|