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/__init__.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Truth-known plasmode generation and biomarker recovery evaluation."""
|
|
2
|
+
|
|
3
|
+
from pyplasmode.data import (
|
|
4
|
+
Population,
|
|
5
|
+
PopulationPartition,
|
|
6
|
+
ResampledPopulation,
|
|
7
|
+
SamplingMethod,
|
|
8
|
+
partition_population,
|
|
9
|
+
resample_population,
|
|
10
|
+
sample_population,
|
|
11
|
+
)
|
|
12
|
+
from pyplasmode.evaluate import (
|
|
13
|
+
evaluate_graph_region,
|
|
14
|
+
evaluate_groups,
|
|
15
|
+
evaluate_module,
|
|
16
|
+
evaluate_panel,
|
|
17
|
+
evaluate_prediction,
|
|
18
|
+
evaluate_ranking,
|
|
19
|
+
evaluate_signal_reconstruction,
|
|
20
|
+
rank_stability,
|
|
21
|
+
summarize_replicates,
|
|
22
|
+
)
|
|
23
|
+
from pyplasmode.generate import (
|
|
24
|
+
PartitionedPlasmode,
|
|
25
|
+
PartitionSampleSizes,
|
|
26
|
+
Plasmode,
|
|
27
|
+
generate,
|
|
28
|
+
generate_partitioned,
|
|
29
|
+
)
|
|
30
|
+
from pyplasmode.nominations import (
|
|
31
|
+
FractionalNomination,
|
|
32
|
+
evaluate_fractional_recovery,
|
|
33
|
+
evaluate_matched_recovery,
|
|
34
|
+
evaluate_outcome_specificity,
|
|
35
|
+
nomination_from_scores,
|
|
36
|
+
sample_matched_nominations,
|
|
37
|
+
)
|
|
38
|
+
from pyplasmode.outcomes import (
|
|
39
|
+
BinaryGeneratedOutcome,
|
|
40
|
+
BinaryOutcome,
|
|
41
|
+
CalibrationError,
|
|
42
|
+
CalibrationResult,
|
|
43
|
+
ContinuousGeneratedOutcome,
|
|
44
|
+
ContinuousOutcome,
|
|
45
|
+
CountGeneratedOutcome,
|
|
46
|
+
CountOutcome,
|
|
47
|
+
Duration,
|
|
48
|
+
GeneratedOutcome,
|
|
49
|
+
OrdinalGeneratedOutcome,
|
|
50
|
+
OrdinalOutcome,
|
|
51
|
+
OutcomeSpec,
|
|
52
|
+
OutcomeSpecificationError,
|
|
53
|
+
RealizationError,
|
|
54
|
+
SurvivalGeneratedOutcome,
|
|
55
|
+
TimeToEventOutcome,
|
|
56
|
+
days,
|
|
57
|
+
duration,
|
|
58
|
+
generate_outcome,
|
|
59
|
+
years,
|
|
60
|
+
)
|
|
61
|
+
from pyplasmode.results import (
|
|
62
|
+
FractionalRecovery,
|
|
63
|
+
GraphRegionEvaluation,
|
|
64
|
+
GroupEvaluation,
|
|
65
|
+
MatchedRecovery,
|
|
66
|
+
ModuleEvaluation,
|
|
67
|
+
OutcomeSpecificity,
|
|
68
|
+
PanelEvaluation,
|
|
69
|
+
PredictionEvaluation,
|
|
70
|
+
RankingEvaluation,
|
|
71
|
+
RankStabilityEvaluation,
|
|
72
|
+
ReplicateSummary,
|
|
73
|
+
SignalReconstructionEvaluation,
|
|
74
|
+
)
|
|
75
|
+
from pyplasmode.structures import (
|
|
76
|
+
FeatureEdge,
|
|
77
|
+
FeatureGraph,
|
|
78
|
+
FeatureSet,
|
|
79
|
+
FeatureSets,
|
|
80
|
+
StructureAlignment,
|
|
81
|
+
)
|
|
82
|
+
from pyplasmode.truth import (
|
|
83
|
+
CorrelatedTruth,
|
|
84
|
+
CustomSignal,
|
|
85
|
+
CustomTruth,
|
|
86
|
+
FeatureWeight,
|
|
87
|
+
InteractionTruth,
|
|
88
|
+
MaterializedTruth,
|
|
89
|
+
ModuleTruth,
|
|
90
|
+
NullTruth,
|
|
91
|
+
RecoveryGroup,
|
|
92
|
+
SparseTruth,
|
|
93
|
+
TruthSpec,
|
|
94
|
+
materialize_truth,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
__version__ = "1.0.0"
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"BinaryGeneratedOutcome",
|
|
101
|
+
"BinaryOutcome",
|
|
102
|
+
"CalibrationError",
|
|
103
|
+
"CalibrationResult",
|
|
104
|
+
"ContinuousGeneratedOutcome",
|
|
105
|
+
"ContinuousOutcome",
|
|
106
|
+
"CorrelatedTruth",
|
|
107
|
+
"CountGeneratedOutcome",
|
|
108
|
+
"CountOutcome",
|
|
109
|
+
"CustomSignal",
|
|
110
|
+
"CustomTruth",
|
|
111
|
+
"Duration",
|
|
112
|
+
"FeatureEdge",
|
|
113
|
+
"FeatureGraph",
|
|
114
|
+
"FeatureSet",
|
|
115
|
+
"FeatureSets",
|
|
116
|
+
"FeatureWeight",
|
|
117
|
+
"FractionalNomination",
|
|
118
|
+
"FractionalRecovery",
|
|
119
|
+
"GeneratedOutcome",
|
|
120
|
+
"GraphRegionEvaluation",
|
|
121
|
+
"GroupEvaluation",
|
|
122
|
+
"InteractionTruth",
|
|
123
|
+
"MatchedRecovery",
|
|
124
|
+
"MaterializedTruth",
|
|
125
|
+
"ModuleEvaluation",
|
|
126
|
+
"ModuleTruth",
|
|
127
|
+
"NullTruth",
|
|
128
|
+
"OrdinalGeneratedOutcome",
|
|
129
|
+
"OrdinalOutcome",
|
|
130
|
+
"OutcomeSpec",
|
|
131
|
+
"OutcomeSpecificationError",
|
|
132
|
+
"OutcomeSpecificity",
|
|
133
|
+
"PanelEvaluation",
|
|
134
|
+
"PartitionSampleSizes",
|
|
135
|
+
"PartitionedPlasmode",
|
|
136
|
+
"Plasmode",
|
|
137
|
+
"Population",
|
|
138
|
+
"PopulationPartition",
|
|
139
|
+
"PredictionEvaluation",
|
|
140
|
+
"RankStabilityEvaluation",
|
|
141
|
+
"RankingEvaluation",
|
|
142
|
+
"RealizationError",
|
|
143
|
+
"RecoveryGroup",
|
|
144
|
+
"ReplicateSummary",
|
|
145
|
+
"ResampledPopulation",
|
|
146
|
+
"SamplingMethod",
|
|
147
|
+
"SignalReconstructionEvaluation",
|
|
148
|
+
"SparseTruth",
|
|
149
|
+
"StructureAlignment",
|
|
150
|
+
"SurvivalGeneratedOutcome",
|
|
151
|
+
"TimeToEventOutcome",
|
|
152
|
+
"TruthSpec",
|
|
153
|
+
"__version__",
|
|
154
|
+
"days",
|
|
155
|
+
"duration",
|
|
156
|
+
"evaluate_fractional_recovery",
|
|
157
|
+
"evaluate_graph_region",
|
|
158
|
+
"evaluate_groups",
|
|
159
|
+
"evaluate_matched_recovery",
|
|
160
|
+
"evaluate_module",
|
|
161
|
+
"evaluate_outcome_specificity",
|
|
162
|
+
"evaluate_panel",
|
|
163
|
+
"evaluate_prediction",
|
|
164
|
+
"evaluate_ranking",
|
|
165
|
+
"evaluate_signal_reconstruction",
|
|
166
|
+
"generate",
|
|
167
|
+
"generate_outcome",
|
|
168
|
+
"generate_partitioned",
|
|
169
|
+
"materialize_truth",
|
|
170
|
+
"nomination_from_scores",
|
|
171
|
+
"partition_population",
|
|
172
|
+
"rank_stability",
|
|
173
|
+
"resample_population",
|
|
174
|
+
"sample_matched_nominations",
|
|
175
|
+
"sample_population",
|
|
176
|
+
"summarize_replicates",
|
|
177
|
+
"years",
|
|
178
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared validation at public array and configuration boundaries."""
|
|
2
|
+
|
|
3
|
+
from numbers import Integral
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import ArrayLike, NDArray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def integer(value: object, name: str, *, minimum: int = 0) -> None:
|
|
10
|
+
if isinstance(value, bool) or not isinstance(value, Integral) or value < minimum:
|
|
11
|
+
raise ValueError(f"{name} must be an integer >= {minimum}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def identities(values: tuple[str, ...], name: str, *, allow_empty: bool = False) -> None:
|
|
15
|
+
if (not values and not allow_empty) or any(
|
|
16
|
+
not isinstance(value, str) or not value or value.strip() != value for value in values
|
|
17
|
+
):
|
|
18
|
+
raise ValueError(f"{name} must contain non-empty normalized strings")
|
|
19
|
+
if len(set(values)) != len(values):
|
|
20
|
+
raise ValueError(f"{name} must be unique")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def indices(values: ArrayLike, name: str) -> NDArray[np.int64]:
|
|
24
|
+
array = np.asarray(values)
|
|
25
|
+
if array.ndim != 1 or not array.size or array.dtype.kind not in "iu" or np.any(array < 0):
|
|
26
|
+
raise ValueError(f"{name} must contain non-negative integer indices")
|
|
27
|
+
if np.any(array > np.iinfo(np.int64).max):
|
|
28
|
+
raise ValueError(f"{name} exceeds the supported index range")
|
|
29
|
+
return np.array(array, dtype=np.int64, copy=True)
|
pyplasmode/data.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Empirical populations and deterministic complete-row resampling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import ArrayLike, NDArray
|
|
10
|
+
|
|
11
|
+
from pyplasmode._validation import identities as validate_identities
|
|
12
|
+
from pyplasmode._validation import indices as integer_indices
|
|
13
|
+
from pyplasmode._validation import integer
|
|
14
|
+
|
|
15
|
+
type SamplingMethod = Literal["with_replacement", "without_replacement"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _readonly_float_matrix(values: ArrayLike, *, name: str) -> NDArray[np.float64]:
|
|
19
|
+
matrix = np.asarray(values, dtype=np.float64)
|
|
20
|
+
if matrix.ndim != 2 or not matrix.shape[0] or not matrix.shape[1]:
|
|
21
|
+
raise ValueError(f"{name} must be a non-empty two-dimensional numeric matrix")
|
|
22
|
+
if np.isinf(matrix).any():
|
|
23
|
+
raise ValueError(f"{name} contains infinite values")
|
|
24
|
+
result = np.array(matrix, dtype=np.float64, copy=True)
|
|
25
|
+
result.setflags(write=False)
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _readonly_indices(values: NDArray[np.int64]) -> NDArray[np.int64]:
|
|
30
|
+
result = np.array(values, dtype=np.int64, copy=True)
|
|
31
|
+
result.setflags(write=False)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class Population:
|
|
37
|
+
"""An empirical biomarker matrix with stable ordered feature identities.
|
|
38
|
+
|
|
39
|
+
Missing values are retained. Infinite values, duplicate identities and shape mismatches
|
|
40
|
+
are rejected.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
X: NDArray[np.float64]
|
|
44
|
+
feature_ids: tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
def __init__(self, X: ArrayLike, feature_ids: tuple[str, ...]) -> None:
|
|
47
|
+
matrix = _readonly_float_matrix(X, name="X")
|
|
48
|
+
identities = tuple(feature_ids)
|
|
49
|
+
if len(identities) != matrix.shape[1]:
|
|
50
|
+
raise ValueError("feature_ids length must equal the number of X columns")
|
|
51
|
+
validate_identities(identities, "feature_ids")
|
|
52
|
+
object.__setattr__(self, "X", matrix)
|
|
53
|
+
object.__setattr__(self, "feature_ids", identities)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def sample_count(self) -> int:
|
|
57
|
+
"""Return the number of empirical rows."""
|
|
58
|
+
return int(self.X.shape[0])
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def feature_count(self) -> int:
|
|
62
|
+
"""Return the number of biomarker columns."""
|
|
63
|
+
return int(self.X.shape[1])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class ResampledPopulation:
|
|
68
|
+
"""A complete-row sample and its in-memory source-row indices."""
|
|
69
|
+
|
|
70
|
+
X: NDArray[np.float64]
|
|
71
|
+
feature_ids: tuple[str, ...]
|
|
72
|
+
source_indices: NDArray[np.int64]
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
matrix = _readonly_float_matrix(self.X, name="X")
|
|
76
|
+
indices = integer_indices(self.source_indices, "source_indices")
|
|
77
|
+
validate_identities(self.feature_ids, "feature_ids")
|
|
78
|
+
if len(self.feature_ids) != matrix.shape[1]:
|
|
79
|
+
raise ValueError("feature_ids length must equal the number of X columns")
|
|
80
|
+
if indices.ndim != 1 or indices.shape[0] != matrix.shape[0]:
|
|
81
|
+
raise ValueError("source_indices must contain one index per resampled row")
|
|
82
|
+
object.__setattr__(self, "X", matrix)
|
|
83
|
+
object.__setattr__(self, "source_indices", _readonly_indices(indices))
|
|
84
|
+
|
|
85
|
+
def as_population(self) -> Population:
|
|
86
|
+
"""Return the sampled rows as a population for truth materialisation."""
|
|
87
|
+
return Population(self.X, self.feature_ids)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class PopulationPartition:
|
|
92
|
+
"""Disjoint empirical source populations with original-row lineage."""
|
|
93
|
+
|
|
94
|
+
training: Population
|
|
95
|
+
validation: Population
|
|
96
|
+
evaluation: Population
|
|
97
|
+
training_source_indices: NDArray[np.int64]
|
|
98
|
+
validation_source_indices: NDArray[np.int64]
|
|
99
|
+
evaluation_source_indices: NDArray[np.int64]
|
|
100
|
+
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
"""Require one complete, disjoint partition over a common feature universe."""
|
|
103
|
+
populations = (self.training, self.validation, self.evaluation)
|
|
104
|
+
indices = tuple(
|
|
105
|
+
_readonly_indices(integer_indices(values, "partition lineage"))
|
|
106
|
+
for values in (
|
|
107
|
+
self.training_source_indices,
|
|
108
|
+
self.validation_source_indices,
|
|
109
|
+
self.evaluation_source_indices,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
if any(
|
|
113
|
+
values.ndim != 1 or values.size != population.sample_count
|
|
114
|
+
for values, population in zip(indices, populations, strict=True)
|
|
115
|
+
):
|
|
116
|
+
raise ValueError("partition lineage must identify every source row")
|
|
117
|
+
if len({population.feature_ids for population in populations}) != 1:
|
|
118
|
+
raise ValueError("partition populations must share one feature universe")
|
|
119
|
+
sets = tuple(set(values.tolist()) for values in indices)
|
|
120
|
+
if any(len(unique) != values.size for unique, values in zip(sets, indices, strict=True)):
|
|
121
|
+
raise ValueError("partition source identities must be unique within each role")
|
|
122
|
+
if any(
|
|
123
|
+
left & right for position, left in enumerate(sets) for right in sets[position + 1 :]
|
|
124
|
+
):
|
|
125
|
+
raise ValueError("source identities cannot cross partition roles")
|
|
126
|
+
combined = set().union(*sets)
|
|
127
|
+
if combined != set(range(len(combined))):
|
|
128
|
+
raise ValueError("partition lineage must be a complete zero-based source universe")
|
|
129
|
+
object.__setattr__(self, "training_source_indices", indices[0])
|
|
130
|
+
object.__setattr__(self, "validation_source_indices", indices[1])
|
|
131
|
+
object.__setattr__(self, "evaluation_source_indices", indices[2])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def partition_population(
|
|
135
|
+
population: Population,
|
|
136
|
+
*,
|
|
137
|
+
training_fraction: float,
|
|
138
|
+
validation_fraction: float,
|
|
139
|
+
seed: int,
|
|
140
|
+
) -> PopulationPartition:
|
|
141
|
+
"""Partition empirical rows before any complete-row resampling.
|
|
142
|
+
|
|
143
|
+
The evaluation fraction is the remainder after training and validation.
|
|
144
|
+
Fractions determine integer boundaries by floor; every role must retain at
|
|
145
|
+
least one source row.
|
|
146
|
+
"""
|
|
147
|
+
integer(seed, "seed")
|
|
148
|
+
if (
|
|
149
|
+
not np.isfinite(training_fraction)
|
|
150
|
+
or not np.isfinite(validation_fraction)
|
|
151
|
+
or training_fraction <= 0.0
|
|
152
|
+
or validation_fraction <= 0.0
|
|
153
|
+
or training_fraction + validation_fraction >= 1.0
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("population partition settings are invalid")
|
|
156
|
+
training_count = int(np.floor(population.sample_count * training_fraction))
|
|
157
|
+
validation_count = int(np.floor(population.sample_count * validation_fraction))
|
|
158
|
+
evaluation_count = population.sample_count - training_count - validation_count
|
|
159
|
+
if min(training_count, validation_count, evaluation_count) < 1:
|
|
160
|
+
raise ValueError("population partition requires at least one source row per role")
|
|
161
|
+
order = np.random.default_rng(seed).permutation(population.sample_count)
|
|
162
|
+
training_indices = np.asarray(order[:training_count], dtype=np.int64)
|
|
163
|
+
validation_indices = np.asarray(
|
|
164
|
+
order[training_count : training_count + validation_count], dtype=np.int64
|
|
165
|
+
)
|
|
166
|
+
evaluation_indices = np.asarray(order[-evaluation_count:], dtype=np.int64)
|
|
167
|
+
return PopulationPartition(
|
|
168
|
+
Population(population.X[training_indices], population.feature_ids),
|
|
169
|
+
Population(population.X[validation_indices], population.feature_ids),
|
|
170
|
+
Population(population.X[evaluation_indices], population.feature_ids),
|
|
171
|
+
training_indices,
|
|
172
|
+
validation_indices,
|
|
173
|
+
evaluation_indices,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def resample_population(
|
|
178
|
+
population: Population,
|
|
179
|
+
sample_size: int,
|
|
180
|
+
rng: np.random.Generator,
|
|
181
|
+
) -> ResampledPopulation:
|
|
182
|
+
"""Sample complete empirical rows with replacement using ``rng``."""
|
|
183
|
+
integer(sample_size, "sample_size", minimum=1)
|
|
184
|
+
indices = rng.integers(0, population.sample_count, size=sample_size, dtype=np.int64)
|
|
185
|
+
return ResampledPopulation(population.X[indices], population.feature_ids, indices)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sample_population(
|
|
189
|
+
population: Population,
|
|
190
|
+
sample_size: int,
|
|
191
|
+
rng: np.random.Generator,
|
|
192
|
+
*,
|
|
193
|
+
method: SamplingMethod = "with_replacement",
|
|
194
|
+
) -> ResampledPopulation:
|
|
195
|
+
"""Sample complete empirical rows under an explicit replacement policy.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
population: Empirical biomarker population.
|
|
199
|
+
sample_size: Number of rows to return.
|
|
200
|
+
rng: NumPy random generator controlling row selection.
|
|
201
|
+
method: Sampling with replacement or sampling without replacement.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Selected complete rows and their source indices.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ValueError: If the size or method is invalid, or a sample without
|
|
208
|
+
replacement exceeds the source population.
|
|
209
|
+
"""
|
|
210
|
+
if method == "with_replacement":
|
|
211
|
+
return resample_population(population, sample_size, rng)
|
|
212
|
+
if method != "without_replacement":
|
|
213
|
+
raise ValueError("sampling method is unsupported")
|
|
214
|
+
integer(sample_size, "sample_size", minimum=1)
|
|
215
|
+
if sample_size > population.sample_count:
|
|
216
|
+
raise ValueError("sample_size exceeds the population without replacement")
|
|
217
|
+
indices = np.asarray(
|
|
218
|
+
rng.choice(population.sample_count, size=sample_size, replace=False),
|
|
219
|
+
dtype=np.int64,
|
|
220
|
+
)
|
|
221
|
+
return ResampledPopulation(population.X[indices], population.feature_ids, indices)
|