pyplasmode 1.0.0__tar.gz → 1.2.0__tar.gz

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 (103) hide show
  1. pyplasmode-1.2.0/CHANGELOG.md +60 -0
  2. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/CITATION.cff +1 -1
  3. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/CONTRIBUTING.md +11 -1
  4. pyplasmode-1.2.0/PKG-INFO +168 -0
  5. pyplasmode-1.2.0/README.md +138 -0
  6. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/api.md +12 -5
  7. pyplasmode-1.2.0/docs/assets/concepts/README.md +8 -0
  8. pyplasmode-1.2.0/docs/benchmark-demo.md +80 -0
  9. pyplasmode-1.2.0/docs/calibration.md +108 -0
  10. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/evaluation.md +86 -28
  11. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/gallery.md +6 -6
  12. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/index.md +27 -11
  13. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/methods.md +38 -21
  14. pyplasmode-1.2.0/docs/metrics.md +174 -0
  15. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/outcomes.md +18 -14
  16. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/privacy-and-limitations.md +6 -2
  17. pyplasmode-1.2.0/docs/references.md +77 -0
  18. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/structures-and-extensions.md +5 -4
  19. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/truths.md +6 -4
  20. pyplasmode-1.2.0/docs/tutorial.md +179 -0
  21. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/validation.md +23 -9
  22. pyplasmode-1.2.0/examples/benchmark_recovery_demo.py +163 -0
  23. pyplasmode-1.2.0/examples/metrics_calibration.py +77 -0
  24. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/quickstart.py +3 -3
  25. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/survival.py +3 -2
  26. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/mkdocs.yml +6 -2
  27. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/pyproject.toml +13 -7
  28. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/__init__.py +8 -2
  29. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/data.py +3 -3
  30. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/evaluate.py +99 -10
  31. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/generate.py +3 -3
  32. pyplasmode-1.2.0/src/pyplasmode/metrics.py +415 -0
  33. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/nominations.py +74 -58
  34. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/outcomes.py +180 -19
  35. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/results.py +22 -2
  36. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/structures.py +5 -5
  37. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/truth.py +2 -2
  38. pyplasmode-1.2.0/tests/integration/test_benchmark_demo.py +56 -0
  39. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/test_documented_workflows.py +23 -3
  40. pyplasmode-1.2.0/tests/statistical/test_auc_calibration.py +62 -0
  41. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/statistical/test_matched_recovery.py +65 -0
  42. pyplasmode-1.2.0/tests/statistical/test_metrics.py +100 -0
  43. pyplasmode-1.2.0/tests/unit/test_auc_calibration.py +176 -0
  44. pyplasmode-1.2.0/tests/unit/test_metrics.py +268 -0
  45. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_nominations.py +20 -1
  46. pyplasmode-1.2.0/tests/unit/test_paired_replicates.py +115 -0
  47. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_public_contracts.py +7 -7
  48. pyplasmode-1.0.0/CHANGELOG.md +0 -18
  49. pyplasmode-1.0.0/PKG-INFO +0 -142
  50. pyplasmode-1.0.0/README.md +0 -116
  51. pyplasmode-1.0.0/docs/assets/concepts/README.md +0 -10
  52. pyplasmode-1.0.0/docs/assets/concepts/plasmode-workflow.pdf +0 -0
  53. pyplasmode-1.0.0/docs/assets/concepts/signal-recovery.pdf +0 -0
  54. pyplasmode-1.0.0/docs/references.md +0 -30
  55. pyplasmode-1.0.0/docs/tutorial.md +0 -140
  56. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/.gitignore +0 -0
  57. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/LICENSE +0 -0
  58. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/THIRD_PARTY_NOTICES.md +0 -0
  59. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/concepts/plasmode-workflow.png +0 -0
  60. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/concepts/signal-recovery.png +0 -0
  61. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/correlated-recovery.csv +0 -0
  62. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/correlated-recovery.pdf +0 -0
  63. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/correlated-recovery.png +0 -0
  64. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/correlated-recovery.svg +0 -0
  65. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/feature-correlations.csv +0 -0
  66. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/outcome-calibration.json +0 -0
  67. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/outcome-families.csv +0 -0
  68. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/outcome-families.pdf +0 -0
  69. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/outcome-families.png +0 -0
  70. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/outcome-families.svg +0 -0
  71. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/signal-shapes.csv +0 -0
  72. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/signal-shapes.pdf +0 -0
  73. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/signal-shapes.png +0 -0
  74. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/patterns/signal-shapes.svg +0 -0
  75. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/external-conformance.csv +0 -0
  76. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/null-control.csv +0 -0
  77. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/outcome-reproducibility.csv +0 -0
  78. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/truth-recovery.csv +0 -0
  79. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/validation.pdf +0 -0
  80. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/validation.png +0 -0
  81. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/docs/assets/validation/validation.svg +0 -0
  82. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/custom_signal.py +0 -0
  83. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/pattern_gallery.py +0 -0
  84. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/recovery.py +0 -0
  85. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/examples/validation_vignette.py +0 -0
  86. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/_validation.py +0 -0
  87. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/src/pyplasmode/py.typed +0 -0
  88. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/__init__.py +0 -0
  89. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/__init__.py +0 -0
  90. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/test_fractional_recovery.py +0 -0
  91. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/test_generate_evaluate.py +0 -0
  92. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/test_outcome_composition.py +0 -0
  93. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/integration/test_pattern_gallery.py +0 -0
  94. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/statistical/__init__.py +0 -0
  95. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/statistical/test_evaluate.py +0 -0
  96. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/statistical/test_outcomes.py +0 -0
  97. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/statistical/test_resampling.py +0 -0
  98. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/__init__.py +0 -0
  99. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_data.py +0 -0
  100. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_evaluate.py +0 -0
  101. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_outcomes.py +0 -0
  102. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_structures.py +0 -0
  103. {pyplasmode-1.0.0 → pyplasmode-1.2.0}/tests/unit/test_truth.py +0 -0
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ ## 1.2.0 — 2026-11-09
4
+
5
+ ### Added
6
+
7
+ - `pyplasmode.metrics`: chance-corrected selection stability with unequal budgets
8
+ and ties, tie-aware RBO, inclusion-first consensus ranks, IPCW cumulative/dynamic
9
+ AUROC, paired participant bootstrap differences and oracle-minus-fitted
10
+ `discrimination_gap` (also available as `learnability_gap`).
11
+ - `target_auc` for binary and survival outcomes, reporting expected oracle AUROC
12
+ and the solved odds/hazard ratio while retaining marginal risk.
13
+ - Metrics and calibration guides, executable examples and a synthetic Cox,
14
+ ridge and XGBoost benchmark comparing held-out AUROC and chance-adjusted recovery.
15
+ Model dependencies are supplied by the optional `benchmark` extra.
16
+ - Identity-aligned paired replicate comparisons with explicit failed/unmatched
17
+ pair counts and conditional Monte Carlo uncertainty.
18
+ - `allow_subset=True` for fractional recovery, matched recovery and outcome
19
+ specificity, preserving generating denominators and the available-panel reference.
20
+ - Python 3.15 support and NumPy string arrays as bootstrap participant IDs.
21
+
22
+ ### Changed
23
+
24
+ - RBO defaults to Webber whole-tie prefixes; independent tie resolution remains
25
+ available through `tie_method="independent"`.
26
+ - Censored AUROC defaults to exact score ties. Use `tied_tolerance=1e-8` to
27
+ reproduce scikit-survival's default score-tie convention.
28
+ - Default survival AUROC horizons precede the administrative cutoff; an explicit
29
+ later administrative cutoff is supported.
30
+ - Calibration parameters support direct lookup, and `metrics` is discoverable
31
+ through the root package. Unattainable target AUROCs raise explicitly.
32
+ - The validation extra requires statsmodels 0.14.6 or later. Source distributions
33
+ omit the unreferenced concept PDFs, which remain in the repository.
34
+
35
+ ### Fixed
36
+
37
+ - Never-selected consensus features sort below selected features even with wide ties.
38
+ - AUROC-targeted calibration tolerates single extreme signal observations and
39
+ rejects requests above the supported effect bracket without clipping.
40
+ - Edgeless graph regions report unavailable edge coverage and expose the target
41
+ edge count while retaining node recovery.
42
+ - Panel restrictions retain module weights, group proxies and joint interaction
43
+ requirements rather than redefining the generating truth.
44
+
45
+ ## 1.0.0
46
+
47
+ First public release. PyPlasmode provides outcome generation, feature-recovery evaluation,
48
+ worked examples and reference validation for biomarker benchmarking.
49
+
50
+ ### Included in this release
51
+
52
+ - Empirical complete-row resampling and source-partitioned generation.
53
+ - Null, sparse, correlated-group, module, interaction and custom signals.
54
+ - Five outcome families calibrated in observable units.
55
+ - Recovery, prediction, panel, reconstruction and rank-stability evaluation.
56
+ - API documentation, worked examples, validation figures and cross-platform CI.
57
+ - A visual gallery of signal shapes, correlated-feature recovery and outcome families,
58
+ with editable vector figures and an executable source-data example.
59
+ - Python 3.12-3.14 and scikit-learn 1.6 through the current 1.x series.
60
+ - BSD 3-Clause licensing.
@@ -2,7 +2,7 @@ cff-version: 1.2.0
2
2
  message: "If you use PyPlasmode in your research, please cite this software using the metadata below."
