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/generate.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Composition of empirical resampling, truth materialisation and outcome generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
from pyplasmode._validation import indices as integer_indices
|
|
11
|
+
from pyplasmode._validation import integer
|
|
12
|
+
from pyplasmode.data import Population, PopulationPartition, SamplingMethod, sample_population
|
|
13
|
+
from pyplasmode.outcomes import (
|
|
14
|
+
BinaryGeneratedOutcome,
|
|
15
|
+
ContinuousGeneratedOutcome,
|
|
16
|
+
CountGeneratedOutcome,
|
|
17
|
+
GeneratedOutcome,
|
|
18
|
+
OrdinalGeneratedOutcome,
|
|
19
|
+
OutcomeSpec,
|
|
20
|
+
SurvivalGeneratedOutcome,
|
|
21
|
+
generate_outcome,
|
|
22
|
+
)
|
|
23
|
+
from pyplasmode.structures import FeatureSets
|
|
24
|
+
from pyplasmode.truth import (
|
|
25
|
+
CorrelatedTruth,
|
|
26
|
+
CustomTruth,
|
|
27
|
+
MaterializedTruth,
|
|
28
|
+
SparseTruth,
|
|
29
|
+
TruthSpec,
|
|
30
|
+
materialize_truth,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class Plasmode:
|
|
36
|
+
"""An in-memory truth-known sample ready for a user-controlled modelling workflow."""
|
|
37
|
+
|
|
38
|
+
X: NDArray[np.float64]
|
|
39
|
+
feature_ids: tuple[str, ...]
|
|
40
|
+
outcome: GeneratedOutcome
|
|
41
|
+
truth: MaterializedTruth
|
|
42
|
+
source_indices: NDArray[np.int64]
|
|
43
|
+
seed: int
|
|
44
|
+
sampling_method: SamplingMethod
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class PartitionSampleSizes:
|
|
49
|
+
"""Requested sampled row counts for the three source roles."""
|
|
50
|
+
|
|
51
|
+
training: int
|
|
52
|
+
validation: int
|
|
53
|
+
evaluation: int
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
"""Require a positive integer count for every role."""
|
|
57
|
+
for value in (self.training, self.validation, self.evaluation):
|
|
58
|
+
integer(value, "partition sample size", minimum=1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class PartitionedPlasmode:
|
|
63
|
+
"""Three generated roles with original empirical source-row lineage."""
|
|
64
|
+
|
|
65
|
+
training: Plasmode
|
|
66
|
+
validation: Plasmode
|
|
67
|
+
evaluation: Plasmode
|
|
68
|
+
training_source_indices: NDArray[np.int64]
|
|
69
|
+
validation_source_indices: NDArray[np.int64]
|
|
70
|
+
evaluation_source_indices: NDArray[np.int64]
|
|
71
|
+
|
|
72
|
+
def __post_init__(self) -> None:
|
|
73
|
+
"""Prove that no original source identity crosses generated roles."""
|
|
74
|
+
samples = (self.training, self.validation, self.evaluation)
|
|
75
|
+
lineages = tuple(
|
|
76
|
+
integer_indices(values, "generated source lineage")
|
|
77
|
+
for values in (
|
|
78
|
+
self.training_source_indices,
|
|
79
|
+
self.validation_source_indices,
|
|
80
|
+
self.evaluation_source_indices,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
if any(
|
|
84
|
+
lineage.ndim != 1 or lineage.size != sample.X.shape[0]
|
|
85
|
+
for lineage, sample in zip(lineages, samples, strict=True)
|
|
86
|
+
):
|
|
87
|
+
raise ValueError("generated role lineage is incomplete")
|
|
88
|
+
identities = tuple(set(values.tolist()) for values in lineages)
|
|
89
|
+
if any(
|
|
90
|
+
left & right
|
|
91
|
+
for position, left in enumerate(identities)
|
|
92
|
+
for right in identities[position + 1 :]
|
|
93
|
+
):
|
|
94
|
+
raise ValueError("an empirical source identity crosses generated roles")
|
|
95
|
+
for name, lineage in zip(
|
|
96
|
+
(
|
|
97
|
+
"training_source_indices",
|
|
98
|
+
"validation_source_indices",
|
|
99
|
+
"evaluation_source_indices",
|
|
100
|
+
),
|
|
101
|
+
lineages,
|
|
102
|
+
strict=True,
|
|
103
|
+
):
|
|
104
|
+
frozen = np.array(lineage, copy=True)
|
|
105
|
+
frozen.setflags(write=False)
|
|
106
|
+
object.__setattr__(self, name, frozen)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def generate(
|
|
110
|
+
population: Population,
|
|
111
|
+
*,
|
|
112
|
+
truth: TruthSpec,
|
|
113
|
+
outcome: OutcomeSpec,
|
|
114
|
+
sample_size: int,
|
|
115
|
+
seed: int,
|
|
116
|
+
sampling_method: SamplingMethod = "with_replacement",
|
|
117
|
+
feature_sets: FeatureSets | None = None,
|
|
118
|
+
) -> Plasmode:
|
|
119
|
+
"""Resample empirical rows, construct truth and generate a calibrated outcome.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
population: Source matrix of shape (participants, features).
|
|
123
|
+
truth: Mechanism defining latent signal and recovery targets.
|
|
124
|
+
outcome: Observable outcome targets and effect per signal standard deviation.
|
|
125
|
+
sample_size: Positive number of generated rows.
|
|
126
|
+
seed: Non-negative seed; separate child streams control rows, truth and outcome.
|
|
127
|
+
sampling_method: With replacement by default; without replacement cannot
|
|
128
|
+
exceed the source population size.
|
|
129
|
+
feature_sets: Named groups or modules used by the truth specification.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An in-memory plasmode with read-only arrays and local source-row indices.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: Invalid settings or an infeasible truth or sampling request.
|
|
136
|
+
CalibrationError: Outcome targets cannot be represented by the selected model.
|
|
137
|
+
RealizationError: A sampled outcome exceeds its diagnostic tolerance.
|
|
138
|
+
"""
|
|
139
|
+
integer(seed, "seed")
|
|
140
|
+
row_seed, truth_seed, outcome_seed = np.random.SeedSequence(seed).spawn(3)
|
|
141
|
+
sampled = sample_population(
|
|
142
|
+
population,
|
|
143
|
+
sample_size,
|
|
144
|
+
np.random.default_rng(row_seed),
|
|
145
|
+
method=sampling_method,
|
|
146
|
+
)
|
|
147
|
+
materialized = materialize_truth(
|
|
148
|
+
truth,
|
|
149
|
+
sampled.as_population(),
|
|
150
|
+
seed=int(truth_seed.generate_state(1, dtype=np.uint32)[0]),
|
|
151
|
+
feature_sets=feature_sets,
|
|
152
|
+
)
|
|
153
|
+
generated = generate_outcome(outcome, materialized.signal, np.random.default_rng(outcome_seed))
|
|
154
|
+
return Plasmode(
|
|
155
|
+
sampled.X,
|
|
156
|
+
sampled.feature_ids,
|
|
157
|
+
generated,
|
|
158
|
+
materialized,
|
|
159
|
+
sampled.source_indices,
|
|
160
|
+
seed,
|
|
161
|
+
sampling_method,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _freeze_partition_truth(
|
|
166
|
+
truth: TruthSpec,
|
|
167
|
+
population: Population,
|
|
168
|
+
*,
|
|
169
|
+
seed: int,
|
|
170
|
+
feature_sets: FeatureSets | None,
|
|
171
|
+
) -> TruthSpec:
|
|
172
|
+
"""Freeze training-derived identities before generating other roles."""
|
|
173
|
+
if isinstance(truth, CustomTruth):
|
|
174
|
+
raise ValueError("partitioned generation requires a reusable declarative truth")
|
|
175
|
+
if isinstance(truth, SparseTruth) and truth.count is not None:
|
|
176
|
+
materialized = materialize_truth(
|
|
177
|
+
truth,
|
|
178
|
+
population,
|
|
179
|
+
seed=seed,
|
|
180
|
+
feature_sets=feature_sets,
|
|
181
|
+
)
|
|
182
|
+
return SparseTruth(
|
|
183
|
+
features=materialized.direct_features,
|
|
184
|
+
weights=tuple(item.weight for item in materialized.weights),
|
|
185
|
+
max_absolute_correlation=truth.max_absolute_correlation,
|
|
186
|
+
)
|
|
187
|
+
if (
|
|
188
|
+
isinstance(truth, CorrelatedTruth)
|
|
189
|
+
and truth.signal_form == "sentinels"
|
|
190
|
+
and not truth.sentinels
|
|
191
|
+
):
|
|
192
|
+
materialized = materialize_truth(
|
|
193
|
+
truth,
|
|
194
|
+
population,
|
|
195
|
+
seed=seed,
|
|
196
|
+
feature_sets=feature_sets,
|
|
197
|
+
)
|
|
198
|
+
return CorrelatedTruth(
|
|
199
|
+
truth.group_names,
|
|
200
|
+
materialized.direct_features,
|
|
201
|
+
weights=truth.weights,
|
|
202
|
+
signal_form=truth.signal_form,
|
|
203
|
+
)
|
|
204
|
+
return truth
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _slice_outcome(outcome: GeneratedOutcome, rows: slice) -> GeneratedOutcome:
|
|
208
|
+
"""Return one role from an outcome generated under a shared mechanism."""
|
|
209
|
+
if isinstance(outcome, BinaryGeneratedOutcome):
|
|
210
|
+
return BinaryGeneratedOutcome(outcome.values[rows], outcome.calibration)
|
|
211
|
+
if isinstance(outcome, ContinuousGeneratedOutcome):
|
|
212
|
+
return ContinuousGeneratedOutcome(outcome.values[rows], outcome.calibration)
|
|
213
|
+
if isinstance(outcome, CountGeneratedOutcome):
|
|
214
|
+
return CountGeneratedOutcome(
|
|
215
|
+
outcome.values[rows], outcome.exposure[rows], outcome.calibration
|
|
216
|
+
)
|
|
217
|
+
if isinstance(outcome, OrdinalGeneratedOutcome):
|
|
218
|
+
return OrdinalGeneratedOutcome(outcome.codes[rows], outcome.categories, outcome.calibration)
|
|
219
|
+
if isinstance(outcome, SurvivalGeneratedOutcome):
|
|
220
|
+
return SurvivalGeneratedOutcome(
|
|
221
|
+
outcome.time[rows],
|
|
222
|
+
outcome.event[rows],
|
|
223
|
+
outcome.latent_event_time[rows],
|
|
224
|
+
outcome.censoring_time[rows],
|
|
225
|
+
outcome.calibration,
|
|
226
|
+
)
|
|
227
|
+
raise TypeError(f"unsupported generated outcome: {type(outcome).__name__}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def generate_partitioned(
|
|
231
|
+
partition: PopulationPartition,
|
|
232
|
+
*,
|
|
233
|
+
truth: TruthSpec,
|
|
234
|
+
outcome: OutcomeSpec,
|
|
235
|
+
sample_sizes: PartitionSampleSizes,
|
|
236
|
+
seed: int,
|
|
237
|
+
sampling_method: SamplingMethod = "with_replacement",
|
|
238
|
+
feature_sets: FeatureSets | None = None,
|
|
239
|
+
) -> PartitionedPlasmode:
|
|
240
|
+
"""Generate source-disjoint roles under one shared outcome mechanism.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
partition: Prior source partition from ``partition_population``.
|
|
244
|
+
truth: Transferable built-in mechanism; custom callables are unsupported.
|
|
245
|
+
outcome: Observable outcome targets over the combined generated population.
|
|
246
|
+
sample_sizes: Positive training, validation and evaluation counts.
|
|
247
|
+
seed: Non-negative root seed for role sampling and mechanism generation.
|
|
248
|
+
sampling_method: Explicit with- or without-replacement policy within each role.
|
|
249
|
+
feature_sets: Required named groups or modules.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Three plasmodes plus original source identities, disjoint across roles.
|
|
253
|
+
Truth identities are frozen on training source rows; sampled roles share
|
|
254
|
+
one signal scaling and outcome calibration. A role's realised rate can
|
|
255
|
+
differ from the combined-population target through sampling variation.
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
ValueError: Invalid roles, unsupported custom truth, or infeasible generation.
|
|
259
|
+
CalibrationError: Outcome targets cannot be represented.
|
|
260
|
+
RealizationError: The combined generated outcome fails a diagnostic check.
|
|
261
|
+
"""
|
|
262
|
+
integer(seed, "seed")
|
|
263
|
+
role_seeds = np.random.SeedSequence(seed).spawn(6)
|
|
264
|
+
(
|
|
265
|
+
truth_selection_seed,
|
|
266
|
+
training_seed,
|
|
267
|
+
validation_seed,
|
|
268
|
+
evaluation_seed,
|
|
269
|
+
truth_materialization_seed,
|
|
270
|
+
outcome_seed,
|
|
271
|
+
) = role_seeds
|
|
272
|
+
frozen_truth = _freeze_partition_truth(
|
|
273
|
+
truth,
|
|
274
|
+
partition.training,
|
|
275
|
+
seed=int(truth_selection_seed.generate_state(1, dtype=np.uint32)[0]),
|
|
276
|
+
feature_sets=feature_sets,
|
|
277
|
+
)
|
|
278
|
+
role_inputs = (
|
|
279
|
+
(
|
|
280
|
+
partition.training,
|
|
281
|
+
partition.training_source_indices,
|
|
282
|
+
sample_sizes.training,
|
|
283
|
+
training_seed,
|
|
284
|
+
),
|
|
285
|
+
(
|
|
286
|
+
partition.validation,
|
|
287
|
+
partition.validation_source_indices,
|
|
288
|
+
sample_sizes.validation,
|
|
289
|
+
validation_seed,
|
|
290
|
+
),
|
|
291
|
+
(
|
|
292
|
+
partition.evaluation,
|
|
293
|
+
partition.evaluation_source_indices,
|
|
294
|
+
sample_sizes.evaluation,
|
|
295
|
+
evaluation_seed,
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
sampled = []
|
|
299
|
+
original_lineage: list[NDArray[np.int64]] = []
|
|
300
|
+
for population, source_indices, sample_size, role_seed in role_inputs:
|
|
301
|
+
role_seed_value = int(role_seed.generate_state(1, dtype=np.uint32)[0])
|
|
302
|
+
sample = sample_population(
|
|
303
|
+
population,
|
|
304
|
+
sample_size,
|
|
305
|
+
np.random.default_rng(role_seed_value),
|
|
306
|
+
method=sampling_method,
|
|
307
|
+
)
|
|
308
|
+
sampled.append((sample, role_seed_value))
|
|
309
|
+
original_lineage.append(source_indices[sample.source_indices])
|
|
310
|
+
combined = Population(
|
|
311
|
+
np.vstack(tuple(sample.X for sample, _ in sampled)),
|
|
312
|
+
partition.training.feature_ids,
|
|
313
|
+
)
|
|
314
|
+
materialized = materialize_truth(
|
|
315
|
+
frozen_truth,
|
|
316
|
+
combined,
|
|
317
|
+
seed=int(truth_materialization_seed.generate_state(1, dtype=np.uint32)[0]),
|
|
318
|
+
feature_sets=feature_sets,
|
|
319
|
+
)
|
|
320
|
+
generated_outcome = generate_outcome(
|
|
321
|
+
outcome,
|
|
322
|
+
materialized.signal,
|
|
323
|
+
np.random.default_rng(outcome_seed),
|
|
324
|
+
)
|
|
325
|
+
generated: list[Plasmode] = []
|
|
326
|
+
start = 0
|
|
327
|
+
for sample, role_seed_value in sampled:
|
|
328
|
+
stop = start + sample.X.shape[0]
|
|
329
|
+
rows = slice(start, stop)
|
|
330
|
+
generated.append(
|
|
331
|
+
Plasmode(
|
|
332
|
+
sample.X,
|
|
333
|
+
sample.feature_ids,
|
|
334
|
+
_slice_outcome(generated_outcome, rows),
|
|
335
|
+
replace(materialized, signal=materialized.signal[rows]),
|
|
336
|
+
sample.source_indices,
|
|
337
|
+
role_seed_value,
|
|
338
|
+
sampling_method,
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
start = stop
|
|
342
|
+
return PartitionedPlasmode(
|
|
343
|
+
generated[0],
|
|
344
|
+
generated[1],
|
|
345
|
+
generated[2],
|
|
346
|
+
original_lineage[0],
|
|
347
|
+
original_lineage[1],
|
|
348
|
+
original_lineage[2],
|
|
349
|
+
)
|