clacell 0.1.0__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.
- clacell/__init__.py +4 -0
- clacell/_marker_based_annotation/__init__.py +10 -0
- clacell/_marker_based_annotation/cli.py +73 -0
- clacell/_marker_based_annotation/clustering.py +121 -0
- clacell/_marker_based_annotation/human_pbmc_marker.json +1 -0
- clacell/_marker_based_annotation/io.py +34 -0
- clacell/_marker_based_annotation/markers.py +263 -0
- clacell/_marker_based_annotation/model_selection.py +216 -0
- clacell/_marker_based_annotation/types.py +45 -0
- clacell/classifier.py +186 -0
- clacell/classifier_copy.py +226 -0
- clacell/marker_annotator.py +33 -0
- clacell/test_robustness.py +248 -0
- clacell-0.1.0.dist-info/METADATA +77 -0
- clacell-0.1.0.dist-info/RECORD +17 -0
- clacell-0.1.0.dist-info/WHEEL +4 -0
- clacell-0.1.0.dist-info/licenses/LICENSE +39 -0
clacell/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .io import read_count_matrix, read_markers
|
|
10
|
+
from .model_selection import select_cluster_model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
p = argparse.ArgumentParser(description="Marker-based annotation pipeline")
|
|
15
|
+
p.add_argument(
|
|
16
|
+
"--matrix", required=True, help="Gene x cell count matrix CSV (index=gene)"
|
|
17
|
+
)
|
|
18
|
+
p.add_argument("--markers", required=True, help="Markers as JSON or CSV/TSV")
|
|
19
|
+
p.add_argument("--outdir", required=True, help="Output directory")
|
|
20
|
+
p.add_argument("--species", default="human", choices=["human", "mouse"])
|
|
21
|
+
p.add_argument("--min-cluster", type=int, default=3)
|
|
22
|
+
p.add_argument("--max-cluster", type=int, default=20)
|
|
23
|
+
p.add_argument("--seed", type=int, default=0)
|
|
24
|
+
return p
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
args = _build_parser().parse_args()
|
|
29
|
+
|
|
30
|
+
matrix = read_count_matrix(args.matrix)
|
|
31
|
+
markers = read_markers(args.markers)
|
|
32
|
+
|
|
33
|
+
result = select_cluster_model(
|
|
34
|
+
matrix,
|
|
35
|
+
markers,
|
|
36
|
+
min_cluster=args.min_cluster,
|
|
37
|
+
max_cluster=args.max_cluster,
|
|
38
|
+
species=args.species,
|
|
39
|
+
random_state=args.seed,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
outdir = Path(args.outdir)
|
|
43
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
params_file = outdir / "best_params.json"
|
|
46
|
+
with params_file.open("w", encoding="utf-8") as f:
|
|
47
|
+
json.dump(result.params_final, f, indent=2)
|
|
48
|
+
|
|
49
|
+
auc_file = outdir / "final_auc.json"
|
|
50
|
+
with auc_file.open("w", encoding="utf-8") as f:
|
|
51
|
+
json.dump(result.auc_final, f, indent=2)
|
|
52
|
+
|
|
53
|
+
clusters = result.model_final.clusters.rename("cluster")
|
|
54
|
+
cluster_labels = pd.DataFrame({"cell": clusters.index, "cluster": clusters.values})
|
|
55
|
+
cluster_labels.to_csv(outdir / "clusters.csv", index=False)
|
|
56
|
+
|
|
57
|
+
labels = pd.DataFrame(
|
|
58
|
+
{
|
|
59
|
+
"cell": result.label_final.index,
|
|
60
|
+
"cell_type_label": result.label_final.values,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
labels.to_csv(outdir / "cell_type_labels.csv", index=False)
|
|
64
|
+
|
|
65
|
+
result.auc_table.to_csv(outdir / "model_grid_auc.csv", index=False)
|
|
66
|
+
result.silhouette_table.to_csv(outdir / "model_grid_silhouette.csv", index=False)
|
|
67
|
+
result.cluster_table.to_csv(outdir / "model_grid_cluster_counts.csv", index=False)
|
|
68
|
+
|
|
69
|
+
print(f"Done. Results written to: {outdir}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import anndata as ad
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import scanpy as sc
|
|
7
|
+
|
|
8
|
+
from .types import ClusterResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_anndata(umi_count: pd.DataFrame) -> ad.AnnData:
|
|
12
|
+
if not isinstance(umi_count, pd.DataFrame):
|
|
13
|
+
raise TypeError(
|
|
14
|
+
"umi_count must be a pandas DataFrame with genes as index and cells as columns"
|
|
15
|
+
)
|
|
16
|
+
adata = ad.AnnData(X=umi_count.T.copy())
|
|
17
|
+
adata.var_names = umi_count.index.astype(str)
|
|
18
|
+
adata.obs_names = umi_count.columns.astype(str)
|
|
19
|
+
return adata
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cluster_scanpy(
|
|
23
|
+
umi_count: pd.DataFrame,
|
|
24
|
+
number_neighbor: int = 30,
|
|
25
|
+
variable_gene: bool = False,
|
|
26
|
+
resolution: float = 1.0,
|
|
27
|
+
num_pc: int = 50,
|
|
28
|
+
species: str = "human",
|
|
29
|
+
normalize: bool = True,
|
|
30
|
+
min_cells: int = 3,
|
|
31
|
+
min_genes: int = 1,
|
|
32
|
+
random_state: int = 0,
|
|
33
|
+
) -> ClusterResult:
|
|
34
|
+
adata = _to_anndata(umi_count)
|
|
35
|
+
|
|
36
|
+
sc.pp.filter_cells(adata, min_genes=min_genes)
|
|
37
|
+
sc.pp.filter_genes(adata, min_cells=min_cells)
|
|
38
|
+
|
|
39
|
+
if species == "human":
|
|
40
|
+
adata.var["mito"] = adata.var_names.str.startswith("MT-")
|
|
41
|
+
elif species == "mouse":
|
|
42
|
+
adata.var["mito"] = adata.var_names.str.startswith("mt-")
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError("species must be 'human' or 'mouse'")
|
|
45
|
+
|
|
46
|
+
sc.pp.calculate_qc_metrics(
|
|
47
|
+
adata, qc_vars=["mito"], percent_top=None, log1p=False, inplace=True
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if normalize:
|
|
51
|
+
sc.pp.normalize_total(adata, target_sum=10000)
|
|
52
|
+
sc.pp.log1p(adata)
|
|
53
|
+
else:
|
|
54
|
+
adata.X = np.log1p(adata.X)
|
|
55
|
+
|
|
56
|
+
if variable_gene:
|
|
57
|
+
sc.pp.highly_variable_genes(adata, min_mean=0.0125, max_mean=3, min_disp=0.5)
|
|
58
|
+
hvg_mask = adata.var["highly_variable"].to_numpy()
|
|
59
|
+
if int(hvg_mask.sum()) == 0:
|
|
60
|
+
adata_use = adata.copy()
|
|
61
|
+
else:
|
|
62
|
+
adata_use = adata[:, hvg_mask].copy()
|
|
63
|
+
else:
|
|
64
|
+
if species == "human":
|
|
65
|
+
mt_rb = adata.var_names.str.contains(
|
|
66
|
+
r"^MT-|^RPL|^RPS|^MRPS|^MRPL", regex=True
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
mt_rb = adata.var_names.str.contains(
|
|
70
|
+
r"^mt-|^Rpl|^Rps|^Mrps|^Mrpl", regex=True
|
|
71
|
+
)
|
|
72
|
+
adata_use = adata[:, ~mt_rb].copy()
|
|
73
|
+
|
|
74
|
+
if adata_use.n_vars == 0:
|
|
75
|
+
adata_use = adata.copy()
|
|
76
|
+
|
|
77
|
+
sc.pp.scale(adata_use)
|
|
78
|
+
|
|
79
|
+
n_pc_eff = min(num_pc, adata_use.n_vars - 1, adata_use.n_obs - 1)
|
|
80
|
+
n_pc_eff = max(n_pc_eff, 2)
|
|
81
|
+
|
|
82
|
+
sc.tl.pca(
|
|
83
|
+
adata_use,
|
|
84
|
+
n_comps=n_pc_eff,
|
|
85
|
+
random_state=random_state,
|
|
86
|
+
mask_var=None,
|
|
87
|
+
)
|
|
88
|
+
sc.pp.neighbors(
|
|
89
|
+
adata_use,
|
|
90
|
+
n_neighbors=number_neighbor,
|
|
91
|
+
n_pcs=n_pc_eff,
|
|
92
|
+
random_state=random_state,
|
|
93
|
+
)
|
|
94
|
+
sc.tl.leiden(
|
|
95
|
+
adata_use,
|
|
96
|
+
resolution=resolution,
|
|
97
|
+
random_state=random_state,
|
|
98
|
+
key_added="cluster",
|
|
99
|
+
flavor="igraph",
|
|
100
|
+
n_iterations=2,
|
|
101
|
+
directed=False,
|
|
102
|
+
)
|
|
103
|
+
sc.tl.tsne(adata_use, n_pcs=n_pc_eff, random_state=random_state)
|
|
104
|
+
|
|
105
|
+
adata.obsm["X_pca"] = adata_use.obsm["X_pca"]
|
|
106
|
+
adata.obsm["X_tsne"] = adata_use.obsm["X_tsne"]
|
|
107
|
+
adata.obs["cluster"] = adata_use.obs["cluster"].astype(str)
|
|
108
|
+
|
|
109
|
+
params = {
|
|
110
|
+
"number_neighbor": number_neighbor,
|
|
111
|
+
"variable_gene": variable_gene,
|
|
112
|
+
"resolution": resolution,
|
|
113
|
+
"num_pc": num_pc,
|
|
114
|
+
"normalize": normalize,
|
|
115
|
+
"min_cells": min_cells,
|
|
116
|
+
"min_genes": min_genes,
|
|
117
|
+
"species": species,
|
|
118
|
+
}
|
|
119
|
+
return ClusterResult(
|
|
120
|
+
adata=adata, clusters=adata.obs["cluster"].copy(), params=params
|
|
121
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"CD4+ T cell":["CD3D+","CD3E+","CD3G+","TRAC+","CD4+","TCF7+","CD27+","IL7R+","CD8A-","CD8B-","GNLY-","NKG7-","CST7-"],"Cytotoxic T cell":["CD3D+","CD3E+","CD3G+","TRAC+","CD8A+","CD8B+","GZMK+","CCL5+","NKG7+","CD4-","FCER1G-"],"B cell":["CD19+","MS4A1+","CD79A+","CD79B+","MZB1+","IGHD+","IGHM+"],"Natural killer cell":["NCAM1+","NKG7+","KLRB1+","KLRD1+","KLRF1+","KLRC1+","KLRC2+","KLRC3+","KLRC4+","CD3D-","CD3E-","CD3G-","CD14-","FCGR3A+","FCGR3B+","ITGAL+","ITGAM+","FCER1G+","TRAC-"],"CD14+ monocyte":["VCAN+","FCN1+","S100A8+","S100A9+","CD14+","ITGAL+","ITGAM+","CSF3R+","CSF1R+","CX3CR1+","FCGR3A-","FCGR3B-","TYROBP+","LYZ+","S100A12+","CD3D-","CD3E-","CD3G-","TRAC-","NKG7-","KLRB1-","KLRD1-"],"CD16+ monocyte":["FCN1+","FCGR3A+","FCGR3B+","ITGAL+","ITGAM+","CSF3R+","CSF1R+","CX3CR1+","CDKN1C+","MS4A7+","S100A8-","S100A9-","S100A12-","CD14-","CD3D-","CD3E-","CD3G-","TRAC-","NKG7-","KLRB1-","KLRD1-"],"Dendritic cell":["HLA-DPB1+","HLA-DPA1+","HLA-DQA1+","ITGAX+","CD3D-","CD3E-","CD3G-","NCAM1-","CD19-","CD14-","CD1C+","CD1E+","FCER1A+","CLEC10A+","FCGR2B+","MS4A1-","CD79A-","CD79B-"],"Plasmacytoid dendritic cell":["IL3RA+","GZMB+","JCHAIN+","IRF7+","TCF4+","LILRA4+","CLEC4C+","ITGAX-","CD3D-","CD3E-","CD3G-","NCAM1-","CD19-","CD14-","MS4A1-","CD79A-","CD79B-"],"Plasma cell":["CD38+","XBP1+","CD27+","SLAMF7+","CD19-","MS4A1-","CD3D-","CD3E-","CD3G-","IGHA1+","IGHA2+","IGHG1+","IGHG2+","IGHG3+","IGHG4+"],"Megakaryocyte":["PF4+","PPBP+","GP5+","ITGA2B+","NRGN+","TUBB1+","SPARC+","RGS18+","MYL9+","GNG11+"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .types import MarkerDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_count_matrix(path: str | Path) -> pd.DataFrame:
|
|
12
|
+
matrix = pd.read_csv(path, index_col=0)
|
|
13
|
+
return matrix
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_markers(path: str | Path) -> MarkerDict:
|
|
17
|
+
p = Path(path)
|
|
18
|
+
if p.suffix.lower() == ".json":
|
|
19
|
+
with p.open("r", encoding="utf-8") as f:
|
|
20
|
+
obj = json.load(f)
|
|
21
|
+
return {str(k): [str(x) for x in v] for k, v in obj.items()}
|
|
22
|
+
|
|
23
|
+
if p.suffix.lower() in {".csv", ".tsv"}:
|
|
24
|
+
sep = "\t" if p.suffix.lower() == ".tsv" else ","
|
|
25
|
+
df = pd.read_csv(p, sep=sep)
|
|
26
|
+
required = {"cell_type", "marker"}
|
|
27
|
+
if not required.issubset(set(df.columns)):
|
|
28
|
+
raise ValueError("Marker table must contain columns: cell_type, marker")
|
|
29
|
+
out: MarkerDict = {}
|
|
30
|
+
for ct, grp in df.groupby("cell_type"):
|
|
31
|
+
out[str(ct)] = [str(x) for x in grp["marker"].tolist()]
|
|
32
|
+
return out
|
|
33
|
+
|
|
34
|
+
raise ValueError("Unsupported marker format. Use .json, .csv, or .tsv")
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from sklearn.metrics import roc_auc_score
|
|
9
|
+
|
|
10
|
+
from .types import MarkerDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class ClusterAucResult:
|
|
15
|
+
auc_assign: pd.Series
|
|
16
|
+
cell_type_assign: pd.Series
|
|
17
|
+
auc_table: pd.DataFrame
|
|
18
|
+
cell_type_score: pd.DataFrame
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class AucLabelResult:
|
|
23
|
+
auc: pd.Series
|
|
24
|
+
label: pd.Series
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_marker(marker: MarkerDict) -> dict[str, tuple[list[str], list[str]]]:
|
|
28
|
+
out: dict[str, tuple[list[str], list[str]]] = {}
|
|
29
|
+
for cell_type, genes in marker.items():
|
|
30
|
+
pos: list[str] = []
|
|
31
|
+
neg: list[str] = []
|
|
32
|
+
for token in genes:
|
|
33
|
+
if not token:
|
|
34
|
+
continue
|
|
35
|
+
if token.endswith("-"):
|
|
36
|
+
neg.append(token[:-1])
|
|
37
|
+
elif token.endswith("+"):
|
|
38
|
+
pos.append(token[:-1])
|
|
39
|
+
else:
|
|
40
|
+
pos.append(token)
|
|
41
|
+
neg_set = set(neg)
|
|
42
|
+
pos = [g for g in pos if g not in neg_set]
|
|
43
|
+
out[cell_type] = (pos, neg)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_marker_score(
|
|
48
|
+
x: pd.DataFrame, marker_list: tuple[list[str], list[str]]
|
|
49
|
+
) -> pd.Series:
|
|
50
|
+
total = x.sum(axis=0).replace(0, np.nan)
|
|
51
|
+
|
|
52
|
+
gene_pos = [g for g in marker_list[0] if g in x.index]
|
|
53
|
+
gene_neg = [g for g in marker_list[1] if g in x.index]
|
|
54
|
+
|
|
55
|
+
if gene_pos and gene_neg:
|
|
56
|
+
out = x.loc[gene_pos].sum(axis=0) - x.loc[gene_neg].sum(axis=0)
|
|
57
|
+
elif gene_pos:
|
|
58
|
+
out = x.loc[gene_pos].sum(axis=0)
|
|
59
|
+
elif gene_neg:
|
|
60
|
+
neg_sum = x.loc[gene_neg].sum(axis=0)
|
|
61
|
+
out = neg_sum.max() - neg_sum
|
|
62
|
+
else:
|
|
63
|
+
out = pd.Series(0.0, index=x.columns)
|
|
64
|
+
|
|
65
|
+
res = out / total * 10000.0
|
|
66
|
+
res = res.fillna(0.0)
|
|
67
|
+
res[res < 0] = 0.0
|
|
68
|
+
return np.log1p(res)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def score_auc(score: pd.Series, label: pd.Series) -> float:
|
|
72
|
+
if label.nunique() != 2:
|
|
73
|
+
return 0.5
|
|
74
|
+
try:
|
|
75
|
+
return float(roc_auc_score(label.astype(int), score))
|
|
76
|
+
except ValueError:
|
|
77
|
+
return 0.5
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def assign_cluster(auc_table: pd.DataFrame) -> tuple[pd.Series, pd.Series]:
|
|
81
|
+
auc_assign = pd.Series(index=auc_table.columns, dtype=float)
|
|
82
|
+
cell_type_assign = pd.Series(index=auc_table.columns, dtype=object)
|
|
83
|
+
for cluster in auc_table.columns:
|
|
84
|
+
col = auc_table[cluster]
|
|
85
|
+
best_type = str(col.idxmax())
|
|
86
|
+
auc_assign.loc[cluster] = float(col.loc[best_type])
|
|
87
|
+
cell_type_assign.loc[cluster] = best_type
|
|
88
|
+
return auc_assign, cell_type_assign
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _cluster_to_celltype_map(auc_table: pd.DataFrame) -> dict[str, str]:
|
|
92
|
+
return {
|
|
93
|
+
str(cluster): str(auc_table[cluster].idxmax()) for cluster in auc_table.columns
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _merge_detector(
|
|
98
|
+
x: pd.DataFrame,
|
|
99
|
+
cluster_name: str,
|
|
100
|
+
cell_type: str,
|
|
101
|
+
cluster_all: pd.Series,
|
|
102
|
+
cluster_to_type: dict[str, str],
|
|
103
|
+
) -> float:
|
|
104
|
+
if cluster_to_type[cluster_name] == cell_type:
|
|
105
|
+
return 0.5
|
|
106
|
+
|
|
107
|
+
cluster_rm = [k for k, v in cluster_to_type.items() if v == cell_type]
|
|
108
|
+
keep = cluster_all.isin([cluster_name] + cluster_rm)
|
|
109
|
+
cluster_keep = cluster_all[keep]
|
|
110
|
+
score = x.loc[keep, cluster_to_type[cluster_name]]
|
|
111
|
+
return score_auc(score, cluster_keep == cluster_name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _reassign_cluster(
|
|
115
|
+
x: pd.DataFrame,
|
|
116
|
+
cluster: pd.Series,
|
|
117
|
+
auc_table: pd.DataFrame,
|
|
118
|
+
) -> tuple[pd.Series, pd.Series, pd.DataFrame]:
|
|
119
|
+
cluster_names = list(auc_table.columns.astype(str))
|
|
120
|
+
cluster_to_type = _cluster_to_celltype_map(auc_table)
|
|
121
|
+
cell_type_candidates = sorted(set(cluster_to_type.values()))
|
|
122
|
+
auc_table_update = auc_table.loc[cell_type_candidates, :].copy()
|
|
123
|
+
|
|
124
|
+
for cl in cluster_names:
|
|
125
|
+
if float(auc_table_update[cl].max()) > 0.9:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
scores: dict[str, float] = {}
|
|
129
|
+
for ct in cell_type_candidates:
|
|
130
|
+
auc_one = _merge_detector(x, cl, ct, cluster, cluster_to_type)
|
|
131
|
+
|
|
132
|
+
current_type = cluster_to_type[cl]
|
|
133
|
+
if (
|
|
134
|
+
sum(v == current_type for v in cluster_to_type.values()) == 1
|
|
135
|
+
and auc_one > 0.55
|
|
136
|
+
):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
cluster_rm = [k for k, v in cluster_to_type.items() if v == ct and k != cl]
|
|
140
|
+
keep_mask = ~cluster.isin(cluster_rm)
|
|
141
|
+
cluster_keep = cluster[keep_mask]
|
|
142
|
+
cluster_score = x.loc[keep_mask, ct]
|
|
143
|
+
scores[ct] = score_auc(cluster_score, cluster_keep == cl)
|
|
144
|
+
|
|
145
|
+
for ct, val in scores.items():
|
|
146
|
+
auc_table_update.loc[ct, cl] = val
|
|
147
|
+
|
|
148
|
+
auc_assign, cell_type_assign = assign_cluster(auc_table_update)
|
|
149
|
+
return auc_assign, cell_type_assign, auc_table_update
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compute_merge_cluster_auc(
|
|
153
|
+
x: pd.DataFrame,
|
|
154
|
+
cluster: pd.Series,
|
|
155
|
+
auc: pd.Series,
|
|
156
|
+
auc_table: pd.DataFrame,
|
|
157
|
+
merge: bool = True,
|
|
158
|
+
unassigned_threshold: float = 0.65,
|
|
159
|
+
) -> AucLabelResult:
|
|
160
|
+
auc_curr = auc.copy()
|
|
161
|
+
auc_table_curr = auc_table.copy()
|
|
162
|
+
_, cell_type_curr = assign_cluster(auc_table_curr)
|
|
163
|
+
|
|
164
|
+
if merge:
|
|
165
|
+
auc_update, cell_type_update, auc_table_update = _reassign_cluster(
|
|
166
|
+
x, cluster, auc_table_curr
|
|
167
|
+
)
|
|
168
|
+
num_iter = 1
|
|
169
|
+
while not cell_type_curr.equals(cell_type_update) and num_iter <= 5:
|
|
170
|
+
num_iter += 1
|
|
171
|
+
auc_curr = auc_update
|
|
172
|
+
cell_type_curr = cell_type_update
|
|
173
|
+
auc_table_curr = auc_table_update
|
|
174
|
+
auc_update, cell_type_update, auc_table_update = _reassign_cluster(
|
|
175
|
+
x, cluster, auc_table_curr
|
|
176
|
+
)
|
|
177
|
+
auc_curr = auc_update
|
|
178
|
+
cell_type_curr = cell_type_update
|
|
179
|
+
auc_table_curr = auc_table_update
|
|
180
|
+
|
|
181
|
+
cluster_names = list(auc_table_curr.columns.astype(str))
|
|
182
|
+
cluster_to_type = _cluster_to_celltype_map(auc_table_curr)
|
|
183
|
+
|
|
184
|
+
unassigned_clusters = [
|
|
185
|
+
c
|
|
186
|
+
for c in cluster_names
|
|
187
|
+
if float(auc_table_curr[c].max()) < unassigned_threshold
|
|
188
|
+
]
|
|
189
|
+
for c in unassigned_clusters:
|
|
190
|
+
cluster_to_type[c] = "Unassigned"
|
|
191
|
+
|
|
192
|
+
merged_auc: dict[str, float] = {}
|
|
193
|
+
for ct in sorted(set(cluster_to_type.values())):
|
|
194
|
+
if ct == "Unassigned":
|
|
195
|
+
continue
|
|
196
|
+
merge_clusters = [c for c, v in cluster_to_type.items() if v == ct]
|
|
197
|
+
merged_auc[ct] = score_auc(x[ct], cluster.isin(merge_clusters))
|
|
198
|
+
|
|
199
|
+
if unassigned_clusters:
|
|
200
|
+
vals = [
|
|
201
|
+
float(auc_curr.loc[c]) for c in unassigned_clusters if c in auc_curr.index
|
|
202
|
+
]
|
|
203
|
+
if vals:
|
|
204
|
+
merged_auc["Unassigned"] = float(np.mean(vals))
|
|
205
|
+
|
|
206
|
+
label = cluster.astype(str).map(cluster_to_type)
|
|
207
|
+
return AucLabelResult(auc=pd.Series(merged_auc, dtype=float), label=label)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def compute_cluster_auc(
|
|
211
|
+
x: pd.DataFrame, cluster: pd.Series, marker: MarkerDict
|
|
212
|
+
) -> ClusterAucResult:
|
|
213
|
+
marker_split = split_marker(marker)
|
|
214
|
+
cell_type_score = pd.DataFrame(
|
|
215
|
+
{
|
|
216
|
+
cell_type: compute_marker_score(x=x, marker_list=marker_set)
|
|
217
|
+
for cell_type, marker_set in marker_split.items()
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
uniq_cluster = pd.Index(cluster.astype(str).unique())
|
|
222
|
+
auc_table = pd.DataFrame(
|
|
223
|
+
index=list(marker_split.keys()), columns=uniq_cluster, dtype=float
|
|
224
|
+
)
|
|
225
|
+
for cell_type in marker_split.keys():
|
|
226
|
+
for cl in uniq_cluster:
|
|
227
|
+
auc_table.loc[cell_type, cl] = score_auc(
|
|
228
|
+
cell_type_score[cell_type], cluster.astype(str) == cl
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
auc_assign, cell_type_assign = assign_cluster(auc_table)
|
|
232
|
+
return ClusterAucResult(
|
|
233
|
+
auc_assign=auc_assign,
|
|
234
|
+
cell_type_assign=cell_type_assign,
|
|
235
|
+
auc_table=auc_table,
|
|
236
|
+
cell_type_score=cell_type_score,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def calculate_auc(
|
|
241
|
+
umi_count: pd.DataFrame,
|
|
242
|
+
cluster: pd.Series,
|
|
243
|
+
marker: MarkerDict,
|
|
244
|
+
merge: bool = True,
|
|
245
|
+
unassigned_threshold: float = 0.65,
|
|
246
|
+
) -> AucLabelResult:
|
|
247
|
+
res = compute_cluster_auc(umi_count, cluster.astype(str), marker)
|
|
248
|
+
return compute_merge_cluster_auc(
|
|
249
|
+
res.cell_type_score,
|
|
250
|
+
cluster.astype(str),
|
|
251
|
+
res.auc_assign,
|
|
252
|
+
res.auc_table,
|
|
253
|
+
merge=merge,
|
|
254
|
+
unassigned_threshold=unassigned_threshold,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def average_auc(auc: pd.Series) -> float:
|
|
259
|
+
counts = Counter(auc.index.tolist())
|
|
260
|
+
if len(auc) >= len(counts) * 2:
|
|
261
|
+
return 0.0
|
|
262
|
+
score = [float(auc.loc[ct]) - 0.7 for ct in counts.keys()]
|
|
263
|
+
return float(np.sum(score))
|