3
3
  title: "PyPlasmode"
4
4
  type: software
5
- version: 1.0.0
5
+ version: 1.2.0
6
6
  repository-code: "https://github.com/BradSegal/PyPlasmode"
7
7
  license: BSD-3-Clause
8
8
  authors:
@@ -4,7 +4,7 @@ Contributions to generators, evaluation functions, documentation and examples ar
4
4
  For a new signal or outcome family, describe the intended use and mathematical definition
5
5
  in an issue before implementing it.
6
6
 
7
- From a checkout, install a supported Python version (3.12-3.14) and uv, then run:
7
+ From a checkout, install a supported Python version (3.12-3.15) and uv, then run:
8
8
 
9
9
  ```bash
10
10
  uv sync --extra validation --group docs
@@ -18,6 +18,10 @@ uv run twine check --strict dist/*
18
18
  uv run check-wheel-contents dist/*.whl
19
19
  ```
20
20
 
21
+ Add `--extra benchmark` to `uv sync` to run the synthetic Cox/ridge/XGBoost
22
+ example and its integration test. Without those optional dependencies the
23
+ integration test is explicitly skipped.
24
+
21
25
  Run `python examples/validation_vignette.py --output validation-output` to regenerate the
22
26
  synthetic validation figures and tables. The `validation` extra is included in the setup above.
