Skip to main content

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:

  1. Fail fast, fail loud — no silent fallbacks or "helpful" defaults
  2. Explicit over implicit — require parameters, don't assume intent
  3. 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_2by2 population attributable fraction now uses the exposure prevalence in Levin's formula.
  • meta.rma reports estimator-specific I²/H² (the default REML/PM previously reported the DerSimonian-Laird value).
  • pk.nca terminal-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 …Solution object with uniform .backend_name, .timing, .warnings, and .info accessors 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 (was d/f/h), n_groups (was k), hazard_ratio (was hr), coef_variation (was cv), std (was sd), prop1/prop2 (was p1/p2), and test_type (was type).
  • Option values use hyphens, not dots: alternative="two-sided" / "one-sided", test_type="two-sample" / "one-sample". mantel_haenszel's measure takes "odds-ratio" / "risk-ratio".
  • GPU precision lives in the backend string: backend="gpu" (float32) or backend="gpu_fp64" (CUDA float64); the separate use_fp64 flag is removed.
  • Errors are ValidationError / ConvergenceError / NumericalError — the first subclasses the builtin ValueError, so existing except ValueError code 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() (ratio of ED50s, Fieller CI)
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() gee::gee() / SAS PROC GENMOD (Liang-Zeger); geepack::geeglm()

The working-correlation parameters use the classical Liang-Zeger (1986) method-of-moments estimator — for AR(1), the lag-1 adjacent-pair moment, the same convention as gee::gee(corstr="AR-M", Mv=1) and SAS PROC GENMOD. geepack::geeglm instead uses the Yan & Fine (2004) all-lag estimating equation; both are consistent when AR(1) is correctly specified, but they differ when it is not. Coefficients and robust SE match geepack under independence and exchangeable.

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>=5.1, numpy, scipy.


License

MIT

Author

Hai-Shuo (contact@sgcx.org)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pystatsbio-4.0.3.tar.gz (1.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pystatsbio-4.0.3-py3-none-any.whl (123.7 kB view details)

Uploaded Python 3

File details

Details for the file pystatsbio-4.0.3.tar.gz.

File metadata

  • Download URL: pystatsbio-4.0.3.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

Hashes for pystatsbio-4.0.3.tar.gz
Algorithm Hash digest
SHA256 98c4cd24189eb26182f1c11d57002f4e7327a98e98c1c35227cb726a734152c9
MD5 44b31a32c61c773f0607dd14cd1dea1f
BLAKE2b-256 9528464d7116121bac9de0b5260e74b9db23a6511d4bf8e5ed1bb3244b77f4f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pystatsbio-4.0.3.tar.gz:

Publisher: publish.yml on sgcx-org/pystatsbio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pystatsbio-4.0.3-py3-none-any.whl.

File metadata

  • Download URL: pystatsbio-4.0.3-py3-none-any.whl
  • Upload date:
  • Size: 123.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pystatsbio-4.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 877fba0d4579bf5bac5e72b04b36cfb3c2311a12f280d0dc4d75c8b6c15cc11b
MD5 bc736990628f1b6f030cd939fd87fa04
BLAKE2b-256 cd71652104bdbdfcc05eddcc43be9a2857628563437db3becc7ed622f24411c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pystatsbio-4.0.3-py3-none-any.whl:

Publisher: publish.yml on sgcx-org/pystatsbio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page