rectanglepy 0.1.7__tar.gz → 0.3.0__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.
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.bumpversion.cfg +1 -1
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/build.yaml +1 -1
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/test.yaml +1 -1
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/CHANGELOG.md +10 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/PKG-INFO +5 -3
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/pyproject.toml +5 -3
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/create_signature.py +96 -39
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/rectangle_signature.py +4 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/rectangle.py +23 -8
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/tl/deconvolution.py +11 -13
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_pp.py +17 -7
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_rectangle.py +10 -4
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.cruft.json +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.editorconfig +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/release.yaml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/release_testpypi.yaml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.gitignore +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.pre-commit-config.yaml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.readthedocs.yaml +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/LICENSE +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/README.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/Makefile +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_static/.gitkeep +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_templates/.gitkeep +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_templates/autosummary/class.rst +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/api.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/changelog.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/conf.py +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/contributing.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/extensions/typed_returns.py +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/index.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/make.bat +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/notebooks/example.ipynb +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/references.bib +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/references.md +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/__init__.py +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_annotations_small.zip +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_counts_small.zip +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/small_fino_bulks.zip +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/__init__.py +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/tl/__init__.py +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/TIL10_signature.txt +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/bulk_small.csv +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/cell_annotations_small.txt +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_mixture_smaller.csv +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_read_fractions_small.txt +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/sc_object_small.csv +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/signature_hao1.csv +0 -0
- {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_tl.py +0 -0
|
@@ -10,6 +10,16 @@ and this project adheres to [Semantic Versioning][].
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.1.8] - 2024-09-06
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- RectangleSignatureResult now returns correlation of bulk genes with the calculated Unknown cell type fraction
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The consensus method now returns the estimations normalized to 1
|
|
22
|
+
|
|
13
23
|
## [0.1.7] - 2024-07-25
|
|
14
24
|
|
|
15
25
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: rectanglepy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Hierarchical deconvolution of bulk transcriptomics
|
|
5
5
|
Project-URL: Documentation, https://rectanglepy.readthedocs.io/
|
|
6
6
|
Project-URL: Source, https://github.com/ComputationalBiomedicineGroup/Rectangle
|
|
@@ -30,10 +30,12 @@ License: MIT License
|
|
|
30
30
|
SOFTWARE.
|
|
31
31
|
License-File: LICENSE
|
|
32
32
|
Requires-Python: >=3.10
|
|
33
|
+
Requires-Dist: anndata<0.10.9,>=0.8.0
|
|
33
34
|
Requires-Dist: loguru
|
|
34
35
|
Requires-Dist: numpy<2.0.0,>=1.0.0
|
|
35
|
-
Requires-Dist: pydeseq2==0.4.
|
|
36
|
-
Requires-Dist: quadprog==0.1.
|
|
36
|
+
Requires-Dist: pydeseq2==0.4.11
|
|
37
|
+
Requires-Dist: quadprog==0.1.12
|
|
38
|
+
Requires-Dist: statsmodels>=0.14.1
|
|
37
39
|
Provides-Extra: dev
|
|
38
40
|
Requires-Dist: bump2version; extra == 'dev'
|
|
39
41
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ requires = ["hatchling"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rectanglepy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Hierarchical deconvolution of bulk transcriptomics"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -19,10 +19,12 @@ urls.Documentation = "https://rectanglepy.readthedocs.io/"
|
|
|
19
19
|
urls.Source = "https://github.com/ComputationalBiomedicineGroup/Rectangle"
|
|
20
20
|
urls.Home-page = "https://github.com/ComputationalBiomedicineGroup/Rectangle"
|
|
21
21
|
dependencies = [
|
|
22
|
-
"pydeseq2==0.4.
|
|
23
|
-
"quadprog==0.1.
|
|
22
|
+
"pydeseq2==0.4.11",
|
|
23
|
+
"quadprog==0.1.12",
|
|
24
24
|
"loguru",
|
|
25
25
|
"numpy>=1.0.0,<2.0.0",
|
|
26
|
+
"anndata>=0.8.0,<0.10.9",
|
|
27
|
+
"statsmodels>=0.14.1"
|
|
26
28
|
]
|
|
27
29
|
|
|
28
30
|
[project.optional-dependencies]
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pandas as pd
|
|
3
|
+
import scipy.sparse
|
|
3
4
|
from anndata import AnnData
|
|
4
5
|
from loguru import logger
|
|
5
6
|
from pandas import DataFrame, Series
|
|
6
7
|
from pydeseq2.dds import DeseqDataSet
|
|
8
|
+
from pydeseq2.default_inference import DefaultInference
|
|
7
9
|
from pydeseq2.ds import DeseqStats
|
|
8
10
|
from scipy.cluster.hierarchy import fcluster, linkage
|
|
9
11
|
from scipy.stats import pearsonr
|
|
@@ -31,10 +33,13 @@ def _create_condition_number_matrices(de_adjusted, pseudo_signature):
|
|
|
31
33
|
de_adjusted_lengths = {annotation: len(de_adjusted[annotation]) for annotation in de_adjusted}
|
|
32
34
|
longest_de_analysis = max(de_adjusted_lengths.values())
|
|
33
35
|
|
|
34
|
-
loop_range = min(longest_de_analysis,
|
|
35
|
-
range_minimum =
|
|
36
|
+
loop_range = min(longest_de_analysis, 80)
|
|
37
|
+
range_minimum = 20
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
# should the data be too small we need to adjust the range, mainly for testing purposes
|
|
40
|
+
if loop_range < 8:
|
|
41
|
+
range_minimum = 2
|
|
42
|
+
elif loop_range < range_minimum:
|
|
38
43
|
range_minimum = 8
|
|
39
44
|
|
|
40
45
|
for i in range(range_minimum, loop_range):
|
|
@@ -69,13 +74,9 @@ def _calculate_cluster_range(number_of_cell_types: int) -> tuple[int, int]:
|
|
|
69
74
|
cluster_factor = 3
|
|
70
75
|
if number_of_cell_types > 12:
|
|
71
76
|
cluster_factor = 4
|
|
72
|
-
if number_of_cell_types > 20:
|
|
73
|
-
cluster_factor = 6
|
|
74
|
-
if number_of_cell_types > 50:
|
|
75
|
-
cluster_factor = 10
|
|
76
77
|
min_number_clusters = max(
|
|
77
78
|
3, number_of_cell_types - cluster_factor
|
|
78
|
-
) # we don't want to cluster too many cell types together
|
|
79
|
+
) # we don't want to cluster too many cell types together, depending on the number of cell types
|
|
79
80
|
max_number_clusters = number_of_cell_types - 1 # we want to have at least one cluster wih multiple cell types
|
|
80
81
|
return min_number_clusters, max_number_clusters
|
|
81
82
|
|
|
@@ -119,45 +120,81 @@ def _filter_de_analysis_results(de_analysis_result, p, logfc):
|
|
|
119
120
|
de_analysis_result["log2_fc"] = de_analysis_result["log2FoldChange"]
|
|
120
121
|
de_analysis_result["gene"] = de_analysis_result.index
|
|
121
122
|
adjusted_result = de_analysis_result[
|
|
122
|
-
(de_analysis_result["
|
|
123
|
+
(de_analysis_result["padj"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
|
|
123
124
|
]
|
|
124
|
-
# if increase p-value and decrease log2FC until genes are found or the threshold is reached
|
|
125
|
-
while len(adjusted_result) < 10 and (min_log2FC > 0.5 and max_p < 0.05):
|
|
126
|
-
min_log2FC = max(min_log2FC - 0.1, 0.5)
|
|
127
|
-
max_p = min(max_p + 0.001, 0.05)
|
|
128
|
-
adjusted_result = de_analysis_result[
|
|
129
|
-
(de_analysis_result["pvalue"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
|
|
130
|
-
]
|
|
131
125
|
|
|
132
126
|
return adjusted_result
|
|
133
127
|
|
|
134
128
|
|
|
135
|
-
def _run_deseq2(
|
|
129
|
+
def _run_deseq2(
|
|
130
|
+
countsig: pd.DataFrame, sc_data, annotations: pd.Series, n_cpus: int = None, gene_expression_threshold=0.5
|
|
131
|
+
) -> dict[str | int, pd.DataFrame]:
|
|
136
132
|
results = {}
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
inference = DefaultInference(n_cpus=n_cpus)
|
|
134
|
+
bootstrapped_signature = _create_bootstrap_signature(countsig, sc_data, annotations)
|
|
135
|
+
np.random.seed(42)
|
|
136
|
+
for _i, cell_type in enumerate(countsig.columns):
|
|
137
|
+
bootstrapped_signature_copy = bootstrapped_signature.copy()
|
|
138
|
+
countsig_copy = countsig.copy()
|
|
139
|
+
sc_data_filtered = sc_data.T[annotations == cell_type]
|
|
140
|
+
expressed_cells = (sc_data_filtered > 0).sum(axis=0)
|
|
141
|
+
if expressed_cells.ndim > 1: # needed for sparse matrices
|
|
142
|
+
expressed_cells = np.squeeze(np.asarray(expressed_cells))
|
|
143
|
+
# make dense out of sparse
|
|
144
|
+
sc_data_filtered = sc_data_filtered.toarray()
|
|
145
|
+
threshold = gene_expression_threshold * sc_data_filtered.shape[0]
|
|
146
|
+
genes = countsig_copy.index[expressed_cells > threshold].tolist()
|
|
147
|
+
bootstrapped_signature_copy = bootstrapped_signature_copy.loc[genes].T
|
|
139
148
|
logger.info(f"Running DE analysis for {cell_type}")
|
|
140
|
-
condition =
|
|
141
|
-
condition
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
condition = ["B" if (cell_type + "_") in x else "A" for x in bootstrapped_signature_copy.index]
|
|
150
|
+
clinical_df = pd.DataFrame({"condition": condition}, index=bootstrapped_signature_copy.index)
|
|
151
|
+
dds = DeseqDataSet(
|
|
152
|
+
counts=bootstrapped_signature_copy,
|
|
153
|
+
metadata=clinical_df,
|
|
154
|
+
design_factors="condition",
|
|
155
|
+
quiet=True,
|
|
156
|
+
inference=inference,
|
|
157
|
+
refit_cooks=False,
|
|
158
|
+
)
|
|
144
159
|
dds.deseq2()
|
|
145
|
-
dds
|
|
146
|
-
dds.varm["dispersions"] = dds.varm["dispersions"].round(3)
|
|
147
|
-
|
|
148
|
-
stat_res = DeseqStats(dds, n_cpus=n_cpus, quiet=True)
|
|
160
|
+
stat_res = DeseqStats(dds, inference=inference, quiet=True, cooks_filter=False)
|
|
149
161
|
stat_res.summary(quiet=True)
|
|
150
|
-
stat_res.lfc_shrink()
|
|
162
|
+
stat_res.lfc_shrink("condition_B_vs_A")
|
|
151
163
|
results[cell_type] = stat_res.results_df
|
|
152
164
|
|
|
153
165
|
return results
|
|
154
166
|
|
|
155
167
|
|
|
168
|
+
def _create_bootstrap_signature(countsig, sc_data, annotations) -> pd.DataFrame:
|
|
169
|
+
if scipy.sparse.issparse(sc_data):
|
|
170
|
+
sc_data = sc_data.toarray()
|
|
171
|
+
celltypes = countsig.columns
|
|
172
|
+
bootstrapped_signature = pd.DataFrame()
|
|
173
|
+
number_of_bootstraps = 5
|
|
174
|
+
samples_per_bootstrap = 250
|
|
175
|
+
for celltype in celltypes:
|
|
176
|
+
sc_data_filtered = sc_data.T[annotations == celltype]
|
|
177
|
+
for i in range(number_of_bootstraps):
|
|
178
|
+
selected_rows = np.random.choice(len(sc_data_filtered), samples_per_bootstrap, replace=True)
|
|
179
|
+
summed_rows = sc_data_filtered[selected_rows].sum(axis=0)
|
|
180
|
+
bootstrapped_signature[f"{celltype}_{i}"] = list(summed_rows)
|
|
181
|
+
bootstrapped_signature.index = countsig.index
|
|
182
|
+
return bootstrapped_signature
|
|
183
|
+
|
|
184
|
+
|
|
156
185
|
def _de_analysis(
|
|
157
|
-
pseudo_count_sig,
|
|
186
|
+
pseudo_count_sig,
|
|
187
|
+
sc_data,
|
|
188
|
+
annotations: pd.Series,
|
|
189
|
+
p,
|
|
190
|
+
lfc,
|
|
191
|
+
optimize_cutoffs: bool,
|
|
192
|
+
n_cpus: int = None,
|
|
193
|
+
genes=None,
|
|
194
|
+
gene_expression_threshold=0.5,
|
|
158
195
|
) -> tuple[Series, dict[str, [str]] :, DataFrame | None]:
|
|
159
196
|
logger.info("Starting DE analysis")
|
|
160
|
-
deseq_results = _run_deseq2(pseudo_count_sig, n_cpus)
|
|
197
|
+
deseq_results = _run_deseq2(pseudo_count_sig, sc_data, annotations, n_cpus, gene_expression_threshold)
|
|
161
198
|
optimization_results = None
|
|
162
199
|
|
|
163
200
|
if optimize_cutoffs:
|
|
@@ -186,6 +223,9 @@ def _get_marker_genes(deseq_results, logfc, p, pseudo_count_sig):
|
|
|
186
223
|
|
|
187
224
|
markers = optimal_condition_matrix.index
|
|
188
225
|
marker_genes_per_cell_type = _get_marker_genes_per_cell_type(de_analysis_adjusted, optimal_condition_number)
|
|
226
|
+
flattened_markers = [item for sublist in marker_genes_per_cell_type.values() for item in sublist]
|
|
227
|
+
duplicated_markers = [x for x in flattened_markers if flattened_markers.count(x) > 1]
|
|
228
|
+
print("duplicated markers: ", duplicated_markers)
|
|
189
229
|
return markers, marker_genes_per_cell_type
|
|
190
230
|
|
|
191
231
|
|
|
@@ -246,6 +286,7 @@ def build_rectangle_signatures(
|
|
|
246
286
|
sample_size: int = 1500,
|
|
247
287
|
n_cpus: int = None,
|
|
248
288
|
run: int = 0,
|
|
289
|
+
gene_expression_threshold=0.5,
|
|
249
290
|
) -> RectangleSignatureResult:
|
|
250
291
|
r"""Builds rectangle signatures based on single-cell count data and annotations.
|
|
251
292
|
|
|
@@ -275,11 +316,16 @@ def build_rectangle_signatures(
|
|
|
275
316
|
The number of cpus to use for the DE analysis. Defaults to the number of cpus available.
|
|
276
317
|
run
|
|
277
318
|
The consensus run number for the analysis. Defaults to 0.
|
|
319
|
+
gene_expression_threshold
|
|
320
|
+
The gene expression threshold for the DE analysis. How many cells need to express a gene to be considered in DGE
|
|
278
321
|
|
|
279
322
|
Returns
|
|
280
323
|
-------
|
|
281
324
|
The result of the rectangle signature analysis which is of type RectangleSignatureResult.
|
|
282
325
|
"""
|
|
326
|
+
annotations = adata.obs[cell_type_col]
|
|
327
|
+
adata = adata[:, adata.X.sum(axis=0) > len(annotations.value_counts())]
|
|
328
|
+
|
|
283
329
|
if bulks is not None:
|
|
284
330
|
genes = list(set(bulks.columns) & set(adata.var_names))
|
|
285
331
|
genes = sorted(genes)
|
|
@@ -287,7 +333,6 @@ def build_rectangle_signatures(
|
|
|
287
333
|
logger.info(f"Using {len(genes)} common genes between bulks and single-cell data")
|
|
288
334
|
adata = adata[:, genes]
|
|
289
335
|
|
|
290
|
-
annotations = adata.obs[cell_type_col]
|
|
291
336
|
if subsample:
|
|
292
337
|
annotations = _even(annotations, sample_size, run)
|
|
293
338
|
adata = adata[annotations.index]
|
|
@@ -307,7 +352,7 @@ def build_rectangle_signatures(
|
|
|
307
352
|
m_rna_biasfactors = _create_bias_factors(pseudo_sig_counts, sc_counts, annotations)
|
|
308
353
|
|
|
309
354
|
marker_genes, marker_genes_per_cell_type, optimization_result = _de_analysis(
|
|
310
|
-
pseudo_sig_counts, sc_counts, annotations, p, lfc, optimize_cutoffs, n_cpus, genes
|
|
355
|
+
pseudo_sig_counts, sc_counts, annotations, p, lfc, optimize_cutoffs, n_cpus, genes, gene_expression_threshold
|
|
311
356
|
)
|
|
312
357
|
pseudo_sig_cpm = _convert_to_cpm(pseudo_sig_counts)
|
|
313
358
|
logger.info("Starting rectangle cluster analysis")
|
|
@@ -327,7 +372,13 @@ def build_rectangle_signatures(
|
|
|
327
372
|
|
|
328
373
|
clustered_biasfact = _create_bias_factors(clustered_signature, sc_counts, clustered_annotations)
|
|
329
374
|
clustered_genes, clustered_marker_genes_per_cell_type, _ = _de_analysis(
|
|
330
|
-
clustered_signature,
|
|
375
|
+
clustered_signature,
|
|
376
|
+
sc_counts,
|
|
377
|
+
clustered_annotations,
|
|
378
|
+
p,
|
|
379
|
+
lfc,
|
|
380
|
+
False,
|
|
381
|
+
gene_expression_threshold=gene_expression_threshold,
|
|
331
382
|
)
|
|
332
383
|
clustered_signature = _convert_to_cpm(clustered_signature)
|
|
333
384
|
return RectangleSignatureResult(
|
|
@@ -359,20 +410,26 @@ def _create_pseudo_count_sig(sc_counts: np.ndarray, annotations: pd.Series, var_
|
|
|
359
410
|
def _optimize_parameters(
|
|
360
411
|
sc_data: pd.DataFrame, annotations: pd.Series, pseudo_signature_counts: pd.DataFrame, de_results, genes=None
|
|
361
412
|
) -> pd.DataFrame:
|
|
362
|
-
#
|
|
363
|
-
lfcs = [x / 100 for x in range(
|
|
364
|
-
ps = [x / 1000 for x in range(
|
|
413
|
+
# search space for p and lfc
|
|
414
|
+
lfcs = [x / 100 for x in range(160, 230, 10)]
|
|
415
|
+
ps = [x / 1000 for x in range(50, 51, 1)]
|
|
365
416
|
|
|
366
417
|
results = []
|
|
367
418
|
logger.info("generating pseudo bulks")
|
|
368
419
|
bulks, real_fractions = _generate_pseudo_bulks(sc_data, annotations, genes)
|
|
369
420
|
for p in ps:
|
|
370
421
|
for lfc in lfcs:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
422
|
+
try:
|
|
423
|
+
rmse, pearson_r = _assess_parameter_fit(
|
|
424
|
+
lfc, p, bulks, real_fractions, pseudo_signature_counts, de_results
|
|
425
|
+
)
|
|
426
|
+
logger.info(f"RMSE:{rmse}, Pearson R:{pearson_r} for p={p}, lfc={lfc}")
|
|
427
|
+
results.append({"p": p, "lfc": lfc, "rmse": rmse, "pearson_r": pearson_r})
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Error in assessing parameter fit for p={p}, lfc={lfc}: {e}")
|
|
374
430
|
|
|
375
431
|
results_df = pd.DataFrame(results)
|
|
432
|
+
# best results first
|
|
376
433
|
results_df = results_df.sort_values(by=["pearson_r", "rmse"], ascending=[False, True])
|
|
377
434
|
|
|
378
435
|
return results_df
|
|
@@ -22,6 +22,8 @@ class RectangleSignatureResult:
|
|
|
22
22
|
The number of marker genes per cell type, as a dictionary. Keys are cell type names, values are the number of marker genes.
|
|
23
23
|
optimization_result
|
|
24
24
|
The result of the p lfc cut off optimization, as a pd.DataFrame. Contains the following columns: p, lfc, pearson_r, rsme
|
|
25
|
+
unkn_gene_corr
|
|
26
|
+
The correlation between the unknown cell type and the genes linked to the unknown cell type.
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
def __init__(
|
|
@@ -36,6 +38,7 @@ class RectangleSignatureResult:
|
|
|
36
38
|
marker_genes_per_cluster: dict[str, [str]] = None,
|
|
37
39
|
clustered_signature_genes: pd.Series = None,
|
|
38
40
|
cluster_assignments: list[int or str] = None,
|
|
41
|
+
unkn_gene_corr: pd.Series = None,
|
|
39
42
|
):
|
|
40
43
|
self.signature_genes = signature_genes
|
|
41
44
|
self.bias_factors = bias_factors
|
|
@@ -47,6 +50,7 @@ class RectangleSignatureResult:
|
|
|
47
50
|
self.clustered_bias_factors = clustered_bias_factors
|
|
48
51
|
self.clustered_signature_genes = clustered_signature_genes
|
|
49
52
|
self.assignments = cluster_assignments
|
|
53
|
+
self.unkn_gene_corr = unkn_gene_corr
|
|
50
54
|
|
|
51
55
|
def get_signature_matrix(self, include_mrna_bias=True) -> pd.DataFrame:
|
|
52
56
|
"""Calculates the signature matrix by multiplying the pseudobulk_sig_cpm DataFrame subset by signature_genes and the bias_factors Series.
|
|
@@ -39,6 +39,7 @@ def rectangle_consens(
|
|
|
39
39
|
p=0.015,
|
|
40
40
|
lfc=1.5,
|
|
41
41
|
n_cpus: int = None,
|
|
42
|
+
gene_expression_threshold=0.5,
|
|
42
43
|
) -> tuple[DataFrame, RectangleSignatureResult, ConsensusResult]:
|
|
43
44
|
r"""All in one deconvolution method. Creates signatures and deconvolutes the bulk data. Has options for subsampling and consensus runs.
|
|
44
45
|
|
|
@@ -70,6 +71,8 @@ def rectangle_consens(
|
|
|
70
71
|
The number of cpus to use for the DE analysis. None value takes all cpus available.
|
|
71
72
|
correct_mrna_bias : bool
|
|
72
73
|
A flag indicating whether to correct for mRNA bias. Defaults to True.
|
|
74
|
+
gene_expression_threshold : float
|
|
75
|
+
The threshold for gene expression. Genes with expression below this threshold are removed from the analysis.
|
|
73
76
|
|
|
74
77
|
Returns
|
|
75
78
|
-------
|
|
@@ -80,12 +83,6 @@ def rectangle_consens(
|
|
|
80
83
|
assert isinstance(adata, AnnData), "adata must be an AnnData object"
|
|
81
84
|
assert isinstance(bulks, DataFrame), "bulks must be a DataFrame"
|
|
82
85
|
|
|
83
|
-
if bulks is not None:
|
|
84
|
-
genes = list(set(bulks.columns) & set(adata.var_names))
|
|
85
|
-
genes = sorted(genes)
|
|
86
|
-
adata = adata[:, genes]
|
|
87
|
-
bulks = bulks[genes]
|
|
88
|
-
|
|
89
86
|
if consensus_runs > 1:
|
|
90
87
|
logger.info(f"Running {consensus_runs} consensus runs with subsample size {sample_size}")
|
|
91
88
|
subsample = True
|
|
@@ -109,14 +106,20 @@ def rectangle_consens(
|
|
|
109
106
|
subsample=subsample,
|
|
110
107
|
sample_size=sample_size,
|
|
111
108
|
run=i,
|
|
109
|
+
gene_expression_threshold=gene_expression_threshold,
|
|
112
110
|
)
|
|
113
|
-
rectangle_signature_results.append(signatures)
|
|
114
111
|
most_recent_signatures = signatures
|
|
115
112
|
cell_fractions = deconvolution(signatures, bulks, correct_mrna_bias, n_cpus)
|
|
116
113
|
estimations.append(cell_fractions)
|
|
114
|
+
unkn_gene_corr = _genes_linked_to_unkn(bulks, cell_fractions["Unknown"])
|
|
115
|
+
signatures.unkn_gene_corr = unkn_gene_corr
|
|
116
|
+
rectangle_signature_results.append(signatures)
|
|
117
|
+
consensus_estimations = pd.concat(estimations).groupby(level=0).median()
|
|
117
118
|
|
|
119
|
+
# normalize the estimations to 1, needed for the consensus
|
|
120
|
+
consensus_estimations = consensus_estimations.div(consensus_estimations.sum(axis=1), axis=0)
|
|
118
121
|
consensus_results = ConsensusResult(estimations, rectangle_signature_results)
|
|
119
|
-
return
|
|
122
|
+
return consensus_estimations, most_recent_signatures, consensus_results
|
|
120
123
|
|
|
121
124
|
|
|
122
125
|
def rectangle(
|
|
@@ -131,6 +134,7 @@ def rectangle(
|
|
|
131
134
|
p=0.015,
|
|
132
135
|
lfc=1.5,
|
|
133
136
|
n_cpus: int = None,
|
|
137
|
+
gene_expression_threshold=0.5,
|
|
134
138
|
) -> tuple[DataFrame, RectangleSignatureResult]:
|
|
135
139
|
r"""All in one deconvolution method. Creates signatures and deconvolutes the bulk data. Has options for subsampling and consensus runs.
|
|
136
140
|
|
|
@@ -156,6 +160,8 @@ def rectangle(
|
|
|
156
160
|
The number of cpus to use for the DE analysis. None value takes all cpus available.
|
|
157
161
|
correct_mrna_bias : bool
|
|
158
162
|
A flag indicating whether to correct for mRNA bias. Defaults to True.
|
|
163
|
+
gene_expression_threshold : float
|
|
164
|
+
The threshold for gene expression. Genes with expression below this threshold are removed from the analysis.
|
|
159
165
|
|
|
160
166
|
Returns
|
|
161
167
|
-------
|
|
@@ -176,6 +182,7 @@ def rectangle(
|
|
|
176
182
|
p=p,
|
|
177
183
|
lfc=lfc,
|
|
178
184
|
n_cpus=n_cpus,
|
|
185
|
+
gene_expression_threshold=gene_expression_threshold,
|
|
179
186
|
)
|
|
180
187
|
return estimations, signatures
|
|
181
188
|
|
|
@@ -197,3 +204,11 @@ def load_tutorial_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
|
|
|
197
204
|
bulks = pd.read_csv(bulks_file, index_col=0, compression="zip")
|
|
198
205
|
|
|
199
206
|
return sc_counts.T, annotations, bulks.T
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _genes_linked_to_unkn(bulks: DataFrame, unkn_fractions: pd.Series):
|
|
210
|
+
genes = bulks.columns
|
|
211
|
+
corr = []
|
|
212
|
+
for gene in genes:
|
|
213
|
+
corr.append(unkn_fractions.corr(bulks.loc[:, gene]))
|
|
214
|
+
return pd.Series(corr, index=genes).sort_values(ascending=False)
|
|
@@ -180,6 +180,8 @@ def deconvolution(
|
|
|
180
180
|
A DataFrame containing the estimated cell fractions resulting from deconvolution. Each row represents a sample and each column represents a cell type.
|
|
181
181
|
|
|
182
182
|
"""
|
|
183
|
+
bulks = bulks.div(bulks.sum(axis=1), axis=0) * 1e6
|
|
184
|
+
|
|
183
185
|
if n_cpus is not None:
|
|
184
186
|
num_processes = n_cpus
|
|
185
187
|
else:
|
|
@@ -223,7 +225,7 @@ def _deconvolute(signatures: RectangleSignatureResult, bulk: pd.Series, correct_
|
|
|
223
225
|
bias_factors = signatures.bias_factors
|
|
224
226
|
|
|
225
227
|
if not correct_mrna_bias:
|
|
226
|
-
bias_factors = bias_factors * 0 + 1
|
|
228
|
+
bias_factors = bias_factors * 0 + 1 # set all bias factors to 1
|
|
227
229
|
|
|
228
230
|
signature = pseudobulk_sig_cpm.loc[signature_genes_direct_reduced] * bias_factors
|
|
229
231
|
start_fractions = _calculate_dwls(signature, bulk)
|
|
@@ -242,36 +244,30 @@ def _deconvolute(signatures: RectangleSignatureResult, bulk: pd.Series, correct_
|
|
|
242
244
|
]
|
|
243
245
|
clustered_signature = clustered_pseudobulk_sig_cpm.loc[clustered_signature_genes] * cluster_bias_factors
|
|
244
246
|
|
|
245
|
-
union_genes = list(set(signature_genes_direct_reduced) | set(clustered_signature_genes))
|
|
246
|
-
union_direct_signature = pseudobulk_sig_cpm.loc[union_genes] * bias_factors
|
|
247
|
-
|
|
248
247
|
try:
|
|
249
248
|
clustered_fractions = _calculate_dwls(clustered_signature, bulk)
|
|
250
249
|
recursive_fractions = _calculate_dwls(signature, bulk, signatures.assignments, clustered_fractions)
|
|
251
250
|
except Exception as e:
|
|
252
251
|
logger.warning(f"Recursive deconvolution failed with error: {e}")
|
|
252
|
+
start_fractions = correct_for_unknown_cell_content(bulk, pseudobulk_sig_cpm, start_fractions, bias_factors)
|
|
253
253
|
return start_fractions
|
|
254
254
|
|
|
255
|
-
union_direct_fraction = _calculate_dwls(union_direct_signature, bulk)
|
|
256
|
-
|
|
257
|
-
averaged_start_fractions = (start_fractions + union_direct_fraction) / 2
|
|
258
|
-
|
|
259
255
|
final_fractions = []
|
|
260
256
|
|
|
261
|
-
low_number_threshold =
|
|
257
|
+
low_number_threshold = 20
|
|
262
258
|
cell_types_with_low_number_of_marker_genes = [
|
|
263
259
|
cell_type
|
|
264
260
|
for cell_type, num_marker_genes in signatures.marker_genes_per_cell_type.items()
|
|
265
261
|
if len(num_marker_genes) < low_number_threshold
|
|
266
262
|
]
|
|
267
263
|
|
|
268
|
-
for cell_type in list(
|
|
264
|
+
for cell_type in list(start_fractions.index):
|
|
269
265
|
if cell_type in cell_types_with_low_number_of_marker_genes:
|
|
270
266
|
final_fractions.append(recursive_fractions[cell_type])
|
|
271
267
|
else:
|
|
272
|
-
final_fractions.append(
|
|
268
|
+
final_fractions.append(start_fractions[cell_type])
|
|
273
269
|
|
|
274
|
-
final_fractions = pd.Series(final_fractions, index=
|
|
270
|
+
final_fractions = pd.Series(final_fractions, index=start_fractions.index)
|
|
275
271
|
|
|
276
272
|
final_fractions = correct_for_unknown_cell_content(bulk, pseudobulk_sig_cpm, final_fractions, bias_factors)
|
|
277
273
|
return final_fractions
|
|
@@ -318,6 +314,8 @@ def correct_for_unknown_cell_content(
|
|
|
318
314
|
estimates_fix.loc["Unknown"] = 0
|
|
319
315
|
return estimates_fix
|
|
320
316
|
|
|
317
|
+
signature_genes = pseudo_signature_cpm.index
|
|
318
|
+
bulk = bulk.loc[signature_genes]
|
|
321
319
|
signature = pseudo_signature_cpm.sort_index()
|
|
322
320
|
bulk = bulk.sort_index()
|
|
323
321
|
|
|
@@ -330,7 +328,7 @@ def correct_for_unknown_cell_content(
|
|
|
330
328
|
# Calculate the unknown cellular content ad the difference of
|
|
331
329
|
# per-sample overall expression levels in the true vs. reconstructed
|
|
332
330
|
# bulk RNA-seq data, divided by the overall expression in the true bulk
|
|
333
|
-
ukn_cc = (bulk - bulk_est
|
|
331
|
+
ukn_cc = (bulk.sum() - bulk_est.sum()) / (bulk.sum())
|
|
334
332
|
ukn_cc = max(0, ukn_cc)
|
|
335
333
|
# Correct (i.e. scale) the cell fraction estimates so that their sum
|
|
336
334
|
# equals 1 - the unknown cellular content estimated above
|
|
@@ -11,6 +11,7 @@ from rectanglepy.pp.create_signature import (
|
|
|
11
11
|
_calculate_cluster_range,
|
|
12
12
|
_create_annotations_from_cluster_labels,
|
|
13
13
|
_create_bias_factors,
|
|
14
|
+
_create_bootstrap_signature,
|
|
14
15
|
_create_fclusters,
|
|
15
16
|
_create_linkage_matrix,
|
|
16
17
|
_create_pseudo_count_sig,
|
|
@@ -171,7 +172,7 @@ def test_asses_fit(small_data):
|
|
|
171
172
|
sc_counts, annotations, bulk = small_data
|
|
172
173
|
sc_counts = sc_counts.astype("int")
|
|
173
174
|
sc_pseudo = sc_counts.groupby(annotations.values, axis=1).sum()
|
|
174
|
-
de_result = _run_deseq2(sc_pseudo,
|
|
175
|
+
de_result = _run_deseq2(sc_pseudo, sc_counts.values, annotations)
|
|
175
176
|
|
|
176
177
|
adata = AnnData(sc_counts.T, obs=annotations.to_frame(name="cell_type"))
|
|
177
178
|
bulks, real_fractions = _generate_pseudo_bulks(adata.X.T, annotations, adata.var_names)
|
|
@@ -196,17 +197,16 @@ def test_de_analysis(small_data):
|
|
|
196
197
|
sc_pseudo = sc_counts.groupby(annotations.values, axis=1).sum()
|
|
197
198
|
|
|
198
199
|
adata = AnnData(sc_counts.T, obs=annotations.to_frame(name="cell_type"))
|
|
199
|
-
r1, r2, r3 = _de_analysis(sc_pseudo, adata.X.T, annotations, 0.
|
|
200
|
+
r1, r2, r3 = _de_analysis(sc_pseudo, adata.X.T, annotations, 0.4, 0.1, False, None, adata.var_names)
|
|
200
201
|
|
|
201
202
|
sc_counts = sc_counts.astype(pd.SparseDtype("int"))
|
|
202
203
|
csr_sparse_matrix = sc_counts.sparse.to_coo().tocsr()
|
|
203
204
|
adata_sparse = AnnData(csr_sparse_matrix.T, obs=annotations.to_frame(name="cell_type"))
|
|
204
|
-
|
|
205
|
+
# test with sparse matrix
|
|
206
|
+
_ = _de_analysis(sc_pseudo, adata_sparse.X.T, annotations, 0.4, 0.1, False, None, adata.var_names)
|
|
205
207
|
|
|
206
|
-
assert
|
|
208
|
+
assert 5 < len(r1) < 50
|
|
207
209
|
assert len(r2) == 3
|
|
208
|
-
assert (r1.values == rs1.values).all()
|
|
209
|
-
assert r2 == rs2
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def test_even(small_data):
|
|
@@ -239,5 +239,15 @@ def test_build_rectangle_signatures_even(small_data):
|
|
|
239
239
|
adata, "cell_type", bulks=bulk.T, p=0.5, lfc=0.1, optimize_cutoffs=False
|
|
240
240
|
)
|
|
241
241
|
|
|
242
|
-
assert results_uneven.signature_genes.equals(results_even.signature_genes)
|
|
243
242
|
assert results_even.bias_factors.equals(results_uneven.bias_factors)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_create_bootstrap_signature(small_data):
|
|
246
|
+
bootstraps_per_cell = 5
|
|
247
|
+
sc_counts, annotations, bulk = small_data
|
|
248
|
+
sc_counts = sc_counts.astype("int")
|
|
249
|
+
sc_pseudo = sc_counts.groupby(annotations.values, axis=1).sum()
|
|
250
|
+
adata = AnnData(sc_counts.T, obs=annotations.to_frame(name="cell_type"))
|
|
251
|
+
bootstrap = _create_bootstrap_signature(sc_pseudo, adata.X.T, annotations)
|
|
252
|
+
|
|
253
|
+
assert len(bootstrap.columns) == len(sc_pseudo.columns) * bootstraps_per_cell
|
|
@@ -15,12 +15,15 @@ def test_load_tutorial_data():
|
|
|
15
15
|
|
|
16
16
|
def test_rectangle():
|
|
17
17
|
sc_data, annotations, bulks = rectanglepy.load_tutorial_data()
|
|
18
|
+
sc_data = sc_data.iloc[:, :2000]
|
|
18
19
|
sc_data_adata = AnnData(sc_data, obs=annotations.to_frame(name="cell_type"))
|
|
19
20
|
|
|
20
21
|
result = rectanglepy.rectangle(sc_data_adata, bulks)
|
|
22
|
+
estimations, signatures = result
|
|
21
23
|
|
|
22
|
-
assert isinstance(
|
|
23
|
-
assert isinstance(
|
|
24
|
+
assert isinstance(estimations, pd.DataFrame)
|
|
25
|
+
assert isinstance(signatures, RectangleSignatureResult)
|
|
26
|
+
assert isinstance(signatures.unkn_gene_corr, pd.Series)
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def test_rectangle_consensus():
|
|
@@ -29,17 +32,20 @@ def test_rectangle_consensus():
|
|
|
29
32
|
sc_data_adata = AnnData(sc_data, obs=annotations.to_frame(name="cell_type"))
|
|
30
33
|
|
|
31
34
|
result = rectanglepy.rectangle_consens(
|
|
32
|
-
sc_data_adata, bulks, optimize_cutoffs=False, p=0.
|
|
35
|
+
sc_data_adata, bulks, optimize_cutoffs=False, p=0.2, lfc=0.0, consensus_runs=3, sample_size=50
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
assert isinstance(result[0], pd.DataFrame)
|
|
36
39
|
assert isinstance(result[1], RectangleSignatureResult)
|
|
37
40
|
assert isinstance(result[2], ConsensusResult)
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
estimations = result[0]
|
|
40
43
|
consensus_results = result[2]
|
|
41
44
|
signature_results = consensus_results.rectangle_signature_results
|
|
42
45
|
bias_factors = [result.bias_factors for result in signature_results]
|
|
46
|
+
# there was a problem with the random sampling, the bias factors should differ
|
|
43
47
|
assert bias_factors[0]["Monocytes"] != bias_factors[1]["Monocytes"]
|
|
44
48
|
assert bias_factors[0]["Monocytes"] != bias_factors[2]["Monocytes"]
|
|
45
49
|
assert bias_factors[1]["Monocytes"] != bias_factors[2]["Monocytes"]
|
|
50
|
+
# in the consensus we have to make sure that the estimations are again normalized to 1
|
|
51
|
+
assert estimations.sum(axis=1).all() == 1.0
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
{rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_read_fractions_small.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|