annotuneagent 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.
Files changed (38) hide show
  1. annotuneagent/__init__.py +68 -0
  2. annotuneagent/__main__.py +6 -0
  3. annotuneagent/_version.py +19 -0
  4. annotuneagent/annotate.py +93 -0
  5. annotuneagent/arbitrate.py +141 -0
  6. annotuneagent/cli.py +366 -0
  7. annotuneagent/decide.py +725 -0
  8. annotuneagent/evaluate.py +121 -0
  9. annotuneagent/evidence.py +606 -0
  10. annotuneagent/granularity/__init__.py +28 -0
  11. annotuneagent/granularity/cl_dag.py +179 -0
  12. annotuneagent/granularity/ontology_map.py +108 -0
  13. annotuneagent/granularity/relations.py +96 -0
  14. annotuneagent/hypotheses.py +38 -0
  15. annotuneagent/local_lora.py +706 -0
  16. annotuneagent/marker_plot.py +172 -0
  17. annotuneagent/markers.py +206 -0
  18. annotuneagent/markers.tsv +378 -0
  19. annotuneagent/mltools.py +128 -0
  20. annotuneagent/ontology/cl_isa.json +1 -0
  21. annotuneagent/ontology/granularity_policy.yaml +8 -0
  22. annotuneagent/ontology/ontology_map.jsonl +198 -0
  23. annotuneagent/prepare.py +1385 -0
  24. annotuneagent/py.typed +0 -0
  25. annotuneagent/read.py +76 -0
  26. annotuneagent/tokenizer/LICENSE +202 -0
  27. annotuneagent/tokenizer/merges.txt +247587 -0
  28. annotuneagent/tokenizer/tokenizer.json +495920 -0
  29. annotuneagent/tokenizer/tokenizer_config.json +305 -0
  30. annotuneagent/tokenizer/vocab.json +248046 -0
  31. annotuneagent/votes.py +83 -0
  32. annotuneagent/workflow.py +2037 -0
  33. annotuneagent-0.1.0.dist-info/METADATA +208 -0
  34. annotuneagent-0.1.0.dist-info/RECORD +38 -0
  35. annotuneagent-0.1.0.dist-info/WHEEL +5 -0
  36. annotuneagent-0.1.0.dist-info/entry_points.txt +2 -0
  37. annotuneagent-0.1.0.dist-info/licenses/LICENSE +201 -0
  38. annotuneagent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,121 @@
1
+ # Copyright 2026 Zhao Li (李钊)
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ """Cluster-level comparison against author labels.
5
+
6
+ Counts are the result. A rate that drops ``uncertain`` clusters is not reported,
7
+ because refusing a label is part of the outcome.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import Counter
13
+
14
+ from anndata import AnnData
15
+
16
+ from .evidence import cluster_ids
17
+
18
+
19
+ def majority_label(labels: list[str]) -> str | None:
20
+ """Most common label. Equal top counts mean the reference is ambiguous."""
21
+ if not labels:
22
+ return None
23
+ counts = Counter(labels)
24
+ ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
25
+ if len(ranked) > 1 and ranked[0][1] == ranked[1][1]:
26
+ return None
27
+ return ranked[0][0]
28
+
29
+
30
+ def _coarse(predicted: str, reference: str, parents: dict[str, str]) -> bool:
31
+ if predicted == reference:
32
+ return True
33
+ if parents.get(predicted) == reference:
34
+ return True
35
+ if parents.get(reference) == predicted:
36
+ return True
37
+ return False
38
+
39
+
40
+ def evaluate(
41
+ records: list[dict[str, object]],
42
+ adata: AnnData,
43
+ cluster_key: str,
44
+ label_key: str,
45
+ *,
46
+ parents: dict[str, str] | None = None,
47
+ prediction_key: str | None = None,
48
+ ) -> dict[str, object]:
49
+ """Compare one record per cluster with the majority author label in that cluster.
50
+
51
+ ``parents`` maps a label to the coarser label that still counts as a match.
52
+ Sibling types that share a parent do not match each other. When a workflow
53
+ wrote cell labels, ``prediction_key`` evaluates those final labels.
54
+ """
55
+ if label_key not in adata.obs.columns:
56
+ raise KeyError(f"obs has no column {label_key!r}")
57
+ if prediction_key is not None and prediction_key not in adata.obs.columns:
58
+ raise KeyError(f"obs has no column {prediction_key!r}")
59
+ parent_map = {} if parents is None else parents
60
+ by_cluster = {str(record["cluster"]): record for record in records}
61
+ n_exact = 0
62
+ n_coarse = 0
63
+ n_ambiguous = 0
64
+ n_label = 0
65
+ n_uncertain = 0
66
+ n_mixed = 0
67
+ per_cluster: list[dict[str, object]] = []
68
+ # A cell with no author label takes no part in the majority.
69
+ author = adata.obs[label_key]
70
+ known = author.notna()
71
+ groups = adata.obs[cluster_key].astype(str)
72
+ for cluster in cluster_ids(adata, cluster_key):
73
+ record = by_cluster[cluster]
74
+ status = str(record["status"])
75
+ predicted = record["cell_type"]
76
+ if prediction_key is not None:
77
+ labels = adata.obs.loc[groups == cluster, prediction_key]
78
+ names = labels.dropna().astype(str).unique()
79
+ if len(names) == 1 and not labels.isna().any():
80
+ status, predicted = "label", str(names[0])
81
+ elif len(names) == 0:
82
+ status, predicted = "uncertain", None
83
+ else:
84
+ status, predicted = "mixed", None
85
+ if status == "label":
86
+ n_label += 1
87
+ elif status == "uncertain":
88
+ n_uncertain += 1
89
+ elif status == "mixed":
90
+ n_mixed += 1
91
+ reference = majority_label(author[(groups == cluster) & known].astype(str).tolist())
92
+ exact = False
93
+ coarse = False
94
+ if reference is None:
95
+ n_ambiguous += 1
96
+ elif status == "label" and isinstance(predicted, str):
97
+ exact = predicted == reference
98
+ coarse = _coarse(predicted, reference, parent_map)
99
+ n_exact += int(exact)
100
+ n_coarse += int(coarse)
101
+ per_cluster.append(
102
+ {
103
+ "cluster": cluster,
104
+ "status": status,
105
+ "predicted": predicted,
106
+ "reference": reference,
107
+ "exact": exact,
108
+ "coarse": coarse,
109
+ }
110
+ )
111
+ n_clusters = len(per_cluster)
112
+ return {
113
+ "n_clusters": n_clusters,
114
+ "n_label": n_label,
115
+ "n_uncertain": n_uncertain,
116
+ "n_mixed": n_mixed,
117
+ "n_ambiguous_reference": n_ambiguous,
118
+ "n_exact": n_exact,
119
+ "n_coarse": n_coarse,
120
+ "clusters": per_cluster,
121
+ }