Diagnostic Plots

OpenPKPD includes a plots subpackage with publication-quality diagnostic figures. All plots require matplotlib.

Installation

uv add "OpenPKPD[plots]"
# or
pip install "OpenPKPD[plots]"

Quick example

from openpkpd.plots.diagnostics import compute_diagnostics
from openpkpd.plots.gof import diagnostic_panel
from openpkpd.plots.pk import spaghetti_plot
from openpkpd.plots.eta import eta_histograms

# Run your model first
result = built.fit()

# Build the diagnostics DataFrame (all residuals, predictions, ETAs)
diag_df = compute_diagnostics(built.population_model, result)

# 2×3 GOF panel
fig = diagnostic_panel(diag_df, title="Theophylline FOCE — GOF")
fig.savefig("gof_panel.png", dpi=150)

# Individual concentration-time profiles
fig2 = spaghetti_plot(diag_df, log_y=True)

# ETA histograms with N(0, ω) overlay
fig3 = eta_histograms(diag_df, result.omega_final)

compute_diagnostics()

All plot functions take a diagnostics DataFrame produced by:

from openpkpd.plots.diagnostics import compute_diagnostics

diag_df = compute_diagnostics(population_model, result)

Columns returned

Column

Description

ID

Subject identifier

TIME

Observation time

DV

Observed value

PRED

Population prediction (ETA = 0)

IPRED

Individual prediction (ETA = η̂ᵢ)

RES

DV PRED

IRES

DV IPRED

WRES

Weighted residual

IWRES

Individual weighted residual

CWRES

Conditional weighted residual (via C_i Cholesky)

ETA1, ETA2, …

Subject-level empirical Bayes estimates

Any covariates

Passed through from the dataset

Only rows with EVID=0 and MDV=0 are included.


GOF plots (plots/gof.py)

diagnostic_panel()

Combined 2×3 panel of all six GOF plots:

from openpkpd.plots.gof import diagnostic_panel

fig = diagnostic_panel(diag_df, title="Model 1 — GOF", figsize=(14, 10))
fig.savefig("gof.png", dpi=150)

Individual GOF functions

from openpkpd.plots.gof import (
    dv_vs_ipred, dv_vs_pred,
    cwres_vs_time, cwres_vs_pred,
    cwres_qq, abs_iwres_vs_ipred,
)

fig = dv_vs_ipred(diag_df, log_scale=True)
fig = cwres_vs_time(diag_df)
fig = cwres_qq(diag_df)

All six functions accept an optional ax argument and return a matplotlib.figure.Figure.


PK plots (plots/pk.py)

from openpkpd.plots.pk import concentration_time, spaghetti_plot, mean_profile

# Individual profiles (one line per subject)
fig = spaghetti_plot(diag_df, log_y=True, mean_overlay=True)

# Single subject
fig = concentration_time(diag_df, individual=3, log_y=True)

# Population mean ± SD band
fig = mean_profile(diag_df, log_y=False, sd_band=True)

PD plots (plots/pd.py)

from openpkpd.plots.pd import (
    effect_time, emax_curve, hysteresis_loop, pd_individual
)

# Effect vs time
fig = effect_time(diag_df, effect_col="DV")

# E vs C with Emax overlay
fig = emax_curve(diag_df, conc_col="IPRED", effect_col="DV",
                 emax=result.theta_final[4],
                 ec50=result.theta_final[5])

# Clockwise/counter-clockwise hysteresis loop
fig = hysteresis_loop(diag_df, conc_col="IPRED", effect_col="DV",
                      color_by_time=True)

# Per-subject dual PK + PD panels
fig = pd_individual(diag_df, conc_col="IPRED", effect_col="DV", n_cols=3)

ETA plots (plots/eta.py)

from openpkpd.plots.eta import eta_histograms, eta_pairs, eta_vs_covariate

# Histograms with N(0, ω_kk) overlay
fig = eta_histograms(diag_df, result.omega_final, overlay_normal=True)

# Pairwise scatter matrix
fig = eta_pairs(diag_df)

# ETA vs continuous covariate
fig = eta_vs_covariate(diag_df, covariate="WT", eta_col="ETA1")

# ETA vs categorical covariate (box plots)
fig = eta_vs_covariate(diag_df, covariate="SEX", eta_col="ETA1",
                       categorical=True)

Model performance plots (plots/model_perf.py)

from openpkpd.plots.model_perf import ofv_history, vpc

# OFV convergence plot
fig = ofv_history(result, log_scale=False)

# Visual predictive check (Monte Carlo simulation)
fig = vpc(diag_df, built.population_model, result, n_sim=200,
          percentiles=(5, 50, 95))

NCA plots (plots/nca.py)

from openpkpd.plots.nca import (
    nca_distributions,
    nca_boxplot,
    nca_profile_plot,
    dose_proportionality_plot,
)

Function

Input

Description

nca_distributions(nca_df)

NCA results DataFrame

Histogram grid for C0 (when available), Cmax, AUC_last, AUC_inf, t½, CL/F, Vz/F

nca_boxplot(nca_df)

NCA results DataFrame

Side-by-side boxplots, optional group_col stratification

nca_profile_plot(times, conc, nca_params)

Raw C-t data + NCAParameters

C-t profile annotated with Cmax, Tmax, AUC fill, and λz regression

dose_proportionality_plot(nca_df)

NCA results DataFrame with dose column

Log-log exposure vs dose with slope-1 reference and power model

See the NCA guide for detailed usage examples.


Saving figures

Every plot function returns a matplotlib.figure.Figure. Save with:

fig.savefig("plot.png", dpi=150, bbox_inches="tight")
fig.savefig("plot.pdf")          # Vector PDF for publication

Style

All plots use a consistent IBM colorblind-friendly palette and minimal grid style. To override:

import matplotlib.pyplot as plt
with plt.rc_context({"figure.figsize": (8, 6)}):
    fig = diagnostic_panel(diag_df)