Biotech and pharmaceutical statistical computing for Python
Project description
PyStatsBio
Biotech and pharmaceutical statistical computing for Python.
Built on PyStatistics for the general statistical computing layer. PyStatsBio provides domain-specific methods for the drug development pipeline: dose-response modeling, sample size/power, diagnostic accuracy, non-compartmental pharmacokinetics, epidemiological measures, meta-analysis, and generalized estimating equations for clustered data.
Design Philosophy
PyStatsBio follows the same principles as PyStatistics:
- Fail fast, fail loud — no silent fallbacks or "helpful" defaults
- Explicit over implicit — require parameters, don't assume intent
- R-level validation — every function is validated against a named R reference
Each function states exactly which R function it replicates and to what tolerance.
What's New in 4.0
Version 4.0 is a correctness release from a full validation sweep of every module against its R reference (drc, PKNCA/NonCompart, pROC/epiR, pwr/PowerTOST, metafor, geepack). It fixes several results that were silently wrong and now agree with the R references to tight tolerances:
doseresponse.ec50()returns the true EC50/ED50 for the asymmetric models (LL.5, W1.4, W2.4, BC.5), by solving the fitted curve for the half-maximal dose rather than returning the raw location parameter (which was off by up to ~27%).doseresponse.fit_drm(model="W2.4")no longer converges to an inferior local optimum on decreasing data.epi.epi_2by2population attributable fraction now uses the exposure prevalence in Levin's formula.meta.rmareports estimator-specific I²/H² (the default REML/PM previously reported the DerSimonian-Laird value).pk.ncaterminal-slope selection uses the WinNonlin/NonCompart "best-fit" rule, matching the reference tools.
Breaking change: epi.rate_standardize(method="indirect") now requires a
standard_weights argument (the standard population's age distribution) to compute
the standardized rate — previously it was silently wrong. The SIR is unaffected.
What's New in 3.0
Version 3.0 tracks the PyStatistics 5.0 API. It requires pystatistics>=5.0
and will not run on 4.x. PyStatsBio's own statistical results are unchanged; the
breaking change is the raised dependency floor plus one relayed value:
- GLM/GEE family names are lowercase. Following PyStatistics 5.0's naming
cleanup, the Gamma family now reports
result.family_name == "gamma"(was"Gamma"), consistent with"gaussian","binomial", and"poisson". Code that compared against the capitalized"Gamma"must switch to"gamma".
Everything introduced in 2.0 — the …Solution result objects, the descriptive
power-analysis parameters, the hyphenated option values, and the
ValidationError / ConvergenceError / NumericalError taxonomy — is
unchanged.
What's New in 2.0
Version 2.0 is a consistency release that aligns the whole library with the
PyStatistics 4.0 API conventions. It contains breaking changes; the
statistical results themselves are unchanged. Requires pystatistics>=4.0.
- Every function returns a
…Solutionobject with uniform.backend_name,.timing,.warnings, and.infoaccessors plus a Jupyter HTML view. All the previous result fields still work (e.g.result.n,result.auc). - Power-analysis parameters are now descriptive:
effect_size(wasd/f/h),n_groups(wask),hazard_ratio(washr),coef_variation(wascv),std(wassd),prop1/prop2(wasp1/p2), andtest_type(wastype). - Option values use hyphens, not dots:
alternative="two-sided"/"one-sided",test_type="two-sample"/"one-sample".mantel_haenszel'smeasuretakes"odds-ratio"/"risk-ratio". - GPU precision lives in the backend string:
backend="gpu"(float32) orbackend="gpu_fp64"(CUDA float64); the separateuse_fp64flag is removed. - Errors are
ValidationError/ConvergenceError/NumericalError— the first subclasses the builtinValueError, so existingexcept ValueErrorcode keeps working.
Quick Start
# --- Clinical trial power / sample size ---
import numpy as np
from pystatsbio import power
# Solve for sample size (two-sample t-test); effect_size is Cohen's d
result = power.power_t_test(effect_size=0.5, power=0.80, alpha=0.05, test_type="two-sample")
print(result.n) # per-group sample size
print(result.summary())
# Solve for power given n
result = power.power_t_test(n=64, effect_size=0.5, alpha=0.05, test_type="two-sample")
print(result.power)
# Paired t-test
result = power.power_paired_t_test(effect_size=0.3, power=0.80, alpha=0.05)
print(result.n)
# Two proportions (effect_size is Cohen's h)
h = 2 * (np.arcsin(np.sqrt(0.50)) - np.arcsin(np.sqrt(0.30)))
result = power.power_prop_test(effect_size=h, power=0.80, alpha=0.05)
print(result.n)
# Survival (log-rank); the effect is the hazard ratio
result = power.power_logrank(hazard_ratio=0.6, power=0.80, alpha=0.05)
print(result.n)
# Non-inferiority for means
result = power.power_noninf_mean(
delta=0.0, std=1.0, margin=0.5, power=0.80, alpha=0.05
)
print(result.n)
# Crossover bioequivalence (PowerTOST method)
result = power.power_crossover_be(coef_variation=0.20, power=0.80, alpha=0.05)
print(result.n) # subjects per sequence
# Cluster-randomized trial
result = power.power_cluster(
effect_size=0.5, icc=0.05, cluster_size=20, power=0.80, alpha=0.05
)
print(result.n) # clusters per arm
# --- Dose-response modeling ---
from pystatsbio import doseresponse
dose = np.array([0.001, 0.01, 0.1, 1.0, 10.0, 100.0])
response = np.array([2.1, 3.5, 12.0, 48.0, 87.5, 97.8])
# Fit 4PL (LL.4) model
result = doseresponse.fit_drm(dose, response, model="LL.4")
print(result.params) # CurveParams(bottom, top, ec50, hill)
print(result.params.ec50) # the fitted EC50
print(result.summary())
# Fit 5PL (asymmetric)
result = doseresponse.fit_drm(dose, response, model="LL.5")
print(result.params)
# Extract EC50 with confidence interval
ec50_result = doseresponse.ec50(result)
print(ec50_result.estimate, ec50_result.ci_lower, ec50_result.ci_upper)
# Relative potency (reference vs. test compound)
ref_result = doseresponse.fit_drm(dose, response, model="LL.4")
test_result = doseresponse.fit_drm(dose * 3, response, model="LL.4")
rp = doseresponse.relative_potency(ref_result, test_result)
print(rp.ratio, rp.ci_lower, rp.ci_upper)
# Benchmark dose (BMD/BMDL)
bmd_result = doseresponse.bmd(result, bmr=0.10)
print(bmd_result.bmd, bmd_result.bmdl)
# Batch fitting (GPU-accelerated for HTS)
# dose_matrix / response_matrix: (n_curves, n_doses) arrays
responses = np.random.rand(500, 6) * 100
doses = np.tile(dose, (500, 1))
batch = doseresponse.fit_drm_batch(doses, responses, model="LL.4", backend="auto")
print(batch.ec50) # shape (500,) — one EC50 per curve
# --- Diagnostic accuracy --- (binary labels come first, then the scores)
from pystatsbio import diagnostic
labels = np.array([0, 0, 0, 1, 1, 0, 1, 1, 0, 1])
scores = np.array([0.1, 0.4, 0.35, 0.8, 0.9, 0.15, 0.6, 0.75, 0.55, 0.95])
# ROC curve + AUC (with DeLong CI)
roc_result = diagnostic.roc(labels, scores)
print(roc_result.auc, roc_result.auc_ci_lower, roc_result.auc_ci_upper)
# Compare two correlated ROC curves (DeLong test)
scores2 = np.random.rand(10)
roc2 = diagnostic.roc(labels, scores2)
test_result = diagnostic.roc_test(
roc_result, roc2, predictor1=scores, predictor2=scores2, response=labels
)
print(test_result.p_value)
# Full diagnostic accuracy at a cutoff
da = diagnostic.diagnostic_accuracy(labels, scores, cutoff=0.5)
print(da.sensitivity, da.specificity, da.ppv, da.npv)
# Optimal cutoff selection
cutoff = diagnostic.optimal_cutoff(roc_result, method="youden")
print(cutoff.cutoff, cutoff.sensitivity, cutoff.specificity)
# Batch AUC for biomarker panel screening
# 200 candidate biomarkers on 100 subjects -> (n_subjects, n_markers)
panel = np.random.rand(100, 200)
batch_auc = diagnostic.batch_auc(np.random.randint(0, 2, 100), panel, backend="auto")
print(batch_auc.auc) # shape (200,) — one AUC per biomarker
# --- Pharmacokinetics (NCA) ---
from pystatsbio import pk
time = np.array([0, 0.5, 1, 2, 4, 6, 8, 12, 24])
conc = np.array([0, 8.5, 12.1, 10.4, 7.2, 4.9, 3.1, 1.4, 0.3])
result = pk.nca(time, conc, route="ev", dose=100.0)
print(result.cmax, result.tmax)
print(result.auc_last, result.auc_inf)
print(result.half_life)
print(result.clearance, result.vz)
print(result.summary())
# --- Epidemiological measures ---
from pystatsbio import epi
# 2x2 table: exposed/unexposed x case/control
table = np.array([[30, 70], [10, 90]])
result = epi.epi_2by2(table)
# risk_ratio / odds_ratio are EpiMeasure value objects (estimate + CI)
print(result.risk_ratio.estimate, result.odds_ratio.estimate)
print(result.summary())
# Mantel-Haenszel stratified analysis
tables = np.array([[[30, 70], [10, 90]], [[20, 80], [15, 85]]])
result = epi.mantel_haenszel(tables, measure="odds-ratio")
print(result.pooled_estimate.estimate, result.cmh_p_value)
# --- Meta-analysis ---
from pystatsbio import meta
effects = np.array([0.5, 0.3, 0.8, 0.6, 0.4])
variances = np.array([0.1, 0.15, 0.2, 0.12, 0.18]) ** 2 # sampling variances
# Random-effects meta-analysis (method: 'DL', 'REML', or 'PM')
result = meta.rma(effects, variances, method="DL")
print(result.estimate, result.ci_lower, result.ci_upper)
print(result.tau2, result.I2)
print(result.summary())
# --- GEE (clustered data) ---
from pystatsbio import gee
y = np.random.randn(200)
X = np.random.randn(200, 3)
cluster_id = np.repeat(np.arange(40), 5)
result = gee.gee(y, X, cluster_id, family="gaussian", corr_structure="exchangeable")
print(result.coefficients, result.robust_se)
print(result.summary())
Modules
| Module | Status | Description |
|---|---|---|
power/ |
Complete | Sample size and power for clinical trial designs |
doseresponse/ |
Complete | 4PL/5PL curve fitting, EC50, relative potency, BMD, batch HTS |
diagnostic/ |
Complete | ROC, AUC, sensitivity/specificity, optimal cutoff, batch biomarker |
pk/ |
Complete | Non-compartmental PK analysis (NCA): AUC, Cmax, CL, Vd, MRT |
epi/ |
Complete | Epidemiological measures: 2x2 tables, rate standardization, Mantel-Haenszel |
meta/ |
Complete | Meta-analysis: fixed/random effects, DerSimonian-Laird, REML |
gee/ |
Complete | Generalized estimating equations for clustered/longitudinal data |
power — Sample Size and Power
| Function | R equivalent |
|---|---|
power_t_test() |
pwr::pwr.t.test() |
power_paired_t_test() |
pwr::pwr.t.test(type="paired") |
power_prop_test() |
pwr::pwr.2p.test() |
power_fisher_test() |
pwr::pwr.2p.test() (via Cohen's h) |
power_logrank() |
gsDesign::nSurv() |
power_anova_oneway() |
pwr::pwr.anova.test() |
power_anova_factorial() |
pwr::pwr.f2.test() |
power_noninf_mean() |
TrialSize::TwoSampleMean.NIS() |
power_noninf_prop() |
TrialSize::TwoSampleProportion.NIS() |
power_equiv_mean() |
TrialSize::TwoSampleMean.Equivalence() |
power_superiority_mean() |
TrialSize::TwoSampleMean.NIS() |
power_crossover_be() |
PowerTOST::sampleN.TOST(), power.TOST(method="nct") |
power_cluster() |
clusterPower, CRTSize (ICC-adjusted) |
doseresponse — Dose-Response Modeling
| Function | R equivalent |
|---|---|
fit_drm() |
drc::drm() |
fit_drm_batch() |
vectorized drc::drm() |
ec50() |
drc::ED() |
relative_potency() |
drc::EDcomp() with parallelism test |
bmd() |
drc::bmd() / BMDS |
Models: LL.4 (4PL), LL.5 (5PL), W1.4 (Weibull-1), W2.4 (Weibull-2),
BC.5 (Brain-Cousens hormesis).
diagnostic — Diagnostic Accuracy
| Function | R equivalent |
|---|---|
roc() |
pROC::roc() with DeLong CI |
roc_test() |
pROC::roc.test() (DeLong) |
diagnostic_accuracy() |
epiR::epi.tests() |
optimal_cutoff() |
OptimalCutpoints::optimal.cutpoints() |
batch_auc() |
vectorized pROC::auc() |
pk — Non-Compartmental Analysis
| Function | R equivalent |
|---|---|
nca() |
PKNCA::pk.nca() / NonCompart::sNCA() |
AUC methods: linear, log, linear-up/log-down (FDA default).
Routes: iv (intravenous), ev (extravascular).
epi — Epidemiological Measures
| Function | R equivalent |
|---|---|
epi_2by2() |
epiR::epi.2by2() |
rate_standardize() |
epitools::ageadjust.direct() / ageadjust.indirect() |
mantel_haenszel() |
stats::mantelhaen.test() |
meta — Meta-Analysis
| Function | R equivalent |
|---|---|
rma(method="DL") |
meta::metagen(method="DL") / metafor::rma() |
rma(method="REML") |
metafor::rma(method="REML") |
rma(method="PM") |
metafor::rma(method="PM") |
cochran_q() / i_squared() / h_squared() |
metafor::rma()$QE / $I2 / $H2 |
gee — Generalized Estimating Equations
| Function | R equivalent |
|---|---|
gee() |
geepack::geeglm() |
Families: gaussian, binomial, poisson.
Correlation structures: independence, exchangeable, ar1, unstructured.
Installation
pip install pystatsbio
# With GPU support (requires PyTorch)
pip install pystatsbio[gpu]
# Development
pip install pystatsbio[dev]
Requires Python 3.11+. Core dependencies: pystatistics, numpy, scipy.
License
MIT
Author
Hai-Shuo (contact@sgcx.org)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pystatsbio-4.0.0.tar.gz.
File metadata
- Download URL: pystatsbio-4.0.0.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fa0ad88aa67a4d457c032be50ab847be3af7839d32c258425d095a1eeaaec49
|
|
| MD5 |
601a5d0e8ab24734ea65de0409f722f5
|
|
| BLAKE2b-256 |
ad97889f39c1430afd6fd3212f37a144f48caba44322ef1781810fff644f0603
|
Provenance
The following attestation bundles were made for pystatsbio-4.0.0.tar.gz:
Publisher:
publish.yml on sgcx-org/pystatsbio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pystatsbio-4.0.0.tar.gz -
Subject digest:
2fa0ad88aa67a4d457c032be50ab847be3af7839d32c258425d095a1eeaaec49 - Sigstore transparency entry: 2170020221
- Sigstore integration time:
-
Permalink:
sgcx-org/pystatsbio@59ac122f9bb7f9c93821b5e273a93bc219f6c7b0 -
Branch / Tag:
refs/tags/v4.0.0 - Owner: https://github.com/sgcx-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59ac122f9bb7f9c93821b5e273a93bc219f6c7b0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pystatsbio-4.0.0-py3-none-any.whl.
File metadata
- Download URL: pystatsbio-4.0.0-py3-none-any.whl
- Upload date:
- Size: 120.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
900daf9cf6cee8c265e69e21d49eb8d6c4dff45433608cdaaf4368bbcb3f4ab5
|
|
| MD5 |
5936dbdb8b1f0786dad0c49cac4162c1
|
|
| BLAKE2b-256 |
90ad154919a125163f9e52d465cfd9cca02c62445ce1db919bb7e756577c2275
|
Provenance
The following attestation bundles were made for pystatsbio-4.0.0-py3-none-any.whl:
Publisher:
publish.yml on sgcx-org/pystatsbio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pystatsbio-4.0.0-py3-none-any.whl -
Subject digest:
900daf9cf6cee8c265e69e21d49eb8d6c4dff45433608cdaaf4368bbcb3f4ab5 - Sigstore transparency entry: 2170020224
- Sigstore integration time:
-
Permalink:
sgcx-org/pystatsbio@59ac122f9bb7f9c93821b5e273a93bc219f6c7b0 -
Branch / Tag:
refs/tags/v4.0.0 - Owner: https://github.com/sgcx-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@59ac122f9bb7f9c93821b5e273a93bc219f6c7b0 -
Trigger Event:
release
-
Statement type: