easydecon 0.1.7__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.
- easydecon/__init__.py +69 -0
- easydecon/_schema.py +365 -0
- easydecon/_validation.py +127 -0
- easydecon/_version.py +3 -0
- easydecon/config.py +24 -0
- easydecon/diagnostics.py +460 -0
- easydecon/easydecon.py +3833 -0
- easydecon/expression_statistics.py +162 -0
- easydecon/extra.py +800 -0
- easydecon/markers.py +2432 -0
- easydecon/modelling.py +441 -0
- easydecon/niche.py +460 -0
- easydecon/obsolote.py +239 -0
- easydecon/refinement.py +494 -0
- easydecon/segmentation.py +193 -0
- easydecon/simulation.py +355 -0
- easydecon-0.1.7.dist-info/METADATA +143 -0
- easydecon-0.1.7.dist-info/RECORD +22 -0
- easydecon-0.1.7.dist-info/WHEEL +5 -0
- easydecon-0.1.7.dist-info/entry_points.txt +2 -0
- easydecon-0.1.7.dist-info/licenses/LICENSE +21 -0
- easydecon-0.1.7.dist-info/top_level.txt +1 -0
easydecon/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Public API for easydecon."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from ._schema import (
|
|
5
|
+
MarkerSchema,
|
|
6
|
+
get_table,
|
|
7
|
+
resolve_marker_columns,
|
|
8
|
+
standardize_marker_dataframe,
|
|
9
|
+
)
|
|
10
|
+
from ._validation import MARKER_ROLE_MODES, UCELL_MARKER_ROLES
|
|
11
|
+
from .config import set_batch_size, set_n_jobs
|
|
12
|
+
from .diagnostics import summarize_easydecon_result, summarize_marker_table
|
|
13
|
+
from .easydecon import (
|
|
14
|
+
assign_clusters_from_df,
|
|
15
|
+
common_markers_gene_expression_and_filter,
|
|
16
|
+
compute_pseudobulk_deseq_markers,
|
|
17
|
+
get_clusters_by_similarity_on_tissue,
|
|
18
|
+
read_markers_dataframe,
|
|
19
|
+
add_df_to_spatialdata,
|
|
20
|
+
)
|
|
21
|
+
from .extra import EasyDeconResult, easydecon_workflow
|
|
22
|
+
from .markers import (
|
|
23
|
+
PreparedMarkers,
|
|
24
|
+
compute_reference_profile_markers,
|
|
25
|
+
prepare_markers,
|
|
26
|
+
select_prepared_markers,
|
|
27
|
+
)
|
|
28
|
+
from .niche import (
|
|
29
|
+
detect_niches_from_easydecon_result,
|
|
30
|
+
detect_spatial_niches_from_posteriors,
|
|
31
|
+
plot_niche_compositions,
|
|
32
|
+
summarize_niche_compositions,
|
|
33
|
+
)
|
|
34
|
+
from .refinement import RefinedGroupResult, refine_group
|
|
35
|
+
|
|
36
|
+
run_easydecon = easydecon_workflow
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__",
|
|
40
|
+
"read_markers_dataframe",
|
|
41
|
+
"common_markers_gene_expression_and_filter",
|
|
42
|
+
"get_clusters_by_similarity_on_tissue",
|
|
43
|
+
"assign_clusters_from_df",
|
|
44
|
+
"add_df_to_spatialdata",
|
|
45
|
+
"compute_pseudobulk_deseq_markers",
|
|
46
|
+
"PreparedMarkers",
|
|
47
|
+
"compute_reference_profile_markers",
|
|
48
|
+
"prepare_markers",
|
|
49
|
+
"select_prepared_markers",
|
|
50
|
+
"set_n_jobs",
|
|
51
|
+
"set_batch_size",
|
|
52
|
+
"easydecon_workflow",
|
|
53
|
+
"run_easydecon",
|
|
54
|
+
"EasyDeconResult",
|
|
55
|
+
"detect_spatial_niches_from_posteriors",
|
|
56
|
+
"detect_niches_from_easydecon_result",
|
|
57
|
+
"summarize_niche_compositions",
|
|
58
|
+
"plot_niche_compositions",
|
|
59
|
+
"MarkerSchema",
|
|
60
|
+
"resolve_marker_columns",
|
|
61
|
+
"standardize_marker_dataframe",
|
|
62
|
+
"get_table",
|
|
63
|
+
"summarize_easydecon_result",
|
|
64
|
+
"summarize_marker_table",
|
|
65
|
+
"RefinedGroupResult",
|
|
66
|
+
"refine_group",
|
|
67
|
+
"UCELL_MARKER_ROLES",
|
|
68
|
+
"MARKER_ROLE_MODES",
|
|
69
|
+
]
|
easydecon/_schema.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Shared schema helpers for marker tables and AnnData-like objects."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from ._validation import UCELL_MARKER_ROLES, format_allowed_values
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class MarkerSchema:
|
|
12
|
+
"""Column names used by an easydecon marker DataFrame."""
|
|
13
|
+
|
|
14
|
+
group_col: str = "group"
|
|
15
|
+
gene_col: str = "names"
|
|
16
|
+
lfc_col: str = "logfoldchanges"
|
|
17
|
+
padj_col: str = "pvals_adj"
|
|
18
|
+
score_col: str = "scores"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
GROUP_ALIASES = [
|
|
22
|
+
"group", "groups", "celltype", "cell_type", "cell type",
|
|
23
|
+
"cluster", "clusters", "annotation", "label", "labels",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
GENE_ALIASES = [
|
|
27
|
+
"names", "name", "gene", "genes", "gene_id", "gene_ids",
|
|
28
|
+
"gene_symbol", "symbol", "feature", "features",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
LFC_ALIASES = [
|
|
32
|
+
"logfoldchanges", "logfoldchange", "log2FoldChange",
|
|
33
|
+
"log2foldchange", "log2fc", "avg_log2FC", "avg_log2fc",
|
|
34
|
+
"avg_logFC", "lfc",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
PADJ_ALIASES = [
|
|
38
|
+
"pvals_adj", "pval_adj", "p_val_adj", "padj",
|
|
39
|
+
"FDR", "fdr", "qval", "q_value", "qvalue",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
SCORE_ALIASES = [
|
|
43
|
+
"scores", "score", "stat", "wald_stat", "statistics",
|
|
44
|
+
"baseMean", "basemean",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_CANONICAL_ALIASES = {
|
|
49
|
+
"group": GROUP_ALIASES,
|
|
50
|
+
"names": GENE_ALIASES,
|
|
51
|
+
"logfoldchanges": LFC_ALIASES,
|
|
52
|
+
"pvals_adj": PADJ_ALIASES,
|
|
53
|
+
"scores": SCORE_ALIASES,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _schema_columns(schema):
|
|
58
|
+
schema = schema or MarkerSchema()
|
|
59
|
+
return {
|
|
60
|
+
"group": schema.group_col,
|
|
61
|
+
"names": schema.gene_col,
|
|
62
|
+
"logfoldchanges": schema.lfc_col,
|
|
63
|
+
"pvals_adj": schema.padj_col,
|
|
64
|
+
"scores": schema.score_col,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_marker_columns(df, schema=None):
|
|
69
|
+
"""Map easydecon's canonical marker columns to columns present in ``df``.
|
|
70
|
+
|
|
71
|
+
Matching is case-insensitive, while the returned values retain the exact
|
|
72
|
+
column labels from the input DataFrame.
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(df, pd.DataFrame):
|
|
75
|
+
raise TypeError("df must be a pandas DataFrame.")
|
|
76
|
+
|
|
77
|
+
columns = list(df.columns)
|
|
78
|
+
schema_columns = _schema_columns(schema)
|
|
79
|
+
resolved = {}
|
|
80
|
+
|
|
81
|
+
for canonical, aliases in _CANONICAL_ALIASES.items():
|
|
82
|
+
# A canonical spelling has priority over every alias or schema hint.
|
|
83
|
+
if canonical in columns:
|
|
84
|
+
resolved[canonical] = canonical
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
candidates = [schema_columns[canonical], canonical, *aliases]
|
|
88
|
+
for candidate in candidates:
|
|
89
|
+
candidate_folded = str(candidate).casefold()
|
|
90
|
+
match = next(
|
|
91
|
+
(
|
|
92
|
+
column
|
|
93
|
+
for column in columns
|
|
94
|
+
if str(column).casefold() == candidate_folded
|
|
95
|
+
),
|
|
96
|
+
None,
|
|
97
|
+
)
|
|
98
|
+
if match is not None:
|
|
99
|
+
resolved[canonical] = match
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
return resolved
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_sort_column(sort_by_column, df, resolved, schema):
|
|
106
|
+
if sort_by_column is None:
|
|
107
|
+
for canonical in ("scores", "logfoldchanges", "pvals_adj"):
|
|
108
|
+
if canonical in df.columns:
|
|
109
|
+
return canonical
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
if sort_by_column in df.columns:
|
|
113
|
+
return sort_by_column
|
|
114
|
+
|
|
115
|
+
requested = str(sort_by_column).casefold()
|
|
116
|
+
for column in df.columns:
|
|
117
|
+
if str(column).casefold() == requested:
|
|
118
|
+
return column
|
|
119
|
+
|
|
120
|
+
for canonical, original in resolved.items():
|
|
121
|
+
if str(original).casefold() == requested and canonical in df.columns:
|
|
122
|
+
return canonical
|
|
123
|
+
|
|
124
|
+
schema_columns = _schema_columns(schema)
|
|
125
|
+
for canonical, aliases in _CANONICAL_ALIASES.items():
|
|
126
|
+
candidates = [canonical, schema_columns[canonical], *aliases]
|
|
127
|
+
if any(str(candidate).casefold() == requested for candidate in candidates):
|
|
128
|
+
if canonical in df.columns:
|
|
129
|
+
return canonical
|
|
130
|
+
|
|
131
|
+
available = ", ".join(map(str, df.columns))
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Could not resolve sort column {sort_by_column!r}. "
|
|
134
|
+
f"Available columns: {available}."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _as_exclusion_set(values):
|
|
139
|
+
if isinstance(values, str):
|
|
140
|
+
return {values}
|
|
141
|
+
return set(values)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_marker_role_column(df, marker_role_column="marker_role"):
|
|
145
|
+
"""Return the actual marker-role column name, matching case-insensitively."""
|
|
146
|
+
if not isinstance(df, pd.DataFrame):
|
|
147
|
+
raise TypeError("df must be a pandas DataFrame.")
|
|
148
|
+
if marker_role_column in df.columns:
|
|
149
|
+
return marker_role_column
|
|
150
|
+
requested = str(marker_role_column).casefold()
|
|
151
|
+
return next(
|
|
152
|
+
(column for column in df.columns if str(column).casefold() == requested),
|
|
153
|
+
None,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def normalize_marker_roles(
|
|
158
|
+
df,
|
|
159
|
+
marker_role_column="marker_role",
|
|
160
|
+
*,
|
|
161
|
+
fill_missing_column=False,
|
|
162
|
+
):
|
|
163
|
+
"""Resolve, normalize, and validate marker-role values.
|
|
164
|
+
|
|
165
|
+
Missing or blank role values become ``"positive"``. Unknown values raise a
|
|
166
|
+
user-facing ``ValueError`` that lists the supported roles.
|
|
167
|
+
"""
|
|
168
|
+
actual_column = resolve_marker_role_column(df, marker_role_column)
|
|
169
|
+
if actual_column is None:
|
|
170
|
+
if fill_missing_column:
|
|
171
|
+
return pd.Series("positive", index=df.index, dtype="object"), None
|
|
172
|
+
return None, None
|
|
173
|
+
|
|
174
|
+
roles = df[actual_column]
|
|
175
|
+
roles = roles.where(~roles.isna(), "positive").astype(str).str.strip().str.casefold()
|
|
176
|
+
roles = roles.replace("", "positive")
|
|
177
|
+
unknown = sorted(set(roles) - set(UCELL_MARKER_ROLES))
|
|
178
|
+
if unknown:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"Unknown marker_role values: "
|
|
181
|
+
f"{unknown}. Allowed values: {format_allowed_values(UCELL_MARKER_ROLES)}."
|
|
182
|
+
)
|
|
183
|
+
return roles, actual_column
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def standardize_marker_dataframe(
|
|
187
|
+
df,
|
|
188
|
+
schema=None,
|
|
189
|
+
gene_universe=None,
|
|
190
|
+
exclude_celltype=None,
|
|
191
|
+
top_n_genes=60,
|
|
192
|
+
sort_by_column=None,
|
|
193
|
+
ascending=False,
|
|
194
|
+
log2fc_min=0.25,
|
|
195
|
+
pval_cutoff=0.05,
|
|
196
|
+
drop_ribosomal=False,
|
|
197
|
+
drop_mitochondrial=False,
|
|
198
|
+
source=None,
|
|
199
|
+
require_group=True,
|
|
200
|
+
require_gene=True,
|
|
201
|
+
copy=True,
|
|
202
|
+
):
|
|
203
|
+
"""Return a consistently named and filtered marker DataFrame."""
|
|
204
|
+
if not isinstance(df, pd.DataFrame):
|
|
205
|
+
raise TypeError("df must be a pandas DataFrame.")
|
|
206
|
+
|
|
207
|
+
result = df.copy() if copy else df
|
|
208
|
+
if result.index.name is not None and result.index.name in result.columns:
|
|
209
|
+
result = result.reset_index(drop=True)
|
|
210
|
+
resolved = resolve_marker_columns(result, schema=schema)
|
|
211
|
+
available = ", ".join(map(str, result.columns))
|
|
212
|
+
|
|
213
|
+
if require_group and "group" not in resolved:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
"Could not resolve a marker group column. "
|
|
216
|
+
f"Available columns: {available}."
|
|
217
|
+
)
|
|
218
|
+
if require_gene and "names" not in resolved:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
"Could not resolve a marker gene column. "
|
|
221
|
+
f"Available columns: {available}."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
rename_columns = {
|
|
225
|
+
original: canonical
|
|
226
|
+
for canonical, original in resolved.items()
|
|
227
|
+
if original != canonical
|
|
228
|
+
}
|
|
229
|
+
result.rename(columns=rename_columns, inplace=True)
|
|
230
|
+
roles, role_column = normalize_marker_roles(result)
|
|
231
|
+
has_marker_role = roles is not None
|
|
232
|
+
if has_marker_role:
|
|
233
|
+
if role_column != "marker_role":
|
|
234
|
+
result.rename(columns={role_column: "marker_role"}, inplace=True)
|
|
235
|
+
result["marker_role"] = roles
|
|
236
|
+
|
|
237
|
+
required_present = [column for column in ("group", "names") if column in result]
|
|
238
|
+
if required_present:
|
|
239
|
+
result.dropna(subset=required_present, inplace=True)
|
|
240
|
+
for column in required_present:
|
|
241
|
+
result[column] = result[column].astype(str)
|
|
242
|
+
|
|
243
|
+
if "logfoldchanges" in result.columns:
|
|
244
|
+
lfc_values = pd.to_numeric(result["logfoldchanges"], errors="coerce")
|
|
245
|
+
if has_marker_role:
|
|
246
|
+
negative_mask = result["marker_role"] == "negative"
|
|
247
|
+
keep_lfc = pd.Series(False, index=result.index)
|
|
248
|
+
keep_lfc.loc[negative_mask] = lfc_values.loc[negative_mask].abs() >= log2fc_min
|
|
249
|
+
keep_lfc.loc[~negative_mask] = lfc_values.loc[~negative_mask] >= log2fc_min
|
|
250
|
+
result = result.loc[keep_lfc]
|
|
251
|
+
else:
|
|
252
|
+
result = result.loc[lfc_values >= log2fc_min]
|
|
253
|
+
if "pvals_adj" in result.columns:
|
|
254
|
+
padj_values = pd.to_numeric(result["pvals_adj"], errors="coerce")
|
|
255
|
+
result = result.loc[padj_values <= pval_cutoff]
|
|
256
|
+
|
|
257
|
+
if "names" in result.columns:
|
|
258
|
+
gene_names = result["names"].str.upper()
|
|
259
|
+
if drop_ribosomal:
|
|
260
|
+
result = result.loc[~gene_names.str.startswith(("RPS", "RPL"))]
|
|
261
|
+
gene_names = result["names"].str.upper()
|
|
262
|
+
if drop_mitochondrial:
|
|
263
|
+
result = result.loc[~gene_names.str.startswith("MT-")]
|
|
264
|
+
|
|
265
|
+
if gene_universe is not None:
|
|
266
|
+
allowed_genes = {str(gene) for gene in gene_universe}
|
|
267
|
+
result = result.loc[result["names"].isin(allowed_genes)]
|
|
268
|
+
|
|
269
|
+
if exclude_celltype is not None and "group" in result.columns:
|
|
270
|
+
excluded_groups = {str(group) for group in _as_exclusion_set(exclude_celltype)}
|
|
271
|
+
result = result.loc[~result["group"].isin(excluded_groups)]
|
|
272
|
+
|
|
273
|
+
sort_column = _resolve_sort_column(sort_by_column, result, resolved, schema)
|
|
274
|
+
if sort_column is not None:
|
|
275
|
+
sort_ascending = (
|
|
276
|
+
True
|
|
277
|
+
if sort_by_column is None and sort_column == "pvals_adj"
|
|
278
|
+
else ascending
|
|
279
|
+
)
|
|
280
|
+
if has_marker_role and sort_column in {"logfoldchanges", "scores"}:
|
|
281
|
+
result["_easydecon_sort_value"] = pd.to_numeric(
|
|
282
|
+
result[sort_column], errors="coerce"
|
|
283
|
+
)
|
|
284
|
+
negative_mask = result["marker_role"] == "negative"
|
|
285
|
+
result.loc[negative_mask, "_easydecon_sort_value"] = result.loc[
|
|
286
|
+
negative_mask, "_easydecon_sort_value"
|
|
287
|
+
].abs()
|
|
288
|
+
result = result.sort_values(
|
|
289
|
+
"_easydecon_sort_value", ascending=sort_ascending, kind="stable"
|
|
290
|
+
).drop(columns="_easydecon_sort_value")
|
|
291
|
+
else:
|
|
292
|
+
result = result.sort_values(sort_column, ascending=sort_ascending, kind="stable")
|
|
293
|
+
|
|
294
|
+
if {"group", "names"}.issubset(result.columns):
|
|
295
|
+
deduplicate_by = ["group", "names", "marker_role"] if has_marker_role else ["group", "names"]
|
|
296
|
+
group_by = ["group", "marker_role"] if has_marker_role else ["group"]
|
|
297
|
+
result = result.drop_duplicates(subset=deduplicate_by, keep="first")
|
|
298
|
+
if top_n_genes is not None:
|
|
299
|
+
groupers = [result[column] for column in group_by]
|
|
300
|
+
result = result.groupby(
|
|
301
|
+
groupers, sort=False, group_keys=False
|
|
302
|
+
).head(top_n_genes)
|
|
303
|
+
result["marker_rank"] = (
|
|
304
|
+
result.groupby([result[column] for column in group_by], sort=False).cumcount() + 1
|
|
305
|
+
)
|
|
306
|
+
result.set_index("group", drop=False, inplace=True)
|
|
307
|
+
else:
|
|
308
|
+
result["marker_rank"] = range(1, len(result) + 1)
|
|
309
|
+
|
|
310
|
+
if source is not None:
|
|
311
|
+
result["marker_source"] = source
|
|
312
|
+
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_table(sdata, bin_size=8, table_key=None, preferred_table_keys=None):
|
|
317
|
+
"""Resolve and validate an AnnData-like table from a container or table."""
|
|
318
|
+
if table_key is not None:
|
|
319
|
+
try:
|
|
320
|
+
table = sdata.tables[table_key]
|
|
321
|
+
except (AttributeError, KeyError, TypeError) as error:
|
|
322
|
+
raise KeyError(f"Table {table_key!r} was not found in sdata.tables.") from error
|
|
323
|
+
else:
|
|
324
|
+
tables = getattr(sdata, "tables", None)
|
|
325
|
+
table = None
|
|
326
|
+
table_found = False
|
|
327
|
+
if tables is not None:
|
|
328
|
+
if isinstance(preferred_table_keys, str):
|
|
329
|
+
keys = [preferred_table_keys]
|
|
330
|
+
else:
|
|
331
|
+
keys = list(preferred_table_keys or [])
|
|
332
|
+
keys.extend(["cell_segmentations", f"square_{bin_size:03}um", "table"])
|
|
333
|
+
for key in dict.fromkeys(keys):
|
|
334
|
+
try:
|
|
335
|
+
table = tables[key]
|
|
336
|
+
table_found = True
|
|
337
|
+
break
|
|
338
|
+
except (KeyError, TypeError):
|
|
339
|
+
continue
|
|
340
|
+
if not table_found:
|
|
341
|
+
table = sdata
|
|
342
|
+
|
|
343
|
+
missing = [name for name in ("obs", "var_names", "X") if not hasattr(table, name)]
|
|
344
|
+
if missing:
|
|
345
|
+
missing_text = ", ".join(missing)
|
|
346
|
+
raise TypeError(
|
|
347
|
+
"Resolved table is not AnnData-like; expected attributes obs, "
|
|
348
|
+
f"var_names, and X. Missing: {missing_text}."
|
|
349
|
+
)
|
|
350
|
+
return table
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
__all__ = [
|
|
354
|
+
"MarkerSchema",
|
|
355
|
+
"GROUP_ALIASES",
|
|
356
|
+
"GENE_ALIASES",
|
|
357
|
+
"LFC_ALIASES",
|
|
358
|
+
"PADJ_ALIASES",
|
|
359
|
+
"SCORE_ALIASES",
|
|
360
|
+
"resolve_marker_columns",
|
|
361
|
+
"resolve_marker_role_column",
|
|
362
|
+
"normalize_marker_roles",
|
|
363
|
+
"standardize_marker_dataframe",
|
|
364
|
+
"get_table",
|
|
365
|
+
]
|
easydecon/_validation.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Shared allowed values and lightweight validation helpers."""
|
|
2
|
+
|
|
3
|
+
from numbers import Real
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MARKER_METHODS = frozenset(
|
|
7
|
+
{
|
|
8
|
+
"auto",
|
|
9
|
+
"existing",
|
|
10
|
+
"scanpy",
|
|
11
|
+
"pydeseq2",
|
|
12
|
+
"deseq2",
|
|
13
|
+
"pseudobulk_deseq2",
|
|
14
|
+
"reference",
|
|
15
|
+
"rctd_like",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
PYDESEQ2_MARKER_METHODS = frozenset(
|
|
19
|
+
{"pydeseq2", "deseq2", "pseudobulk_deseq2"}
|
|
20
|
+
)
|
|
21
|
+
REFERENCE_MARKER_METHODS = frozenset({"reference", "rctd_like"})
|
|
22
|
+
REFERENCE_CONTRASTS = frozenset({"mean_other", "max_other"})
|
|
23
|
+
FILTERING_ALGORITHMS = frozenset({"permutation", "quantile", "nb"})
|
|
24
|
+
PHASE1_OUTPUT_STATS = frozenset({"expression", "minus_log10_p"})
|
|
25
|
+
AGGREGATION_METHODS = frozenset({"sum", "mean", "median", "coverage"})
|
|
26
|
+
SIMILARITY_METHODS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"correlation",
|
|
29
|
+
"cosine",
|
|
30
|
+
"jaccard",
|
|
31
|
+
"overlap",
|
|
32
|
+
"wjaccard",
|
|
33
|
+
"diagnostic",
|
|
34
|
+
"sum",
|
|
35
|
+
"mean",
|
|
36
|
+
"median",
|
|
37
|
+
"euclidean",
|
|
38
|
+
"auc",
|
|
39
|
+
"ucell",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
EVIDENCE_TO_LIKELIHOOD_METHODS = frozenset({"row_normalize", "softmax"})
|
|
43
|
+
ASSIGN_METHODS = frozenset({"max", "zmax", "hybrid"})
|
|
44
|
+
UCELL_MARKER_ROLES = frozenset({"positive", "negative", "presence", "identity"})
|
|
45
|
+
MARKER_ROLE_MODES = frozenset({"shared", "phase_specific"})
|
|
46
|
+
MARKER_ROLE_INFERENCE_MODES = frozenset({"none", "signed", "scanpy_signed"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_allowed_values(values) -> str:
|
|
50
|
+
"""Format allowed values deterministically for user-facing messages."""
|
|
51
|
+
return ", ".join(repr(value) for value in sorted(values))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_choice(value, allowed, name: str):
|
|
55
|
+
"""Return *value* when it belongs to *allowed*, otherwise raise."""
|
|
56
|
+
if value not in allowed:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"{name} must be one of: {format_allowed_values(allowed)}. "
|
|
59
|
+
f"Got {value!r}."
|
|
60
|
+
)
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_interval(minimum, maximum, inclusive_min, inclusive_max):
|
|
65
|
+
left = "[" if inclusive_min else "("
|
|
66
|
+
right = "]" if inclusive_max else ")"
|
|
67
|
+
return f"{left}{minimum:g}, {maximum:g}{right}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_probability_range(
|
|
71
|
+
value,
|
|
72
|
+
name: str,
|
|
73
|
+
*,
|
|
74
|
+
inclusive_min=True,
|
|
75
|
+
inclusive_max=True,
|
|
76
|
+
_minimum=0.0,
|
|
77
|
+
_maximum=1.0,
|
|
78
|
+
):
|
|
79
|
+
"""Validate a numeric probability-like value against bounded endpoints."""
|
|
80
|
+
message = (
|
|
81
|
+
f"{name} must be between 0 and 1."
|
|
82
|
+
if _minimum == 0 and _maximum == 1 and inclusive_min and inclusive_max
|
|
83
|
+
else f"{name} must be in "
|
|
84
|
+
f"{_format_interval(_minimum, _maximum, inclusive_min, inclusive_max)}."
|
|
85
|
+
)
|
|
86
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
87
|
+
raise ValueError(message)
|
|
88
|
+
minimum_ok = value >= _minimum if inclusive_min else value > _minimum
|
|
89
|
+
maximum_ok = value <= _maximum if inclusive_max else value < _maximum
|
|
90
|
+
if not minimum_ok or not maximum_ok:
|
|
91
|
+
raise ValueError(message)
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_positive(value, name: str, *, allow_zero=False):
|
|
96
|
+
"""Validate that a numeric value is positive (or non-negative)."""
|
|
97
|
+
message = (
|
|
98
|
+
f"{name} must be greater than or equal to 0."
|
|
99
|
+
if allow_zero
|
|
100
|
+
else f"{name} must be greater than 0."
|
|
101
|
+
)
|
|
102
|
+
if isinstance(value, bool) or not isinstance(value, Real):
|
|
103
|
+
raise ValueError(message)
|
|
104
|
+
if (allow_zero and value < 0) or (not allow_zero and value <= 0):
|
|
105
|
+
raise ValueError(message)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"MARKER_METHODS",
|
|
111
|
+
"PYDESEQ2_MARKER_METHODS",
|
|
112
|
+
"REFERENCE_MARKER_METHODS",
|
|
113
|
+
"REFERENCE_CONTRASTS",
|
|
114
|
+
"FILTERING_ALGORITHMS",
|
|
115
|
+
"PHASE1_OUTPUT_STATS",
|
|
116
|
+
"AGGREGATION_METHODS",
|
|
117
|
+
"SIMILARITY_METHODS",
|
|
118
|
+
"EVIDENCE_TO_LIKELIHOOD_METHODS",
|
|
119
|
+
"ASSIGN_METHODS",
|
|
120
|
+
"UCELL_MARKER_ROLES",
|
|
121
|
+
"MARKER_ROLE_MODES",
|
|
122
|
+
"MARKER_ROLE_INFERENCE_MODES",
|
|
123
|
+
"format_allowed_values",
|
|
124
|
+
"validate_choice",
|
|
125
|
+
"validate_probability_range",
|
|
126
|
+
"validate_positive",
|
|
127
|
+
]
|
easydecon/_version.py
ADDED
easydecon/config.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Config:
|
|
6
|
+
n_jobs: int = 5
|
|
7
|
+
batch_size: int | str = 1000
|
|
8
|
+
|
|
9
|
+
config = Config()
|
|
10
|
+
|
|
11
|
+
def set_n_jobs(n):
|
|
12
|
+
n = int(n)
|
|
13
|
+
if n == 0:
|
|
14
|
+
raise ValueError("n_jobs cannot be 0. Use 1 for serial or -1 for all CPUs.")
|
|
15
|
+
config.n_jobs = n
|
|
16
|
+
|
|
17
|
+
def set_batch_size(n):
|
|
18
|
+
if n == "auto":
|
|
19
|
+
config.batch_size = n
|
|
20
|
+
return
|
|
21
|
+
n = int(n)
|
|
22
|
+
if n < 1:
|
|
23
|
+
raise ValueError("batch_size must be a positive integer or 'auto'.")
|
|
24
|
+
config.batch_size = n
|