rectanglepy 0.0.39__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ from .create_signature import build_rectangle_signatures
2
+ from .rectangle_signature import RectangleSignatureResult
3
+
4
+ __all__ = ["build_rectangle_signatures", "RectangleSignatureResult"]
@@ -0,0 +1,456 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from anndata import AnnData
4
+ from loguru import logger
5
+ from pandas import DataFrame, Series
6
+ from pydeseq2.dds import DeseqDataSet
7
+ from pydeseq2.ds import DeseqStats
8
+ from scipy.cluster.hierarchy import fcluster, linkage
9
+ from scipy.stats import pearsonr
10
+ from sklearn.metrics import silhouette_score
11
+
12
+ from rectanglepy.tl.deconvolution import solve_qp
13
+
14
+ from .rectangle_signature import RectangleSignatureResult
15
+
16
+
17
+ def _convert_to_cpm(count_sc_data):
18
+ return count_sc_data * 1e6 / np.sum(count_sc_data)
19
+
20
+
21
+ def _create_condition_number_matrix(de_adjusted, pseudo_signature: pd.DataFrame, max_gene_number: int) -> pd.DataFrame:
22
+ genes = set()
23
+ for annotation in de_adjusted:
24
+ genes.update(_find_signature_genes(max_gene_number, de_adjusted[annotation]))
25
+ sliced_sc_data = pseudo_signature.loc[list(genes)]
26
+ return sliced_sc_data
27
+
28
+
29
+ def _create_condition_number_matrices(de_adjusted, pseudo_signature):
30
+ condition_number_matrices = []
31
+ de_adjusted_lengths = {annotation: len(de_adjusted[annotation]) for annotation in de_adjusted}
32
+ longest_de_analysis = max(de_adjusted_lengths.values())
33
+
34
+ loop_range = min(longest_de_analysis, 200)
35
+ range_minimum = 30
36
+
37
+ if loop_range < range_minimum:
38
+ range_minimum = 8
39
+
40
+ for i in range(range_minimum, loop_range):
41
+ condition_number_matrices.append(_create_condition_number_matrix(de_adjusted, pseudo_signature, i))
42
+
43
+ return condition_number_matrices, range_minimum
44
+
45
+
46
+ def _find_signature_genes(number_of_genes, de_result):
47
+ result_len = len(de_result)
48
+ if result_len > 0:
49
+ number_of_genes = min(number_of_genes, result_len)
50
+ de_result = de_result.sort_values(by=["log2_fc"], ascending=False)
51
+ return de_result["gene"][0:number_of_genes].values
52
+ return []
53
+
54
+
55
+ def _create_linkage_matrix(signature: pd.DataFrame):
56
+ method = "complete"
57
+ metric = "euclidean"
58
+ return linkage((np.log(signature + 1)).T, method=method, metric=metric)
59
+
60
+
61
+ def _calculate_silhouette_scores(signature, linkage_matrix, cluster_range):
62
+ scores = []
63
+ for num_clust in cluster_range:
64
+ scores.append(silhouette_score(signature, fcluster(linkage_matrix, t=num_clust, criterion="maxclust")))
65
+ return scores
66
+
67
+
68
+ def _calculate_cluster_range(number_of_cell_types: int) -> tuple[int, int]:
69
+ cluster_factor = 3
70
+ if number_of_cell_types > 12:
71
+ 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
+ min_number_clusters = max(
77
+ 3, number_of_cell_types - cluster_factor
78
+ ) # we don't want to cluster too many cell types together
79
+ max_number_clusters = number_of_cell_types - 1 # we want to have at least one cluster wih multiple cell types
80
+ return min_number_clusters, max_number_clusters
81
+
82
+
83
+ def _create_fclusters(signature: pd.DataFrame, linkage_matrix) -> list[int]:
84
+ min_number_clusters, max_number_clusters = _calculate_cluster_range(len(signature.columns))
85
+ cluster_range = range(min_number_clusters, max_number_clusters)
86
+
87
+ scores = _calculate_silhouette_scores((np.log(signature + 1)).T, linkage_matrix, cluster_range)
88
+ cluster_number = scores.index(max(scores)) + min_number_clusters
89
+ clusters = fcluster(linkage_matrix, criterion="maxclust", t=cluster_number)
90
+
91
+ if len(set(clusters)) == 1:
92
+ # default clustering clustered all cell types in same cluster, fallback to distance metric
93
+ distance = linkage_matrix[0][2]
94
+ clusters = fcluster(linkage_matrix, criterion="distance", t=distance)
95
+
96
+ return list(clusters)
97
+
98
+
99
+ def _get_fcluster_assignments(fclusters: list[int], signature_columns: pd.Index) -> list[int | str]:
100
+ assignments = []
101
+ clusters = list(fclusters)
102
+ for cluster, cell in zip(fclusters, signature_columns):
103
+ if clusters.count(cluster) > 1:
104
+ assignments.append(cluster)
105
+ else:
106
+ assignments.append(cell)
107
+ return assignments
108
+
109
+
110
+ def _create_annotations_from_cluster_labels(labels, annotations, signature):
111
+ label_dict = dict(zip(signature.columns, labels))
112
+ cluster_annotations = [str(label_dict[x]) for x in annotations]
113
+ return pd.Series(cluster_annotations, index=annotations.index)
114
+
115
+
116
+ def _filter_de_analysis_results(de_analysis_result, p, logfc):
117
+ min_log2FC = logfc
118
+ max_p = p
119
+ de_analysis_result["log2_fc"] = de_analysis_result["log2FoldChange"]
120
+ de_analysis_result["gene"] = de_analysis_result.index
121
+ adjusted_result = de_analysis_result[
122
+ (de_analysis_result["pvalue"] < max_p) & (de_analysis_result["log2_fc"] > min_log2FC)
123
+ ]
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
+
132
+ return adjusted_result
133
+
134
+
135
+ def _run_deseq2(countsig: pd.DataFrame, n_cpus: int = None) -> dict[str | int, pd.DataFrame]:
136
+ results = {}
137
+ count_df = countsig[countsig.sum(axis=1) > 0].T
138
+ for i, cell_type in enumerate(countsig.columns):
139
+ 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)
144
+ 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)
149
+ stat_res.summary(quiet=True)
150
+ stat_res.lfc_shrink()
151
+ results[cell_type] = stat_res.results_df
152
+
153
+ return results
154
+
155
+
156
+ def _de_analysis(
157
+ pseudo_count_sig, sc_data, annotations, p, lfc, optimize_cutoffs: bool, n_cpus: int = None, genes=None
158
+ ) -> tuple[Series, dict[str, int] :, DataFrame | None]:
159
+ logger.info("Starting DE analysis")
160
+ deseq_results = _run_deseq2(pseudo_count_sig, n_cpus)
161
+ optimization_results = None
162
+
163
+ if optimize_cutoffs:
164
+ logger.info("Optimizing cutoff parameters p and lfc")
165
+ optimization_results = _optimize_parameters(sc_data, annotations, pseudo_count_sig, deseq_results, genes)
166
+ p, lfc = optimization_results.iloc[0, 0:2]
167
+ logger.info(f"Optimization done\n Best cutoffs p: {p} and lfc: {lfc}")
168
+
169
+ markers, marker_genes_per_cell_type = _get_marker_genes(deseq_results, lfc, p, pseudo_count_sig)
170
+ return pd.Series(markers), marker_genes_per_cell_type, optimization_results
171
+
172
+
173
+ def _get_marker_genes(deseq_results, logfc, p, pseudo_count_sig):
174
+ de_analysis_adjusted = {
175
+ annotation: _filter_de_analysis_results(result, p, logfc) for annotation, result in deseq_results.items()
176
+ }
177
+
178
+ pseudo_cpm_sig = _convert_to_cpm(pseudo_count_sig)
179
+ condition_number_matrices, range_minimum = _create_condition_number_matrices(de_analysis_adjusted, pseudo_cpm_sig)
180
+ condition_numbers = [np.linalg.cond(np.linalg.qr(x)[1], 1) for x in condition_number_matrices]
181
+ optimal_condition_index = condition_numbers.index(min(condition_numbers))
182
+
183
+ optimal_condition_number = optimal_condition_index + range_minimum
184
+ logger.info(f"Optimal condition number: {optimal_condition_number}")
185
+ optimal_condition_matrix = condition_number_matrices[optimal_condition_index]
186
+
187
+ markers = optimal_condition_matrix.index
188
+ marker_genes_per_cell_type = _count_marker_genes_per_cell_type(de_analysis_adjusted, optimal_condition_number)
189
+ return markers, marker_genes_per_cell_type
190
+
191
+
192
+ def _count_marker_genes_per_cell_type(de_analysis_adjusted, optimal_condition_number: int) -> dict[str, int]:
193
+ marker_genes_per_cell_type = {}
194
+ for annotation, result in de_analysis_adjusted.items():
195
+ marker_genes_per_cell_type[annotation] = min(len(result), optimal_condition_number)
196
+ logger.info(f"Number of marker genes per cell type: {str(marker_genes_per_cell_type)}")
197
+ return marker_genes_per_cell_type
198
+
199
+
200
+ def _create_bias_factors(countsig: pd.DataFrame, sc_counts: pd.DataFrame | np.ndarray, annotations) -> pd.Series:
201
+ number_of_expressed_genes = (sc_counts > 0).sum(axis=0)
202
+ number_of_expressed_genes = np.squeeze(np.asarray(number_of_expressed_genes))
203
+ bias_factors = [
204
+ np.mean(number_of_expressed_genes[[annotation == x for x in annotations]]) for annotation in countsig.columns
205
+ ]
206
+ mRNA_bias = bias_factors / np.min(bias_factors)
207
+ return pd.Series(mRNA_bias, index=countsig.columns)
208
+
209
+
210
+ def _create_clustered_data(
211
+ pseudo_sig_cpm: pd.DataFrame, marker_genes, annotations: pd.Series, sc_counts: pd.DataFrame | np.ndarray, genes
212
+ ) -> (pd.DataFrame, pd.Series, list[int | str]):
213
+ if len(pseudo_sig_cpm.columns) < 5:
214
+ logger.info("Not enough cell types to perform clustering, returning direct rectangle signature")
215
+ return pd.DataFrame(), pd.Series(), []
216
+
217
+ linkage_matrix = _create_linkage_matrix(pseudo_sig_cpm.loc[marker_genes])
218
+
219
+ clusters = _create_fclusters(pseudo_sig_cpm.loc[marker_genes], linkage_matrix)
220
+
221
+ if len(set(clusters)) <= 2:
222
+ logger.info("Not enough clusters to perform clustered signature analysis, returning direct rectangle signature")
223
+ return pd.DataFrame(), pd.Series(), []
224
+
225
+ logger.info("Starting clustered analysis")
226
+
227
+ assignments = _get_fcluster_assignments(clusters, pseudo_sig_cpm.columns)
228
+ clustered_annotations = _create_annotations_from_cluster_labels(assignments, annotations, pseudo_sig_cpm)
229
+
230
+ clustered_signature = _create_pseudo_count_sig(sc_counts, clustered_annotations, genes)
231
+
232
+ return clustered_signature, clustered_annotations, assignments
233
+
234
+
235
+ def build_rectangle_signatures(
236
+ adata: AnnData,
237
+ cell_type_col: str = "cell_type",
238
+ bulks: pd.DataFrame = None,
239
+ *,
240
+ optimize_cutoffs=True,
241
+ layer: str = None,
242
+ raw: bool = False,
243
+ p=0.015,
244
+ lfc=1.5,
245
+ subsample: bool = False,
246
+ sample_size: int = 1500,
247
+ n_cpus: int = None,
248
+ ) -> RectangleSignatureResult:
249
+ r"""Builds rectangle signatures based on single-cell count data and annotations.
250
+
251
+ Parameters
252
+ ----------
253
+ adata
254
+ The single-cell count data as a DataFrame. DataFrame must have the genes as index and cell identifier as columns. Each entry should be in raw counts.
255
+ bulks
256
+ The bulk data as a DataFrame. DataFrame must have the bulk identifier as index and the genes as columns. Each entry should be in transcripts per million (TPM).
257
+ cell_type_col
258
+ The annotations corresponding to the single-cell count data. Series data should have the cell identifier as index and the annotations as values.
259
+ layer
260
+ The Anndata layer to use for the single-cell data. Defaults to None.
261
+ raw
262
+ A flag indicating whether to use the raw Anndata data. Defaults to False.
263
+ subsample : bool
264
+ A flag indicating whether to balance the single-cell data. Defaults to False.
265
+ sample_size : int
266
+ The number of cells to balance the single-cell data to. Defaults to 1500. If cell number is less than this number it takes the original number of cells.
267
+ optimize_cutoffs
268
+ Indicates whether to optimize the p-value and log fold change cutoffs using gridsearch. Defaults to True.
269
+ p
270
+ The p-value threshold for the DE analysis (only used if optimize_cutoffs is False).
271
+ lfc
272
+ The log fold change threshold for the DE analysis (only used if optimize_cutoffs is False).
273
+ n_cpus
274
+ The number of cpus to use for the DE analysis. Defaults to the number of cpus available.
275
+
276
+ Returns
277
+ -------
278
+ The result of the rectangle signature analysis which is of type RectangleSignatureResult.
279
+ """
280
+ if bulks is not None:
281
+ genes = list(set(bulks.columns) & set(adata.var_names))
282
+ genes = sorted(genes)
283
+ assert len(genes) > 0, "No common genes between bulks and single-cell data"
284
+ logger.info(f"Using {len(genes)} common genes between bulks and single-cell data")
285
+ adata = adata[:, genes]
286
+
287
+ annotations = adata.obs[cell_type_col]
288
+ if subsample:
289
+ annotations = _even(annotations, sample_size)
290
+ adata = adata[annotations.index]
291
+
292
+ if layer is not None:
293
+ sc_counts = adata.layers[layer]
294
+ elif raw:
295
+ sc_counts = adata.raw.X
296
+ else:
297
+ sc_counts = adata.X
298
+
299
+ sc_counts = sc_counts.T
300
+
301
+ genes = adata.var_names
302
+
303
+ pseudo_sig_counts = _create_pseudo_count_sig(sc_counts, annotations, genes)
304
+ m_rna_biasfactors = _create_bias_factors(pseudo_sig_counts, sc_counts, annotations)
305
+
306
+ marker_genes, marker_genes_per_cell_type, optimization_result = _de_analysis(
307
+ pseudo_sig_counts, sc_counts, annotations, p, lfc, optimize_cutoffs, n_cpus, genes
308
+ )
309
+ pseudo_sig_cpm = _convert_to_cpm(pseudo_sig_counts)
310
+ logger.info("Starting rectangle cluster analysis")
311
+
312
+ clustered_signature, clustered_annotations, assignments = _create_clustered_data(
313
+ pseudo_sig_cpm, marker_genes, annotations, sc_counts, genes
314
+ )
315
+
316
+ if len(clustered_signature) == 0:
317
+ return RectangleSignatureResult(
318
+ signature_genes=marker_genes,
319
+ pseudobulk_sig_cpm=pseudo_sig_cpm,
320
+ bias_factors=m_rna_biasfactors,
321
+ marker_genes_per_cell_type=marker_genes_per_cell_type,
322
+ optimization_result=optimization_result,
323
+ )
324
+
325
+ clustered_biasfact = _create_bias_factors(clustered_signature, sc_counts, clustered_annotations)
326
+ clustered_genes = _de_analysis(clustered_signature, sc_counts, clustered_annotations, p, lfc, False)[0]
327
+ clustered_signature = _convert_to_cpm(clustered_signature)
328
+ return RectangleSignatureResult(
329
+ signature_genes=marker_genes,
330
+ pseudobulk_sig_cpm=pseudo_sig_cpm,
331
+ bias_factors=m_rna_biasfactors,
332
+ marker_genes_per_cell_type=marker_genes_per_cell_type,
333
+ optimization_result=optimization_result,
334
+ clustered_pseudobulk_sig_cpm=clustered_signature,
335
+ clustered_signature_genes=clustered_genes,
336
+ clustered_bias_factors=clustered_biasfact,
337
+ cluster_assignments=assignments,
338
+ )
339
+
340
+
341
+ def _create_pseudo_count_sig(sc_counts: np.ndarray, annotations: pd.Series, var_names) -> pd.DataFrame:
342
+ unique_labels, label_indices = np.unique(annotations, return_inverse=True)
343
+ grouped_sum = np.zeros((len(unique_labels), sc_counts.shape[0]))
344
+ for i, _label in enumerate(unique_labels):
345
+ label_columns = label_indices == i
346
+ grouped_sum[i, :] = np.sum(sc_counts.T[label_columns, :], axis=0)
347
+ grouped_sum = grouped_sum.T
348
+
349
+ grouped_sum = pd.DataFrame(grouped_sum, index=var_names, columns=unique_labels).astype(int)
350
+ return grouped_sum
351
+
352
+
353
+ def _optimize_parameters(
354
+ sc_data: pd.DataFrame, annotations: pd.Series, pseudo_signature_counts: pd.DataFrame, de_results, genes=None
355
+ ) -> pd.DataFrame:
356
+ # if there are many cell types we relax the cutoffs
357
+ lfcs = [x / 100 for x in range(140, 200, 10)]
358
+ ps = [x / 1000 for x in range(15, 20, 1)]
359
+
360
+ results = []
361
+ logger.info("generating pseudo bulks")
362
+ bulks, real_fractions = _generate_pseudo_bulks(sc_data, annotations, genes)
363
+ for p in ps:
364
+ for lfc in lfcs:
365
+ rmse, pearson_r = _assess_parameter_fit(lfc, p, bulks, real_fractions, pseudo_signature_counts, de_results)
366
+ logger.info(f"RMSE:{rmse}, Pearson R:{pearson_r} for p={p}, lfc={lfc}")
367
+ results.append({"p": p, "lfc": lfc, "rmse": rmse, "pearson_r": pearson_r})
368
+
369
+ results_df = pd.DataFrame(results)
370
+ results_df = results_df.sort_values(by=["pearson_r", "rmse"], ascending=[False, True])
371
+
372
+ return results_df
373
+
374
+
375
+ def _assess_parameter_fit(
376
+ lfc: float, p: float, bulks, real_fractions, pseudo_signature_counts, de_results
377
+ ) -> (float, float):
378
+ estimated_fractions = _generate_estimated_fractions(pseudo_signature_counts, bulks, p, lfc, de_results)
379
+
380
+ real_fractions = real_fractions.sort_index()
381
+
382
+ estimated_fractions = estimated_fractions.sort_index()
383
+
384
+ rsme = np.sqrt(np.mean((real_fractions - estimated_fractions) ** 2))
385
+ pearson_r = pearsonr(real_fractions.values.flatten(), estimated_fractions.values.flatten())[0]
386
+ return rsme, pearson_r
387
+
388
+
389
+ def _generate_pseudo_bulks(sc_data, annotations, genes=None):
390
+ number_of_bulks = 50
391
+ split_size = 50
392
+ bulks = []
393
+ real_fractions = []
394
+ np.random.seed(42)
395
+ for _ in range(number_of_bulks):
396
+ indices = []
397
+ cell_numbers = []
398
+ for annotation in annotations.unique():
399
+ annotation_indices = annotations[annotations == annotation].index
400
+ upper_limit = min(split_size, len(annotation_indices))
401
+ number_of_cells = np.random.randint(0, upper_limit)
402
+ cell_numbers.append(number_of_cells)
403
+ random_annotations = np.random.choice(annotation_indices, number_of_cells, replace=False)
404
+ random_annotations_indices = [annotations.index.get_loc(x) for x in random_annotations]
405
+ indices.extend(random_annotations_indices)
406
+
407
+ random_cells = sc_data[:, indices] # Select columns by indices for ndarray
408
+ random_cells_sum = random_cells.sum(axis=1)
409
+ random_cells_sum = np.squeeze(np.asarray(random_cells_sum))
410
+
411
+ pseudo_bulk = random_cells_sum * 1e6 / np.sum(random_cells_sum)
412
+ bulks.append(pseudo_bulk)
413
+
414
+ cell_fractions = np.array(cell_numbers) / np.sum(cell_numbers)
415
+ cell_fractions = pd.Series(cell_fractions, index=annotations.unique())
416
+ real_fractions.append(cell_fractions)
417
+ bulks = pd.DataFrame(bulks).T
418
+ if genes is not None:
419
+ bulks.index = genes
420
+ return bulks, pd.DataFrame(real_fractions).T
421
+
422
+
423
+ def _generate_estimated_fractions(pseudo_bulk_sig, bulks, p, logfc, de_results):
424
+ marker_genes = pd.Series(_get_marker_genes(de_results, logfc, p, pseudo_bulk_sig)[0])
425
+ signature = (pseudo_bulk_sig * 1e6 / np.sum(pseudo_bulk_sig)).loc[marker_genes]
426
+
427
+ estimated_fractions = bulks.apply(lambda x: _solve_quadratic_programming(signature, x), axis=0)
428
+ estimated_fractions.index = signature.columns
429
+
430
+ return estimated_fractions
431
+
432
+
433
+ def _solve_quadratic_programming(signature, bulk):
434
+ genes = list(set(signature.index) & set(bulk.index))
435
+ signature = signature.loc[genes].sort_index()
436
+ bulk = bulk.loc[genes].sort_index().astype("double")
437
+
438
+ return solve_qp(signature, bulk)
439
+
440
+
441
+ def _reduce_to_common_genes(bulks: pd.DataFrame, sc_data: pd.DataFrame):
442
+ genes = list(set(bulks.index) & set(sc_data.index))
443
+ sc_data = sc_data.loc[genes].sort_index()
444
+ bulks = bulks.loc[genes].sort_index()
445
+ return bulks, sc_data
446
+
447
+
448
+ def _even(annotations: pd.Series, number: int) -> pd.Series:
449
+ assert number > 0, "Number of cells must be greater than 0"
450
+ annotation_counts = annotations.value_counts()
451
+ selected_cells = []
452
+ for annotation in annotation_counts.index:
453
+ cells = annotations[annotations == annotation].index
454
+ cells = np.random.choice(cells, min(number, len(cells)), replace=False)
455
+ selected_cells.extend(cells)
456
+ return annotations.loc[selected_cells]
@@ -0,0 +1,80 @@
1
+ import pandas as pd
2
+
3
+
4
+ class RectangleSignatureResult:
5
+ """Represents the result of a rectangle signature analysis (Created by the method pp.build_rectangle_signatures).
6
+
7
+ Parameters
8
+ ----------
9
+ signature_genes
10
+ The signature genes as a pd.Series.
11
+ bias_factors
12
+ The mRNA bias factors associated with each cell type.
13
+ pseudobulk_sig_cpm
14
+ The pseudo bulk signature build from the single cell data, contains all genes. Normalized to CPM. Columns are cell types, rows are genes.
15
+ clustered_pseudobulk_sig_cpm
16
+ The clustered pseudo bulk signature build from the single cell data, contains all genes. Normalized to CPM. Columns are cell types, rows are genes.
17
+ clustered_bias_factors
18
+ The bias factors associated with each cell type cluster.
19
+ cluster_assignments
20
+ The assignments of signature cell-types to clusters, as a list of ints or strings. In the same order as the pseudobulk_sig_cpm columns.
21
+ marker_genes_per_cell_type
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
+ optimization_result
24
+ The result of the p lfc cut off optimization, as a pd.DataFrame. Contains the following columns: p, lfc, pearson_r, rsme
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ signature_genes: pd.Series,
30
+ bias_factors: pd.Series,
31
+ pseudobulk_sig_cpm: pd.DataFrame,
32
+ marker_genes_per_cell_type: dict[str, int],
33
+ optimization_result: pd.DataFrame = None,
34
+ clustered_pseudobulk_sig_cpm: pd.DataFrame = None,
35
+ clustered_bias_factors: pd.Series = None,
36
+ clustered_signature_genes: pd.Series = None,
37
+ cluster_assignments: list[int or str] = None,
38
+ ):
39
+ self.signature_genes = signature_genes
40
+ self.bias_factors = bias_factors
41
+ self.pseudobulk_sig_cpm = pseudobulk_sig_cpm
42
+ self.marker_genes_per_cell_type = marker_genes_per_cell_type
43
+ self.optimization_result = optimization_result
44
+ self.clustered_pseudobulk_sig_cpm = clustered_pseudobulk_sig_cpm
45
+ self.clustered_bias_factors = clustered_bias_factors
46
+ self.clustered_signature_genes = clustered_signature_genes
47
+ self.assignments = cluster_assignments
48
+
49
+ def cell_types_with_low_number_of_marker_genes(self) -> list[str]:
50
+ """Returns the cell types with less than threshold marker genes.
51
+
52
+ Returns
53
+ -------
54
+ list[str]: The cell types with less than threshold marker genes.
55
+
56
+ """
57
+ low_annotation_threshold = 30
58
+ return [
59
+ cell_type
60
+ for cell_type, count in self.marker_genes_per_cell_type.items()
61
+ if count < low_annotation_threshold
62
+ ]
63
+
64
+ def get_signature_matrix(self, include_mrna_bias=True) -> pd.DataFrame:
65
+ """Calculates the signature matrix by multiplying the pseudobulk_sig_cpm DataFrame subset by signature_genes and the bias_factors Series.
66
+
67
+ Parameters
68
+ ----------
69
+ include_mrna_bias
70
+ If True, the method includes mRNA bias in the calculation. Defaults to True.
71
+
72
+ Returns
73
+ -------
74
+ pandas.DataFrame: The signature matrix. Where columns are cell types and rows are genes.
75
+
76
+ """
77
+ if include_mrna_bias:
78
+ return self.pseudobulk_sig_cpm.loc[self.signature_genes] * self.bias_factors
79
+ else:
80
+ return self.pseudobulk_sig_cpm.loc[self.signature_genes]
@@ -0,0 +1,117 @@
1
+ import pandas as pd
2
+ from anndata import AnnData
3
+ from loguru import logger
4
+ from pandas import DataFrame
5
+ from pkg_resources import resource_stream
6
+
7
+ from .pp import RectangleSignatureResult, build_rectangle_signatures
8
+ from .tl import deconvolution
9
+
10
+
11
+ def rectangle(
12
+ adata: AnnData,
13
+ bulks: DataFrame,
14
+ cell_type_col: str = "cell_type",
15
+ *,
16
+ layer: str = None,
17
+ raw: bool = False,
18
+ subsample: bool = False,
19
+ sample_size: int = 1500,
20
+ consensus_runs: int = 1,
21
+ correct_mrna_bias: bool = True,
22
+ optimize_cutoffs=True,
23
+ p=0.015,
24
+ lfc=1.5,
25
+ n_cpus: int = None,
26
+ ) -> tuple[DataFrame, RectangleSignatureResult]:
27
+ r"""Builds rectangle signatures based on single-cell count data and annotations.
28
+
29
+ Parameters
30
+ ----------
31
+ adata
32
+ The single-cell count data as a DataFrame. DataFrame must have the genes as index and cell identifier as columns. Each entry should be in raw counts.
33
+ bulks
34
+ The bulk data as a DataFrame. DataFrame must have the bulk identifier as index and the genes as columns. Each entry should be in transcripts per million (TPM).
35
+ cell_type_col
36
+ The annotations corresponding to the single-cell count data. Series data should have the cell identifier as index and the annotations as values.
37
+ layer
38
+ The Anndata layer to use for the single-cell data. Defaults to None.
39
+ raw
40
+ A flag indicating whether to use the raw Anndata data. Defaults to False.
41
+ subsample : bool
42
+ A flag indicating whether to balance the single-cell data. Defaults to False.
43
+ sample_size : int
44
+ The number of cells to balance the single-cell data to. Defaults to 1500. If cell number is less than this number it takes the original number of cells.
45
+ consensus_runs : int
46
+ The number of consensus runs to perform. Defaults to 1 for a singular deconvolution run. Consensus runs are performed by subsampling the single-cell data and running the analysis multiple times. The results are then aggregated.
47
+ optimize_cutoffs
48
+ Indicates whether to optimize the p-value and log fold change cutoffs using gridsearch. Defaults to True.
49
+ p
50
+ The p-value threshold for the DE analysis (only used if optimize_cutoffs is False).
51
+ lfc
52
+ The log fold change threshold for the DE analysis (only used if optimize_cutoffs is False).
53
+ n_cpus
54
+ The number of cpus to use for the DE analysis. Defaults to the number of cpus available.
55
+ correct_mrna_bias : bool
56
+ A flag indicating whether to correct for mRNA bias. Defaults to True.
57
+
58
+ Returns
59
+ -------
60
+ DataFrame : The estimated cell fractions.
61
+ RectangleSignatureResult : The result of the rectangle signature analysis.
62
+ """
63
+ assert isinstance(adata, AnnData), "adata must be an AnnData object"
64
+ assert isinstance(bulks, DataFrame), "bulks must be a DataFrame"
65
+
66
+ if bulks is not None:
67
+ genes = list(set(bulks.columns) & set(adata.var_names))
68
+ genes = sorted(genes)
69
+ adata = adata[:, genes]
70
+ bulks = bulks[genes]
71
+
72
+ if consensus_runs > 1:
73
+ logger.info(f"Running {consensus_runs} consensus runs with subsample size {sample_size}")
74
+ subsample = True
75
+
76
+ estimations = []
77
+ most_recent_signatures = None
78
+
79
+ for _i in range(consensus_runs):
80
+ logger.info(f"Running consensus run {_i + 1} of {consensus_runs}")
81
+ signatures = build_rectangle_signatures(
82
+ adata,
83
+ cell_type_col,
84
+ bulks=bulks,
85
+ optimize_cutoffs=optimize_cutoffs,
86
+ layer=layer,
87
+ raw=raw,
88
+ p=p,
89
+ lfc=lfc,
90
+ n_cpus=n_cpus,
91
+ subsample=subsample,
92
+ sample_size=sample_size,
93
+ )
94
+ cell_fractions = deconvolution(signatures, bulks, correct_mrna_bias, n_cpus)
95
+ estimations.append(cell_fractions)
96
+ most_recent_signatures = signatures
97
+
98
+ return pd.concat(estimations).groupby(level=0).median(), most_recent_signatures
99
+
100
+
101
+ def load_tutorial_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
102
+ """Loads the single-cell count data, annotations, and bulk data from the tutorial.
103
+
104
+ Returns
105
+ -------
106
+ The single-cell count data, annotations, and bulk data.
107
+ """
108
+ with resource_stream(__name__, "data/hao1_annotations_small.csv") as annotations_file:
109
+ annotations = pd.read_csv(annotations_file, index_col=0)["0"]
110
+
111
+ with resource_stream(__name__, "data/hao1_counts_small.csv") as counts_file:
112
+ sc_counts = pd.read_csv(counts_file, index_col=0).astype("int")
113
+
114
+ with resource_stream(__name__, "data/small_fino_bulks.csv") as bulks_file:
115
+ bulks = pd.read_csv(bulks_file, index_col=0)
116
+
117
+ return sc_counts.T, annotations, bulks.T
@@ -0,0 +1,3 @@
1
+ from .deconvolution import deconvolution, solve_qp
2
+
3
+ __all__ = ["deconvolution"]