rectanglepy 0.1.8__tar.gz → 0.4.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.8 → rectanglepy-0.4.0}/.bumpversion.cfg +1 -1
  2. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/workflows/build.yaml +1 -1
  3. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/workflows/test.yaml +1 -1
  4. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/PKG-INFO +4 -3
  5. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/pyproject.toml +4 -3
  6. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/pp/create_signature.py +85 -35
  7. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/rectangle.py +10 -7
  8. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/tl/deconvolution.py +13 -15
  9. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/test_pp.py +17 -7
  10. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/test_rectangle.py +2 -2
  11. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.cruft.json +0 -0
  12. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.editorconfig +0 -0
  13. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  15. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/workflows/release.yaml +0 -0
  18. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.github/workflows/release_testpypi.yaml +0 -0
  19. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.gitignore +0 -0
  20. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.pre-commit-config.yaml +0 -0
  21. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/.readthedocs.yaml +0 -0
  22. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/CHANGELOG.md +0 -0
  23. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/LICENSE +0 -0
  24. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/README.md +0 -0
  25. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/Makefile +0 -0
  26. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/_static/.gitkeep +0 -0
  27. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/_templates/.gitkeep +0 -0
  28. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/_templates/autosummary/class.rst +0 -0
  29. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/api.md +0 -0
  30. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/changelog.md +0 -0
  31. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/conf.py +0 -0
  32. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/contributing.md +0 -0
  33. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/extensions/typed_returns.py +0 -0
  34. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/index.md +0 -0
  35. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/make.bat +0 -0
  36. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/notebooks/example.ipynb +0 -0
  37. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/references.bib +0 -0
  38. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/docs/references.md +0 -0
  39. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/__init__.py +0 -0
  40. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/data/hao1_annotations_small.zip +0 -0
  41. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/data/hao1_counts_small.zip +0 -0
  42. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/data/small_fino_bulks.zip +0 -0
  43. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/pp/__init__.py +0 -0
  44. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/pp/rectangle_signature.py +0 -0
  45. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/src/rectanglepy/tl/__init__.py +0 -0
  46. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/TIL10_signature.txt +0 -0
  47. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/bulk_small.csv +0 -0
  48. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/cell_annotations_small.txt +0 -0
  49. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/quanTIseq_SimRNAseq_mixture_smaller.csv +0 -0
  50. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/quanTIseq_SimRNAseq_read_fractions_small.txt +0 -0
  51. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/sc_object_small.csv +0 -0
  52. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/data/signature_hao1.csv +0 -0
  53. {rectanglepy-0.1.8 → rectanglepy-0.4.0}/tests/test_tl.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.1.8
2
+ current_version = 0.4.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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rectanglepy
3
- Version: 0.1.8
3
+ Version: 0.4.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.1
37
- 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
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.1.8"
7
+ version = "0.4.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.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
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, 200)
35
- range_minimum = 30
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["pvalue"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
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(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]:
138
132
  results = {}
139
- count_df = countsig[countsig.sum(axis=1) > 0].T
140
- 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
141
148
  logger.info(f"Running DE analysis for {cell_type}")
142
- condition = np.zeros(len(countsig.columns))
143
- condition[i] = 1
144
- clinical_df = pd.DataFrame({"condition": condition}, index=countsig.columns)
145
- 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
+ )
146
159
  dds.deseq2()
147
- dds.varm["LFC"] = dds.varm["LFC"].round(4)
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, 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,
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, 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,
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
- # if there are many cell types we relax the cutoffs
365
- lfcs = [x / 100 for x in range(140, 200, 10)]
366
- 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)]
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
- # normalize the estimations to 1
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 = 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
@@ -315,9 +311,11 @@ def correct_for_unknown_cell_content(
315
311
  if estimates.sum() == 0:
316
312
  estimates_fix = estimates
317
313
  # analysis fails if all cell fractions are zero, so we set the unknown cell content to ß
318
- estimates_fix.loc["Unknown"] = 0
314
+ estimates_fix["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,11 +328,11 @@ 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
337
335
  estimates_fix = estimates / estimates.sum() * (1 - ukn_cc)
338
- estimates_fix.loc["Unknown"] = abs(ukn_cc)
336
+ estimates_fix["Unknown"] = abs(ukn_cc)
339
337
 
340
338
  return estimates_fix
@@ -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,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[:, :500]
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.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
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