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.
Files changed (53) hide show
  1. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.bumpversion.cfg +1 -1
  2. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/build.yaml +1 -1
  3. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/test.yaml +1 -1
  4. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/CHANGELOG.md +10 -0
  5. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/PKG-INFO +5 -3
  6. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/pyproject.toml +5 -3
  7. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/create_signature.py +96 -39
  8. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/rectangle_signature.py +4 -0
  9. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/rectangle.py +23 -8
  10. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/tl/deconvolution.py +11 -13
  11. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_pp.py +17 -7
  12. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_rectangle.py +10 -4
  13. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.cruft.json +0 -0
  14. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.editorconfig +0 -0
  15. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  19. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/release.yaml +0 -0
  20. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.github/workflows/release_testpypi.yaml +0 -0
  21. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.gitignore +0 -0
  22. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.pre-commit-config.yaml +0 -0
  23. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/.readthedocs.yaml +0 -0
  24. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/LICENSE +0 -0
  25. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/README.md +0 -0
  26. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/Makefile +0 -0
  27. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_static/.gitkeep +0 -0
  28. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_templates/.gitkeep +0 -0
  29. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/_templates/autosummary/class.rst +0 -0
  30. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/api.md +0 -0
  31. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/changelog.md +0 -0
  32. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/conf.py +0 -0
  33. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/contributing.md +0 -0
  34. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/extensions/typed_returns.py +0 -0
  35. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/index.md +0 -0
  36. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/make.bat +0 -0
  37. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/notebooks/example.ipynb +0 -0
  38. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/references.bib +0 -0
  39. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/docs/references.md +0 -0
  40. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/__init__.py +0 -0
  41. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_annotations_small.zip +0 -0
  42. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/hao1_counts_small.zip +0 -0
  43. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/data/small_fino_bulks.zip +0 -0
  44. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/pp/__init__.py +0 -0
  45. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/src/rectanglepy/tl/__init__.py +0 -0
  46. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/TIL10_signature.txt +0 -0
  47. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/bulk_small.csv +0 -0
  48. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/cell_annotations_small.txt +0 -0
  49. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_mixture_smaller.csv +0 -0
  50. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/quanTIseq_SimRNAseq_read_fractions_small.txt +0 -0
  51. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/sc_object_small.csv +0 -0
  52. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/data/signature_hao1.csv +0 -0
  53. {rectanglepy-0.1.7 → rectanglepy-0.3.0}/tests/test_tl.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.7
2
+ current_version = 0.3.0
3
3
  tag = True
4
4
  commit = True
5
5
 
@@ -1,4 +1,4 @@
1
- name: CI Checks
1
+ name: CI Build
2
2
 
3
3
  on:
4
4
  push:
@@ -1,4 +1,4 @@
1
- name: CI Checks
1
+ name: CI Tests
2
2
 
3
3
  on:
4
4
  push:
@@ -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.1.7
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.1
36
- Requires-Dist: quadprog==0.1.11
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.1.7"
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.1",
23
- "quadprog==0.1.11",
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, 200)
35
- range_minimum = 30
36
+ loop_range = min(longest_de_analysis, 80)
37
+ range_minimum = 20
36
38
 
37
- if loop_range < range_minimum:
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["pvalue"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
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(countsig: pd.DataFrame, n_cpus: int = None) -> dict[str | int, pd.DataFrame]:
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
- count_df = countsig[countsig.sum(axis=1) > 0].T
138
- for i, cell_type in enumerate(countsig.columns):
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 = np.zeros(len(countsig.columns))
141
- condition[i] = 1
142
- clinical_df = pd.DataFrame({"condition": condition}, index=countsig.columns)
143
- dds = DeseqDataSet(counts=count_df, metadata=clinical_df, design_factors="condition", quiet=True, n_cpus=n_cpus)
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.varm["LFC"] = dds.varm["LFC"].round(4)
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, sc_data, annotations, p, lfc, optimize_cutoffs: bool, n_cpus: int = None, genes=None
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, sc_counts, clustered_annotations, p, lfc, False
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
- # if there are many cell types we relax the cutoffs
363
- lfcs = [x / 100 for x in range(140, 200, 10)]
364
- ps = [x / 1000 for x in range(15, 20, 1)]
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
- rmse, pearson_r = _assess_parameter_fit(lfc, p, bulks, real_fractions, pseudo_signature_counts, de_results)
372
- logger.info(f"RMSE:{rmse}, Pearson R:{pearson_r} for p={p}, lfc={lfc}")
373
- results.append({"p": p, "lfc": lfc, "rmse": rmse, "pearson_r": pearson_r})
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 pd.concat(estimations).groupby(level=0).median(), most_recent_signatures, consensus_results
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 = 30
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(averaged_start_fractions.index):
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(averaged_start_fractions[cell_type])
268
+ final_fractions.append(start_fractions[cell_type])
273
269
 
274
- final_fractions = pd.Series(final_fractions, index=averaged_start_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).sum() / (bulk.sum())
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, None)
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.3, 0.5, False, None, adata.var_names)
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
- rs1, rs2, rs3 = _de_analysis(sc_pseudo, adata_sparse.X.T, annotations, 0.3, 0.5, False, None, adata.var_names)
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 30 < len(r1) < 40
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(result[0], pd.DataFrame)
23
- assert isinstance(result[1], RectangleSignatureResult)
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.5, lfc=0.0, consensus_runs=3, sample_size=50
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
- # all bias factors should differ, else there is a problem with the random sampling
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