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/__init__.py +178 -0
- pyplasmode/_validation.py +29 -0
- pyplasmode/data.py +221 -0
- pyplasmode/evaluate.py +392 -0
- pyplasmode/generate.py +349 -0
- pyplasmode/nominations.py +623 -0
- pyplasmode/outcomes.py +709 -0
- pyplasmode/py.typed +0 -0
- pyplasmode/results.py +172 -0
- pyplasmode/structures.py +123 -0
- pyplasmode/truth.py +510 -0
- pyplasmode-1.0.0.dist-info/METADATA +142 -0
- pyplasmode-1.0.0.dist-info/RECORD +15 -0
- pyplasmode-1.0.0.dist-info/WHEEL +4 -0
- pyplasmode-1.0.0.dist-info/licenses/LICENSE +29 -0
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
|
pyplasmode/structures.py
ADDED
|
@@ -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
|
+
)
|