rectanglepy 0.1.8__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.8 → rectanglepy-0.3.0}/.bumpversion.cfg +1 -1
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/workflows/build.yaml +1 -1
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/workflows/test.yaml +1 -1
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/PKG-INFO +4 -3
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/pyproject.toml +4 -3
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/pp/create_signature.py +85 -35
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/rectangle.py +10 -7
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/tl/deconvolution.py +11 -13
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/test_pp.py +17 -7
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/test_rectangle.py +2 -2
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.cruft.json +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.editorconfig +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/workflows/release.yaml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.github/workflows/release_testpypi.yaml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.gitignore +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.pre-commit-config.yaml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/.readthedocs.yaml +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/CHANGELOG.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/LICENSE +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/README.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/Makefile +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/_static/.gitkeep +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/_templates/.gitkeep +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/_templates/autosummary/class.rst +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/api.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/changelog.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/conf.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/contributing.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/extensions/typed_returns.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/index.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/make.bat +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/notebooks/example.ipynb +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/references.bib +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/docs/references.md +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/__init__.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_annotations_small.zip +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_counts_small.zip +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/data/small_fino_bulks.zip +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/pp/__init__.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/pp/rectangle_signature.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/src/rectanglepy/tl/__init__.py +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/TIL10_signature.txt +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/bulk_small.csv +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/cell_annotations_small.txt +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_mixture_smaller.csv +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_read_fractions_small.txt +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/sc_object_small.csv +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/data/signature_hao1.csv +0 -0
- {rectanglepy-0.1.8 → rectanglepy-0.3.0}/tests/test_tl.py +0 -0
|
@@ -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
|
|
@@ -33,8 +33,9 @@ Requires-Python: >=3.10
|
|
|
33
33
|
Requires-Dist: anndata<0.10.9,>=0.8.0
|
|
34
34
|
Requires-Dist: loguru
|
|
35
35
|
Requires-Dist: numpy<2.0.0,>=1.0.0
|
|
36
|
-
Requires-Dist: pydeseq2==0.4.
|
|
37
|
-
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
|
|
38
39
|
Provides-Extra: dev
|
|
39
40
|
Requires-Dist: bump2version; extra == 'dev'
|
|
40
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,11 +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
26
|
"anndata>=0.8.0,<0.10.9",
|
|
27
|
+
"statsmodels>=0.14.1"
|
|
27
28
|
]
|
|
28
29
|
|
|
29
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,9 +33,10 @@ 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
|
|
|
39
|
+
# should the data be too small we need to adjust the range, mainly for testing purposes
|
|
37
40
|
if loop_range < 8:
|
|
38
41
|
range_minimum = 2
|
|
39
42
|
elif loop_range < range_minimum:
|
|
@@ -71,13 +74,9 @@ def _calculate_cluster_range(number_of_cell_types: int) -> tuple[int, int]:
|
|
|
71
74
|
cluster_factor = 3
|
|
72
75
|
if number_of_cell_types > 12:
|
|
73
76
|
cluster_factor = 4
|
|
74
|
-
if number_of_cell_types > 20:
|
|
75
|
-
cluster_factor = 6
|
|
76
|
-
if number_of_cell_types > 50:
|
|
77
|
-
cluster_factor = 10
|
|
78
77
|
min_number_clusters = max(
|
|
79
78
|
3, number_of_cell_types - cluster_factor
|
|
80
|
-
) # 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
|
|
81
80
|
max_number_clusters = number_of_cell_types - 1 # we want to have at least one cluster wih multiple cell types
|
|
82
81
|
return min_number_clusters, max_number_clusters
|
|
83
82
|
|
|
@@ -121,45 +120,81 @@ def _filter_de_analysis_results(de_analysis_result, p, logfc):
|
|
|
121
120
|
de_analysis_result["log2_fc"] = de_analysis_result["log2FoldChange"]
|
|
122
121
|
de_analysis_result["gene"] = de_analysis_result.index
|
|
123
122
|
adjusted_result = de_analysis_result[
|
|
124
|
-
(de_analysis_result["
|
|
123
|
+
(de_analysis_result["padj"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
|
|
125
124
|
]
|
|
126
|
-
# if increase p-value and decrease log2FC until genes are found or the threshold is reached
|
|
127
|
-
while len(adjusted_result) < 10 and (min_log2FC > 0.5 and max_p < 0.05):
|
|
128
|
-
min_log2FC = max(min_log2FC - 0.1, 0.5)
|
|
129
|
-
max_p = min(max_p + 0.001, 0.05)
|
|
130
|
-
adjusted_result = de_analysis_result[
|
|
131
|
-
(de_analysis_result["pvalue"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
|
|
132
|
-
]
|
|
133
125
|
|
|
134
126
|
return adjusted_result
|
|
135
127
|
|
|
136
128
|
|
|
137
|
-
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]:
|
|
138
132
|
results = {}
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
148
|
logger.info(f"Running DE analysis for {cell_type}")
|
|
142
|
-
condition =
|
|
143
|
-
condition
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
)
|
|
146
159
|
dds.deseq2()
|
|
147
|
-
dds
|
|
148
|
-
dds.varm["dispersions"] = dds.varm["dispersions"].round(3)
|
|
149
|
-
|
|
150
|
-
stat_res = DeseqStats(dds, n_cpus=n_cpus, quiet=True)
|
|
160
|
+
stat_res = DeseqStats(dds, inference=inference, quiet=True, cooks_filter=False)
|
|
151
161
|
stat_res.summary(quiet=True)
|
|
152
|
-
stat_res.lfc_shrink()
|
|
162
|
+
stat_res.lfc_shrink("condition_B_vs_A")
|
|
153
163
|
results[cell_type] = stat_res.results_df
|
|
154
164
|
|
|
155
165
|
return results
|
|
156
166
|
|
|
157
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
|
+
|
|
158
185
|
def _de_analysis(
|
|
159
|
-
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,
|
|
160
195
|
) -> tuple[Series, dict[str, [str]] :, DataFrame | None]:
|
|
161
196
|
logger.info("Starting DE analysis")
|
|
162
|
-
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)
|
|
163
198
|
optimization_results = None
|
|
164
199
|
|
|
165
200
|
if optimize_cutoffs:
|
|
@@ -188,6 +223,9 @@ def _get_marker_genes(deseq_results, logfc, p, pseudo_count_sig):
|
|
|
188
223
|
|
|
189
224
|
markers = optimal_condition_matrix.index
|
|
190
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)
|
|
191
229
|
return markers, marker_genes_per_cell_type
|
|
192
230
|
|
|
193
231
|
|
|
@@ -248,6 +286,7 @@ def build_rectangle_signatures(
|
|
|
248
286
|
sample_size: int = 1500,
|
|
249
287
|
n_cpus: int = None,
|
|
250
288
|
run: int = 0,
|
|
289
|
+
gene_expression_threshold=0.5,
|
|
251
290
|
) -> RectangleSignatureResult:
|
|
252
291
|
r"""Builds rectangle signatures based on single-cell count data and annotations.
|
|
253
292
|
|
|
@@ -277,11 +316,16 @@ def build_rectangle_signatures(
|
|
|
277
316
|
The number of cpus to use for the DE analysis. Defaults to the number of cpus available.
|
|
278
317
|
run
|
|
279
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
|
|
280
321
|
|
|
281
322
|
Returns
|
|
282
323
|
-------
|
|
283
324
|
The result of the rectangle signature analysis which is of type RectangleSignatureResult.
|
|
284
325
|
"""
|
|
326
|
+
annotations = adata.obs[cell_type_col]
|
|
327
|
+
adata = adata[:, adata.X.sum(axis=0) > len(annotations.value_counts())]
|
|
328
|
+
|
|
285
329
|
if bulks is not None:
|
|
286
330
|
genes = list(set(bulks.columns) & set(adata.var_names))
|
|
287
331
|
genes = sorted(genes)
|
|
@@ -289,7 +333,6 @@ def build_rectangle_signatures(
|
|
|
289
333
|
logger.info(f"Using {len(genes)} common genes between bulks and single-cell data")
|
|
290
334
|
adata = adata[:, genes]
|
|
291
335
|
|
|
292
|
-
annotations = adata.obs[cell_type_col]
|
|
293
336
|
if subsample:
|
|
294
337
|
annotations = _even(annotations, sample_size, run)
|
|
295
338
|
adata = adata[annotations.index]
|
|
@@ -309,7 +352,7 @@ def build_rectangle_signatures(
|
|
|
309
352
|
m_rna_biasfactors = _create_bias_factors(pseudo_sig_counts, sc_counts, annotations)
|
|
310
353
|
|
|
311
354
|
marker_genes, marker_genes_per_cell_type, optimization_result = _de_analysis(
|
|
312
|
-
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
|
|
313
356
|
)
|
|
314
357
|
pseudo_sig_cpm = _convert_to_cpm(pseudo_sig_counts)
|
|
315
358
|
logger.info("Starting rectangle cluster analysis")
|
|
@@ -329,7 +372,13 @@ def build_rectangle_signatures(
|
|
|
329
372
|
|
|
330
373
|
clustered_biasfact = _create_bias_factors(clustered_signature, sc_counts, clustered_annotations)
|
|
331
374
|
clustered_genes, clustered_marker_genes_per_cell_type, _ = _de_analysis(
|
|
332
|
-
clustered_signature,
|
|
375
|
+
clustered_signature,
|
|
376
|
+
sc_counts,
|
|
377
|
+
clustered_annotations,
|
|
378
|
+
p,
|
|
379
|
+
lfc,
|
|
380
|
+
False,
|
|
381
|
+
gene_expression_threshold=gene_expression_threshold,
|
|
333
382
|
)
|
|
334
383
|
clustered_signature = _convert_to_cpm(clustered_signature)
|
|
335
384
|
return RectangleSignatureResult(
|
|
@@ -361,9 +410,9 @@ def _create_pseudo_count_sig(sc_counts: np.ndarray, annotations: pd.Series, var_
|
|
|
361
410
|
def _optimize_parameters(
|
|
362
411
|
sc_data: pd.DataFrame, annotations: pd.Series, pseudo_signature_counts: pd.DataFrame, de_results, genes=None
|
|
363
412
|
) -> pd.DataFrame:
|
|
364
|
-
#
|
|
365
|
-
lfcs = [x / 100 for x in range(
|
|
366
|
-
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)]
|
|
367
416
|
|
|
368
417
|
results = []
|
|
369
418
|
logger.info("generating pseudo bulks")
|
|
@@ -380,6 +429,7 @@ def _optimize_parameters(
|
|
|
380
429
|
logger.error(f"Error in assessing parameter fit for p={p}, lfc={lfc}: {e}")
|
|
381
430
|
|
|
382
431
|
results_df = pd.DataFrame(results)
|
|
432
|
+
# best results first
|
|
383
433
|
results_df = results_df.sort_values(by=["pearson_r", "rmse"], ascending=[False, True])
|
|
384
434
|
|
|
385
435
|
return results_df
|
|
@@ -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,6 +106,7 @@ 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
111
|
most_recent_signatures = signatures
|
|
114
112
|
cell_fractions = deconvolution(signatures, bulks, correct_mrna_bias, n_cpus)
|
|
@@ -117,7 +115,8 @@ def rectangle_consens(
|
|
|
117
115
|
signatures.unkn_gene_corr = unkn_gene_corr
|
|
118
116
|
rectangle_signature_results.append(signatures)
|
|
119
117
|
consensus_estimations = pd.concat(estimations).groupby(level=0).median()
|
|
120
|
-
|
|
118
|
+
|
|
119
|
+
# normalize the estimations to 1, needed for the consensus
|
|
121
120
|
consensus_estimations = consensus_estimations.div(consensus_estimations.sum(axis=1), axis=0)
|
|
122
121
|
consensus_results = ConsensusResult(estimations, rectangle_signature_results)
|
|
123
122
|
return consensus_estimations, most_recent_signatures, consensus_results
|
|
@@ -135,6 +134,7 @@ def rectangle(
|
|
|
135
134
|
p=0.015,
|
|
136
135
|
lfc=1.5,
|
|
137
136
|
n_cpus: int = None,
|
|
137
|
+
gene_expression_threshold=0.5,
|
|
138
138
|
) -> tuple[DataFrame, RectangleSignatureResult]:
|
|
139
139
|
r"""All in one deconvolution method. Creates signatures and deconvolutes the bulk data. Has options for subsampling and consensus runs.
|
|
140
140
|
|
|
@@ -160,6 +160,8 @@ def rectangle(
|
|
|
160
160
|
The number of cpus to use for the DE analysis. None value takes all cpus available.
|
|
161
161
|
correct_mrna_bias : bool
|
|
162
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.
|
|
163
165
|
|
|
164
166
|
Returns
|
|
165
167
|
-------
|
|
@@ -180,6 +182,7 @@ def rectangle(
|
|
|
180
182
|
p=p,
|
|
181
183
|
lfc=lfc,
|
|
182
184
|
n_cpus=n_cpus,
|
|
185
|
+
gene_expression_threshold=gene_expression_threshold,
|
|
183
186
|
)
|
|
184
187
|
return estimations, signatures
|
|
185
188
|
|
|
@@ -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,7 +15,7 @@ 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[:, :
|
|
18
|
+
sc_data = sc_data.iloc[:, :2000]
|
|
19
19
|
sc_data_adata = AnnData(sc_data, obs=annotations.to_frame(name="cell_type"))
|
|
20
20
|
|
|
21
21
|
result = rectanglepy.rectangle(sc_data_adata, bulks)
|
|
@@ -32,7 +32,7 @@ def test_rectangle_consensus():
|
|
|
32
32
|
sc_data_adata = AnnData(sc_data, obs=annotations.to_frame(name="cell_type"))
|
|
33
33
|
|
|
34
34
|
result = rectanglepy.rectangle_consens(
|
|
35
|
-
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
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
assert isinstance(result[0], pd.DataFrame)
|
|
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
|
|
File without changes
|
|
File without changes
|
{rectanglepy-0.1.8 → 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
|