23
27
 
@@ -46,3 +50,9 @@ Record random seeds and dependency versions when sharing reproducible analyses.
46
50
  The CI matrix covers minimum numerical dependencies on Python 3.12 and current dependencies
47
51
  on supported Python versions, with Linux, Windows and macOS jobs. Test dependency changes
48
52
  against this matrix. Build release artifacts from the tagged version after CI passes.
53
+
54
+ Attach the checked wheel and source distribution to a draft GitHub release, then publish
55
+ the release. The `publish.yml` workflow validates those distributions and uploads them to
56
+ PyPI through the `pypi` trusted-publishing environment. For an existing release, run
57
+ `gh workflow run publish.yml --ref main -f tag=v1.0.0` with its tag. No PyPI API token is
58
+ required, and already-published versions must not be replaced.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyplasmode
3
+ Version: 1.2.0
4
+ Summary: Simulate outcomes from feature data and evaluate prediction and feature selection.
5
+ Project-URL: Repository, https://github.com/BradSegal/PyPlasmode
6
+ Project-URL: Documentation, https://bradsegal.github.io/PyPlasmode/
7
+ Project-URL: Issues, https://github.com/BradSegal/PyPlasmode/issues
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
18
+ Requires-Python: <3.16,>=3.12
19
+ Requires-Dist: numpy<3,>=2.0
20
+ Requires-Dist: scikit-learn<2,>=1.6
21
+ Requires-Dist: scipy<2,>=1.14
22
+ Provides-Extra: benchmark
23
+ Requires-Dist: scikit-survival<1,>=0.24; extra == 'benchmark'
24
+ Requires-Dist: threadpoolctl<4,>=3.1; extra == 'benchmark'
25
+ Requires-Dist: xgboost<4,>=2.1; extra == 'benchmark'
26
+ Provides-Extra: validation
27
+ Requires-Dist: matplotlib<4,>=3.10; extra == 'validation'
28
+ Requires-Dist: statsmodels<0.16,>=0.14.6; extra == 'validation'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # PyPlasmode
32
+
33
+ PyPlasmode tests prediction and feature selection against a known signal. It combines your
34
+ feature data with simulated outcomes, so you can measure both how accurately a model predicts
35
+ and how well it identifies the features used to generate those outcomes.
36
+
37
+ Built for biomarker benchmarking, the library works with numeric tabular data more generally.
38
+ Its inputs are a matrix of observations and named features, not a particular assay or dataset.
39
+ A plasmode uses observed measurements to retain realistic feature relationships while you
40
+ control the outcome-generating mechanism. Synthetic matrices work too, as in the examples below.
41
+
42
+ Use it with your existing modelling workflow: generate a dataset, fit and tune your models,
43
+ then evaluate their predictions, feature rankings or selected panels.
44
+
45
+ [![From observed measurements to simulated outcomes, model predictions and feature recovery](https://raw.githubusercontent.com/BradSegal/PyPlasmode/main/docs/assets/concepts/plasmode-workflow.png)](https://bradsegal.github.io/PyPlasmode/methods/)
46
+
47
+ The same generated dataset supports two comparisons: predictions against outcomes, and
48
+ selected features against the signal that generated them.
49
+
50
+ Explore the [visual gallery](https://bradsegal.github.io/PyPlasmode/gallery/) for signal patterns, correlated-feature recovery
51
+ and the five outcome families.
52
+
53
+ The [feature-selection tutorial](https://bradsegal.github.io/PyPlasmode/tutorial/) follows a complete example from correlated
54
+ measurements through model fitting, attribution and recovery above chance.
55
+
56
+ ## Installation
57
+
58
+ Requires Python 3.12-3.15. Install PyPlasmode 1.2.0 from [PyPI](https://pypi.org/project/pyplasmode/):
59
+
60
+ ```bash
61
+ python -m pip install pyplasmode
62
+ ```
63
+
64
+ The runtime dependencies are NumPy, SciPy and scikit-learn. To include the
65
+ reference validation and survival examples:
66
+
67
+ ```bash
68
+ python -m pip install "pyplasmode[validation]"
69
+ ```
70
+
71
+ ## Ten-minute example
72
+
73
+ ```python
74
+ import numpy as np
75
+ from sklearn.linear_model import LogisticRegression
76
+ import pyplasmode as ppm
77
+
78
+ rng = np.random.default_rng(1)
79
+ population = ppm.Population(
80
+ rng.normal(size=(2_000, 100)),
81
+ tuple(f"feature_{index}" for index in range(100)),
82
+ )
83
+
84
+ partition = ppm.partition_population(
85
+ population,
86
+ training_fraction=0.6,
87
+ validation_fraction=0.2,
88
+ seed=7,
89
+ )
90
+ samples = ppm.generate_partitioned(
91
+ partition,
92
+ truth=ppm.SparseTruth(features=("feature_3", "feature_17", "feature_42")),
93
+ outcome=ppm.BinaryOutcome(probability=0.15, odds_ratio=2.0),
94
+ sample_sizes=ppm.PartitionSampleSizes(1_200, 400, 400),
95
+ sampling_method="without_replacement",
96
+ seed=42,
97
+ )
98
+
99
+ train, test = samples.training, samples.evaluation
100
+ model = LogisticRegression(max_iter=1_000).fit(train.X, train.outcome.values)
101
+ ranking = tuple(train.feature_ids[index] for index in np.argsort(-np.abs(model.coef_[0])))
102
+
103
+ recovery = ppm.evaluate_ranking(ranking, train.truth, depths=(10, 20, 50))
104
+ prediction = ppm.evaluate_prediction(
105
+ test.outcome.values,
106
+ model.predict_proba(test.X)[:, 1],
107
+ metric="roc_auc",
108
+ )
109
+ print(recovery.at_depth[0].recall)
110
+ print(prediction.value)
111
+ ```
112
+
113
+ Recall measures how many of the three generating features appear in the model's top ten;
114
+ AUC measures prediction on held-out observations. Repeat the comparison with different seeds
115
+ to estimate average performance and its Monte Carlo uncertainty. In 1.2.0,
116
+ `compare_paired_replicates` aligns procedures by world identity before computing
117
+ paired effects and explicitly counts failed or unmatched pairs. See
118
+ [paired evaluation](https://bradsegal.github.io/PyPlasmode/evaluation/#paired-procedure-comparisons).
119
+
120
+ Source rows are split before resampling, keeping training, validation and test
121
+ rows separate. This example uses a fixed model; the validation sample is available for
122
+ hyperparameter tuning. See [Methods](https://bradsegal.github.io/PyPlasmode/methods/) for the generation process.
123
+
124
+ ## What can be varied
125
+
126
+ - **Signal:** null, sparse features, correlated groups, distributed modules, pairwise
127
+ interactions or a custom function.
128
+ - **Outcome:** binary probability, continuous mean and spread, count rate, ordinal category
129
+ probabilities or cumulative event risks at native-time horizons.
130
+ - **Evaluation:** feature and group recovery, weighted-module and graph-region coverage,
131
+ signal reconstruction, prediction accuracy, panel performance and ranking stability.
132
+ Additional functions handle tied scores, incomplete selections and matched chance references.
133
+
134
+ Start with [Methods](https://bradsegal.github.io/PyPlasmode/methods/), then choose a [signal](https://bradsegal.github.io/PyPlasmode/truths/) and
135
+ [outcome](https://bradsegal.github.io/PyPlasmode/outcomes/). The [evaluation guide](https://bradsegal.github.io/PyPlasmode/evaluation/) defines each result.
136
+ The [validation example](https://bradsegal.github.io/PyPlasmode/validation/) checks the generators and recovery metrics against
137
+ known values and reference estimators.
138
+
139
+ The executable [quickstart](https://github.com/BradSegal/PyPlasmode/blob/main/examples/quickstart.py),
140
+ [survival example](https://github.com/BradSegal/PyPlasmode/blob/main/examples/survival.py)
141
+ and [custom signal](https://github.com/BradSegal/PyPlasmode/blob/main/examples/custom_signal.py)
142
+ use synthetic inputs. The [documentation](https://bradsegal.github.io/PyPlasmode/)
143
+ includes the generation methods, evaluation definitions and API reference.
144
+
145
+ ## Working with empirical data
146
+
147
+ Resampling preserves the source data's feature relationships and missingness patterns.
148
+ It can reproduce individual records, so generated datasets require the same privacy protections
149
+ as their source. See [Data handling](https://bradsegal.github.io/PyPlasmode/privacy-and-limitations/).
150
+
151
+ ## Licence
152
+
153
+ PyPlasmode uses the [BSD 3-Clause License](https://github.com/BradSegal/PyPlasmode/blob/main/LICENSE).
154
+
155
+ ## New in 1.2.0
156
+
157
+ Version 1.2.0 extends generation and evaluation. `pyplasmode.metrics` adds chance-corrected
158
+ selection stability, tie-aware RBO, consensus ranks, censored cumulative/dynamic
159
+ AUROC, paired participant bootstrap differences and discrimination gaps. Binary
160
+ and survival specifications accept `target_auc` and report the solved effect
161
+ and expected oracle AUROC. See [metrics](https://bradsegal.github.io/PyPlasmode/metrics/),
162
+ [calibration](https://bradsegal.github.io/PyPlasmode/calibration/) and the [worked example](https://github.com/BradSegal/PyPlasmode/blob/main/examples/metrics_calibration.py).
163
+
164
+ The [synthetic benchmark](https://bradsegal.github.io/PyPlasmode/benchmark-demo/) fits Cox, ridge and XGBoost
165
+ across three signal mechanisms and three target oracle AUROCs, then compares
166
+ held-out AUROC with chance-adjusted top-20 recovery. Install the optional
167
+ `benchmark` extra to run `python examples/benchmark_recovery_demo.py`.
168
+ Install it with `python -m pip install "pyplasmode[benchmark]"`.
@@ -0,0 +1,138 @@
1
+ # PyPlasmode
2
+
3
+ PyPlasmode tests prediction and feature selection against a known signal. It combines your
4
+ feature data with simulated outcomes, so you can measure both how accurately a model predicts
5
+ and how well it identifies the features used to generate those outcomes.
6
+
7
+ Built for biomarker benchmarking, the library works with numeric tabular data more generally.
8
+ Its inputs are a matrix of observations and named features, not a particular assay or dataset.
9
+ A plasmode uses observed measurements to retain realistic feature relationships while you
10
+ control the outcome-generating mechanism. Synthetic matrices work too, as in the examples below.
11
+
12
+ Use it with your existing modelling workflow: generate a dataset, fit and tune your models,
13
+ then evaluate their predictions, feature rankings or selected panels.
14
+
15
+ [![From observed measurements to simulated outcomes, model predictions and feature recovery](https://raw.githubusercontent.com/BradSegal/PyPlasmode/main/docs/assets/concepts/plasmode-workflow.png)](https://bradsegal.github.io/PyPlasmode/methods/)
16
+
17
+ The same generated dataset supports two comparisons: predictions against outcomes, and
18
+ selected features against the signal that generated them.
19
+
20
+ Explore the [visual gallery](https://bradsegal.github.io/PyPlasmode/gallery/) for signal patterns, correlated-feature recovery
21
+ and the five outcome families.
22
+
23
+ The [feature-selection tutorial](https://bradsegal.github.io/PyPlasmode/tutorial/) follows a complete example from correlated
24
+ measurements through model fitting, attribution and recovery above chance.
25
+
26
+ ## Installation
27
+
28
+ Requires Python 3.12-3.15. Install PyPlasmode 1.2.0 from [PyPI](https://pypi.org/project/pyplasmode/):
29
+
30
+ ```bash
31
+ python -m pip install pyplasmode
32
+ ```
33
+
34
+ The runtime dependencies are NumPy, SciPy and scikit-learn. To include the
35
+ reference validation and survival examples:
36
+
37
+ ```bash
38
+ python -m pip install "pyplasmode[validation]"
39
+ ```
40
+
41
+ ## Ten-minute example
42
+
43
+ ```python
44
+ import numpy as np
45
+ from sklearn.linear_model import LogisticRegression
46
+ import pyplasmode as ppm
47
+
48
+ rng = np.random.default_rng(1)
49
+ population = ppm.Population(
50
+ rng.normal(size=(2_000, 100)),
51
+ tuple(f"feature_{index}" for index in range(100)),
52
+ )
53
+
54
+ partition = ppm.partition_population(
55
+ population,
56
+ training_fraction=0.6,
57
+ validation_fraction=0.2,
58
+ seed=7,
59
+ )
60
+ samples = ppm.generate_partitioned(
61
+ partition,
62
+ truth=ppm.SparseTruth(features=("feature_3", "feature_17", "feature_42")),
63
+ outcome=ppm.BinaryOutcome(probability=0.15, odds_ratio=2.0),
64
+ sample_sizes=ppm.PartitionSampleSizes(1_200, 400, 400),
65
+ sampling_method="without_replacement",
66
+ seed=42,
67
+ )
68
+
69
+ train, test = samples.training, samples.evaluation
70
+ model = LogisticRegression(max_iter=1_000).fit(train.X, train.outcome.values)
71
+ ranking = tuple(train.feature_ids[index] for index in np.argsort(-np.abs(model.coef_[0])))
72
+
73
+ recovery = ppm.evaluate_ranking(ranking, train.truth, depths=(10, 20, 50))
74
+ prediction = ppm.evaluate_prediction(
75
+ test.outcome.values,
76
+ model.predict_proba(test.X)[:, 1],
77
+ metric="roc_auc",
78
+ )
79
+ print(recovery.at_depth[0].recall)
80
+ print(prediction.value)
81
+ ```
82
+
83
+ Recall measures how many of the three generating features appear in the model's top ten;
84
+ AUC measures prediction on held-out observations. Repeat the comparison with different seeds
85
+ to estimate average performance and its Monte Carlo uncertainty. In 1.2.0,
86
+ `compare_paired_replicates` aligns procedures by world identity before computing
87
+ paired effects and explicitly counts failed or unmatched pairs. See
88
+ [paired evaluation](https://bradsegal.github.io/PyPlasmode/evaluation/#paired-procedure-comparisons).
89
+
90
+ Source rows are split before resampling, keeping training, validation and test
91
+ rows separate. This example uses a fixed model; the validation sample is available for
92
+ hyperparameter tuning. See [Methods](https://bradsegal.github.io/PyPlasmode/methods/) for the generation process.
93
+
94
+ ## What can be varied
95
+
96
+ - **Signal:** null, sparse features, correlated groups, distributed modules, pairwise
97
+ interactions or a custom function.
98
+ - **Outcome:** binary probability, continuous mean and spread, count rate, ordinal category
99
+ probabilities or cumulative event risks at native-time horizons.
100
+ - **Evaluation:** feature and group recovery, weighted-module and graph-region coverage,
101
+ signal reconstruction, prediction accuracy, panel performance and ranking stability.
102
+ Additional functions handle tied scores, incomplete selections and matched chance references.
103
+
104
+ Start with [Methods](https://bradsegal.github.io/PyPlasmode/methods/), then choose a [signal](https://bradsegal.github.io/PyPlasmode/truths/) and
105
+ [outcome](https://bradsegal.github.io/PyPlasmode/outcomes/). The [evaluation guide](https://bradsegal.github.io/PyPlasmode/evaluation/) defines each result.
106
+ The [validation example](https://bradsegal.github.io/PyPlasmode/validation/) checks the generators and recovery metrics against
107
+ known values and reference estimators.
108
+
109
+ The executable [quickstart](https://github.com/BradSegal/PyPlasmode/blob/main/examples/quickstart.py),
110
+ [survival example](https://github.com/BradSegal/PyPlasmode/blob/main/examples/survival.py)
111
+ and [custom signal](https://github.com/BradSegal/PyPlasmode/blob/main/examples/custom_signal.py)
112
+ use synthetic inputs. The [documentation](https://bradsegal.github.io/PyPlasmode/)
113
+ includes the generation methods, evaluation definitions and API reference.
114
+
115
+ ## Working with empirical data
116
+
117
+ Resampling preserves the source data's feature relationships and missingness patterns.
118
+ It can reproduce individual records, so generated datasets require the same privacy protections
119
+ as their source. See [Data handling](https://bradsegal.github.io/PyPlasmode/privacy-and-limitations/).
120
+
121
+ ## Licence
122
+
123
+ PyPlasmode uses the [BSD 3-Clause License](https://github.com/BradSegal/PyPlasmode/blob/main/LICENSE).
124
+
125
+ ## New in 1.2.0
126
+
127
+ Version 1.2.0 extends generation and evaluation. `pyplasmode.metrics` adds chance-corrected
128
+ selection stability, tie-aware RBO, consensus ranks, censored cumulative/dynamic
129
+ AUROC, paired participant bootstrap differences and discrimination gaps. Binary
130
+ and survival specifications accept `target_auc` and report the solved effect
131
+ and expected oracle AUROC. See [metrics](https://bradsegal.github.io/PyPlasmode/metrics/),
132
+ [calibration](https://bradsegal.github.io/PyPlasmode/calibration/) and the [worked example](https://github.com/BradSegal/PyPlasmode/blob/main/examples/metrics_calibration.py).
133
+
134
+ The [synthetic benchmark](https://bradsegal.github.io/PyPlasmode/benchmark-demo/) fits Cox, ridge and XGBoost
135
+ across three signal mechanisms and three target oracle AUROCs, then compares
136
+ held-out AUROC with chance-adjusted top-20 recovery. Install the optional
137
+ `benchmark` extra to run `python examples/benchmark_recovery_demo.py`.
138
+ Install it with `python -m pip install "pyplasmode[benchmark]"`.
@@ -1,19 +1,19 @@
1
1
  # API reference
2
2
 
3
3
  The common entry points are exported from `pyplasmode`. Matrices have shape
4
- `(participants, features)`; `feature_ids` supplies one unique string per column.
4
+ `(observations, features)`; `feature_ids` supplies one unique string per column.
5
5
  Generated arrays are read-only. Convert result dataclasses to plain records with
6
6
  `dataclasses.asdict` for your reporting workflow.
7
7
 
8
8
  Use `generate` for one sample or `partition_population` followed by
9
- `generate_partitioned` for source-disjoint development and evaluation. Access
9
+ `generate_partitioned` to keep source rows separate between training, validation and test. Access
10
10
  `outcome.values` for binary, continuous and count outcomes, `outcome.codes` for
11
11
  ordinal outcomes, and `outcome.time` plus `outcome.event` for survival outcomes.
12
12
  Survival times are in days; latent event and censoring times beyond administrative
13
13
  follow-up are represented by infinity.
14
14
 
15
- The rendered reference includes signatures, types and result fields. See the
16
- methods and evaluation guides for the scientific definitions.
15
+ The reference below lists signatures, types and result fields. See
16
+ [Methods](methods.md) for generation and [Evaluation](evaluation.md) for scoring.
17
17
 
18
18
  ## Populations and sampling
19
19
 
@@ -39,10 +39,17 @@ methods and evaluation guides for the scientific definitions.
39
39
 
40
40
  ::: pyplasmode.evaluate
41
41
 
42
- ## Fractional nominations and matched references
42
+ ## Tied selections and matched references
43
+
44
+ The `nominations` module represents selections with membership weights, so tied scores
45
+ can share a place in a leading list. It also compares recovery with matched random selection.
43
46
 
44
47
  ::: pyplasmode.nominations
45
48
 
46
49
  ## Evaluation results
47
50
 
48
51
  ::: pyplasmode.results
52
+
53
+ ## Selection agreement and censored discrimination
54
+
55
+ ::: pyplasmode.metrics
@@ -0,0 +1,8 @@
1
+ # Concept diagrams
2
+
3
+ These diagrams illustrate outcome generation and feature recovery in PyPlasmode.
4
+ The repository retains the vector PDFs for editing and print use; the documentation
5
+ uses PNG previews. The source distribution includes the previews.
6
+
7
+ - `plasmode-workflow`: observed features, generating signal, outcomes and evaluation.
8
+ - `signal-recovery`: generating contributors and features selected by a fitted model.
@@ -0,0 +1,80 @@
1
+ # Prediction and biomarker recovery on synthetic data
2
+
3
+ [`examples/benchmark_recovery_demo.py`](https://github.com/BradSegal/PyPlasmode/blob/main/examples/benchmark_recovery_demo.py)
4
+ is a small, self-contained comparison of Cox, ridge and XGBoost. It generates
5
+ all features and outcomes in memory; it requires no participant data or downloads.
6
+
7
+ On macOS, install the OpenMP runtime required by XGBoost with
8
+ `brew install libomp`. Then install the optional model dependencies and download
9
+ the linked example:
10
+
11
+ ```bash
12
+ python -m pip install "pyplasmode[benchmark]"
13
+ python benchmark_recovery_demo.py --output benchmark-results.csv
14
+ ```
15
+
16
+ The core package does not require scikit-survival or XGBoost. The extra is
17
+ tested on Python 3.12–3.15 on Linux, with Windows and macOS covered by CI on
18
+ Python 3.13; installation depends on the model libraries' platform support.
19
+
20
+ The default is 3,000 rows and 120 features, with one numerical thread. Allow a
21
+ few minutes on a laptop CPU. `--rows 1000` provides a smaller demonstration;
22
+ `--seed` sets the random seed (default 42). The final console table and optional
23
+ CSV contain all 27 comparisons: three mechanisms, three effect sizes and three
24
+ methods. The CSV retains full precision, expected and realised oracle AUROC,
25
+ the recovery definition and the achieved nomination size.
26
+
27
+ ## What is compared
28
+
29
+ Features are Gaussian, arranged in 30 independent blocks of four. Features in
30
+ the same block have population correlation 0.7. There are three signals:
31
+
32
+ | Signal | Generating mechanism | Recovery at depth 20 |
33
+ | --- | --- | --- |
34
+ | Sparse | Eight direct features, one in each of eight blocks | Fraction of direct features selected |
35
+ | Correlated | Eight direct sentinels, each with three admissible correlated substitutes | Fraction of groups represented by at least one selected member |
36
+ | Distributed | Equal weights on the first 60 features | Fraction of absolute generating weight selected |
37
+
38
+ The sparse and correlated signals use the same generating features. Their
39
+ different recovery definitions show how allowing correlated substitutes changes
40
+ the question being answered. A 20-feature panel cannot cover all 60 distributed
41
+ features; its maximum unadjusted weight coverage is one third. Recovery columns
42
+ therefore measure each stated mechanism and should be interpreted within it.
43
+
44
+ `TimeToEventOutcome(target_auc=...)` sets expected ten-year oracle AUROC to
45
+ 0.60, 0.70 and 0.80 in the finite synthetic covariate population. Ten-year event
46
+ risk is 0.20 and eleven-year risk is 0.22. Follow-up ends at eleven years,
47
+ with no earlier censoring. Calibration precedes outcome sampling, so realised
48
+ held-out oracle AUROC varies around the requested value. Common random draws
49
+ are used across settings.
50
+
51
+ A fixed 60:40 split separates training and evaluation. Scaling is learnt on
52
+ training features only. Cox uses all observed event times with L2 penalty 1.
53
+ Ridge uses ten-year event indicators with penalty 10. XGBoost uses the same
54
+ indicators, 80 depth-two trees, learning rate 0.05 and one CPU thread. There is
55
+ no hyperparameter search. For ridge and XGBoost the horizon labels are fully
56
+ observed because there is no early censoring; this construction would need to
57
+ change if early censoring were introduced.
58
+
59
+ ## Reading the output
60
+
61
+ AUROC is the ten-year cumulative/dynamic estimate from
62
+ `pyplasmode.metrics.cumulative_dynamic_auc`, using the training censoring
63
+ distribution. Higher scores mean higher risk. With the chosen follow-up this
64
+ equals ordinary binary AUROC at ten years.
65
+
66
+ Absolute standardised coefficients rank Cox and ridge features; XGBoost uses
67
+ gain importance. `nomination_from_scores` allocates boundary ties fractionally
68
+ and does not fill unused slots with zero-importance features. `Top20` reports
69
+ the achieved size. `evaluate_matched_recovery` compares each nomination to
70
+ truth-blind random nominations with the same size, positive support and tie
71
+ geometry, within one common feature stratum. All blocks have the same size and
72
+ population correlation in this example.
73
+
74
+ `Adjusted` is **observed minus expected chance recovery**, without rescaling
75
+ or clipping. Negative values are possible. `Target` is the expected oracle AUROC requested and achieved by calibration.
76
+ The `Oracle` column is the realised
77
+ held-out AUROC of the generating signal. Compare fitted AUROC and recovery
78
+ across the three targets: stronger discrimination need not yield an equally
79
+ large change in recovery. This is one synthetic split with fixed model settings,
80
+ so its table demonstrates the evaluation rather than establishing a method ranking.
@@ -0,0 +1,108 @@
1
+ # Calibrating oracle AUROC
2
+
3
+ Use `target_auc` when the intended simulation difficulty is easier to specify
4
+ than an odds or hazard ratio. The generator solves a non-negative signal
5
+ coefficient, while preserving the requested marginal probability or event risks.
6
+ The resulting ratio is at least one per signal standard deviation.
7
+
8
+ ```python
9
+ import numpy as np
10
+ import pyplasmode as ppm
11
+
12
+ signal = np.random.default_rng(12).normal(size=20_000)
13
+ binary = ppm.generate_outcome(
14
+ ppm.BinaryOutcome(probability=0.1, target_auc=0.7),
15
+ signal,
16
+ np.random.default_rng(3),
17
+ )
18
+ assert abs(binary.calibration.oracle_auc - 0.7) < 1e-7
19
+ odds_ratio = binary.calibration["odds_ratio"]
20
+ ```
21
+
22
+ For survival, set `auc_horizon` strictly before the administrative cutoff.
23
+ It defaults to the last event-risk point before the cutoff, or half the cutoff
24
+ when there is only one point. Interior horizons interpolate the cumulative
25
+ baseline hazard under the specified piecewise-constant rates. To target exactly
26
+ ten years with one risk point, set `administrative_horizon=ppm.years(11)`.
27
+ Survivors then remain under observation without additional events or censoring
28
+ between the final risk point and that later cutoff. Independent censoring changes the precision of a
29
+ censoring-aware estimate, but not the population oracle AUROC or solved hazard
30
+ ratio. All event and censoring risk points are still calibrated. The example
31
+ includes year 11 to retain follow-up beyond the ten-year AUROC horizon; unlike
32
+ setting only a later administrative cutoff, this also specifies additional
33
+ event and censoring risk between years 10 and 11.
34
+
35
+ ```python
36
+ horizon = ppm.years(10)
37
+ survival = ppm.generate_outcome(
38
+ ppm.TimeToEventOutcome(
39
+ event_risks={horizon: 0.005, ppm.years(11): 0.006},
40
+ target_auc=0.72,
41
+ auc_horizon=horizon,
42
+ censoring_risks={horizon: 0.05, ppm.years(11): 0.055},
43
+ ),
44
+ signal,
45
+ np.random.default_rng(3),
46
+ )
47
+ assert abs(survival.calibration.oracle_auc - 0.72) < 1e-7
48
+ hazard_ratio = survival.calibration["hazard_ratio"]
49
+ ```
50
+
51
+ ## What is solved
52
+
53
+ The signal is standardised on the supplied rows. Binary outcomes use logistic
54
+ probabilities; survival outcomes use `1-exp(-H0(t)*exp(beta*z))`. For each trial
55
+ coefficient, the intercept or baseline hazard is recalibrated to the requested
56
+ marginal risk. Brent root-finding then matches the expected oracle AUROC.
57
+
58
+ The expectation integrates case/control pairs over the empirical signal
59
+ population, with half credit for ties. It uses independent draws from that
60
+ population, including draws of the same source row. It does not optimise a noisy
61
+ AUROC from one realised outcome sample. `CalibrationResult.oracle_auc` records
62
+ this achieved expectation; `target_auc` records the request. Realised AUROC will
63
+ vary with the seed, sample size, event count and censoring. The solved effect is
64
+ deterministic for a given signal and specification; repeating the generator
65
+ with the same seed also repeats the outcomes.
66
+
67
+ `generate_partitioned` calibrates once on the combined partition covariates,
68
+ so its returned samples share one solved effect. Calling `generate` separately
69
+ on training and test cohorts solves `target_auc` independently on each cohort.
70
+ Use `generate_partitioned` for a shared mechanism, or extract the solved ratio
71
+ with `train.outcome.calibration["odds_ratio"]` and pass it explicitly for test
72
+ outcomes. Their marginal baselines are still recalibrated separately.
73
+
74
+ `CalibrationResult.parameter_dict` returns a fresh parameter mapping;
75
+ `calibration["signal_coefficient"]` looks up one value. For protective effects
76
+ (`odds_ratio < 1` or `hazard_ratio < 1`), use the solved coefficient times the
77
+ standardised signal as the oracle risk score. Passing the unflipped signal
78
+ would reverse the score direction. `target_auc` always solves a non-negative
79
+ coefficient.
80
+
81
+ ## Invalid and unattainable targets
82
+
83
+ Specify exactly one binary effect measure or `target_auc`, and exactly one
84
+ survival `hazard_ratio` or `target_auc`. A target must lie in `[0.5, 1)`; invalid
85
+ specifications raise `OutcomeSpecificationError`. A survival target requires
86
+ positive event risk below one at its evaluation horizon.
87
+
88
+ A target of 0.5 solves ratio one, including constant signals. A constant signal
89
+ cannot attain a higher target. Ties and the signal distribution can also limit
90
+ attainable discrimination. The solver supports log ratios from zero to eight,
91
+ independently of the largest signal value. Logistic probabilities use `expit`
92
+ and the intercept bracket expands to cover the supplied offsets. Survival
93
+ multipliers saturate at `exp(-700)` and `exp(700)` to keep extreme tail hazards
94
+ finite. No observations are dropped or winsorised. A request outside the
95
+ supported effect range or the signal's attainable discrimination raises
96
+ `CalibrationError`; it is never silently clipped. The target describes the
97
+ true generating predictor: a model fitted to finite training data will usually
98
+ have lower held-out AUROC.
99
+
100
+ ## Validation
101
+
102
+ The statistical suite checks expected AUROC to absolute tolerance `1e-7`.
103
+ Generated binary AUROCs are checked within 0.025 across probabilities 0.02, 0.2
104
+ and 0.5 and targets 0.5, 0.6, 0.75 and 0.9, using 80,000 synthetic participants
105
+ per cell. Survival checks use 120,000 participants, event risks 0.005, 0.05 and
106
+ 0.2, censoring risks 0, 0.05 and 0.3, and targets 0.5, 0.61, 0.72 and 0.79;
107
+ the realised IPCW tolerance is 0.035. These are reproducible simulation checks,
108
+ not a guarantee of this accuracy for a user's smaller sample.