pyplasmode 1.0.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.
pyplasmode/py.typed ADDED
File without changes
pyplasmode/results.py ADDED
@@ -0,0 +1,172 @@
1
+ """Typed aggregate results returned by biomarker evaluators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class RankingAtDepth:
11
+ """Exact-feature recovery at one leading-list depth."""
12
+
13
+ depth: int
14
+ selected_count: int
15
+ true_positive_count: int
16
+ target_count: int
17
+ recall: float
18
+ precision: float
19
+ false_discovery_fraction: float
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class RankingEvaluation:
24
+ """Exact recovery across declared depths."""
25
+
26
+ first_relevant_rank: int | None
27
+ at_depth: tuple[RankingAtDepth, ...]
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class GroupAtDepth:
32
+ """Admissible substitute-group recovery at one depth."""
33
+
34
+ depth: int
35
+ recovered_group_count: int
36
+ group_count: int
37
+ group_recall: float
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class GroupEvaluation:
42
+ """Recovery of at least one declared member per substitute group."""
43
+
44
+ at_depth: tuple[GroupAtDepth, ...]
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class ModuleAtDepth:
49
+ """Absolute-weight coverage of a distributed module at one depth."""
50
+
51
+ depth: int
52
+ recovered_member_count: int
53
+ member_count: int
54
+ weighted_coverage: float
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class ModuleEvaluation:
59
+ """Distributed module coverage across declared depths."""
60
+
61
+ at_depth: tuple[ModuleAtDepth, ...]
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class GraphRegionAtDepth:
66
+ """Node and induced-edge recovery for a declared graph region."""
67
+
68
+ depth: int
69
+ node_coverage: float
70
+ edge_coverage: float
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class GraphRegionEvaluation:
75
+ """Graph-region recovery across declared depths."""
76
+
77
+ at_depth: tuple[GraphRegionAtDepth, ...]
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class SignalReconstructionEvaluation:
82
+ """Held-out linear reconstruction of a known latent signal."""
83
+
84
+ selected_feature_count: int
85
+ evaluation_count: int
86
+ r_squared: float
87
+ root_mean_squared_error: float
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class PredictionEvaluation:
92
+ """One standard prediction metric over supplied outputs."""
93
+
94
+ metric: str
95
+ value: float
96
+ sample_count: int
97
+
98
+
99
+ @dataclass(frozen=True, slots=True)
100
+ class PanelEvaluation:
101
+ """Prediction retained by a panel relative to supplied reference predictions."""
102
+
103
+ metric: str
104
+ reference_value: float
105
+ panel_value: float
106
+ difference: float
107
+ sample_count: int
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class RankStabilityEvaluation:
112
+ """Pairwise leading-set, full-rank and top-weighted stability."""
113
+
114
+ ranking_count: int
115
+ feature_count: int
116
+ depth: int
117
+ mean_leading_jaccard: float
118
+ mean_leading_intersection: float
119
+ expected_random_intersection: float
120
+ excess_leading_intersection: float
121
+ mean_spearman: float
122
+ mean_rank_biased_overlap: float
123
+
124
+
125
+ @dataclass(frozen=True, slots=True)
126
+ class ReplicateSummary:
127
+ """Mean, Monte Carlo uncertainty and failure count across replicates."""
128
+
129
+ replicate_count: int
130
+ failure_count: int
131
+ mean: float
132
+ standard_deviation: float
133
+ standard_error: float
134
+ confidence_interval_low: float
135
+ confidence_interval_high: float
136
+
137
+
138
+ @dataclass(frozen=True, slots=True)
139
+ class FractionalRecovery:
140
+ """Mechanism-specific recovery for one fractional nomination."""
141
+
142
+ estimand: Literal["exact", "group", "module", "interaction"]
143
+ value: float
144
+ truth_support: int
145
+ positive_score_support_size: int
146
+ requested_depth: int
147
+ achieved_size: int
148
+
149
+
150
+ @dataclass(frozen=True, slots=True)
151
+ class MatchedRecovery:
152
+ """Observed recovery calibrated against an analytic matched reference."""
153
+
154
+ estimand: Literal["exact", "group", "module", "interaction"]
155
+ method: Literal["analytic"]
156
+ requested_depth: int
157
+ achieved_size: int
158
+ observed_recovery: float
159
+ expected_recovery: float
160
+ chance_adjusted_recovery: float
161
+ admissible_panel_count: int
162
+
163
+
164
+ @dataclass(frozen=True, slots=True)
165
+ class OutcomeSpecificity:
166
+ """Own-truth recovery relative to disjoint foreign truths."""
167
+
168
+ estimand: Literal["exact", "group", "module", "interaction"]
169
+ own_adjusted_recovery: float
170
+ mean_foreign_adjusted_recovery: float
171
+ specificity: float
172
+ foreign_truth_count: int
@@ -0,0 +1,123 @@
1
+ """Resource-agnostic biomarker sets, graphs and alignment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+
9
+ from pyplasmode.data import Population
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class StructureAlignment:
14
+ """Explicit alignment of structure identities to a population."""
15
+
16
+ mapped: tuple[str, ...]
17
+ unmapped: tuple[str, ...]
18
+ excluded: tuple[str, ...]
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class FeatureSet:
23
+ """One named biomarker membership set."""
24
+
25
+ name: str
26
+ members: tuple[str, ...]
27
+
28
+ def __post_init__(self) -> None:
29
+ if not self.name or self.name.strip() != self.name:
30
+ raise ValueError("feature-set name must be a non-empty normalized string")
31
+ if not self.members or any(not member for member in self.members):
32
+ raise ValueError("a feature set must contain non-empty identities")
33
+ if len(set(self.members)) != len(self.members):
34
+ raise ValueError("feature-set members must be unique")
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class FeatureSets:
39
+ """An ordered collection of named biomarker membership sets."""
40
+
41
+ sets: tuple[FeatureSet, ...]
42
+
43
+ def __post_init__(self) -> None:
44
+ if not self.sets:
45
+ raise ValueError("FeatureSets must contain at least one set")
46
+ names = tuple(item.name for item in self.sets)
47
+ if len(set(names)) != len(names):
48
+ raise ValueError("feature-set names must be unique")
49
+
50
+ def get(self, name: str) -> FeatureSet:
51
+ """Return one named feature set or fail explicitly."""
52
+ for item in self.sets:
53
+ if item.name == name:
54
+ return item
55
+ raise ValueError(f"unknown feature set: {name}")
56
+
57
+ def align(self, population: Population) -> StructureAlignment:
58
+ """Report mapped, unmapped and population-excluded identities."""
59
+ declared = {member for item in self.sets for member in item.members}
60
+ population_ids = set(population.feature_ids)
61
+ return StructureAlignment(
62
+ mapped=tuple(identity for identity in population.feature_ids if identity in declared),
63
+ unmapped=tuple(sorted(declared - population_ids)),
64
+ excluded=tuple(
65
+ identity for identity in population.feature_ids if identity not in declared
66
+ ),
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class FeatureEdge:
72
+ """One finite non-negative undirected biomarker edge."""
73
+
74
+ source: str
75
+ target: str
76
+ weight: float = 1.0
77
+
78
+ def __post_init__(self) -> None:
79
+ if not self.source or not self.target or self.source == self.target:
80
+ raise ValueError("edge endpoints must be distinct non-empty identities")
81
+ if not np.isfinite(self.weight) or self.weight < 0.0:
82
+ raise ValueError("edge weight must be finite and non-negative")
83
+
84
+
85
+ @dataclass(frozen=True, slots=True)
86
+ class FeatureGraph:
87
+ """A weighted undirected graph over explicit biomarker identities."""
88
+
89
+ nodes: tuple[str, ...]
90
+ edges: tuple[FeatureEdge, ...]
91
+
92
+ def __post_init__(self) -> None:
93
+ if not self.nodes or any(not node for node in self.nodes):
94
+ raise ValueError("graph nodes must be non-empty identities")
95
+ if len(set(self.nodes)) != len(self.nodes):
96
+ raise ValueError("graph nodes must be unique")
97
+ node_set = set(self.nodes)
98
+ pairs: set[tuple[str, str]] = set()
99
+ for edge in self.edges:
100
+ if edge.source not in node_set or edge.target not in node_set:
101
+ raise ValueError("every edge endpoint must be declared in graph nodes")
102
+ pair = (
103
+ (edge.source, edge.target)
104
+ if edge.source < edge.target
105
+ else (edge.target, edge.source)
106
+ )
107
+ if pair in pairs:
108
+ raise ValueError("undirected graph edges must be unique")
109
+ pairs.add(pair)
110
+
111
+ def align(self, population: Population) -> StructureAlignment:
112
+ """Report mapped, unmapped and population-excluded graph nodes."""
113
+ graph_nodes = set(self.nodes)
114
+ population_ids = set(population.feature_ids)
115
+ return StructureAlignment(
116
+ mapped=tuple(
117
+ identity for identity in population.feature_ids if identity in graph_nodes
118
+ ),
119
+ unmapped=tuple(sorted(graph_nodes - population_ids)),
120
+ excluded=tuple(
121
+ identity for identity in population.feature_ids if identity not in graph_nodes
122
+ ),
123
+ )