Univariate DPV calibration: OLS fit, inverse-regression prediction, LOD/LOQ.
Project description
dpv-calibration
Univariate OLS calibration for Differential Pulse Voltammetry (DPV). Given a set of standard scans at known concentrations, the model fits a calibration line, predicts analyte concentration from new scans via inverse regression, and reports 95% confidence intervals plus LOD/LOQ classification. All statistics follow Lavagnini & Magno (2007).
Depends on dpv-monopeak for peak feature extraction from raw voltage/current arrays.
Usage with AI assistants
This README is designed to be pasted directly into an LLM coding assistant as context. All types, units, parameter names, return values, and mathematical definitions are documented explicitly so that an LLM can generate correct, complete code against this API without access to the source.
Installation
pip install dpv-calibration
For plotting support (plot_calibration):
pip install "dpv-calibration[plot]"
Quick start
import numpy as np
from dpv_calibration import DPVScan, CalibrationModel, plot_calibration
# Build calibration scans (v in volts, i in µA, concentration_M in mol/L)
scans = [
DPVScan(v=v_arr, i=i_arr, concentration_M=1e-9),
DPVScan(v=v_arr, i=i_arr, concentration_M=1e-8),
# ... one DPVScan per replicate per concentration level
]
# Fit — log scale is auto-selected when dynamic range exceeds 1000×
model = CalibrationModel(feature="Height")
model.fit(scans)
print(model)
# CalibrationModel(feature='Height', log_x=True, R²=0.9821, RMSECV=0.3104,
# LOD=3.14e-13 M, LOQ=1.05e-12 M)
# Predict from a new scan
result = model.predict(v=new_v, i=new_i)
print(result.concentration_M) # float, mol/L
print(result.status) # "below_lod" | "detected" | "quantifiable"
print(result.ci) # (lower_M, upper_M) — 95% CI in mol/L
# Plot (requires matplotlib)
ax = plot_calibration(model)
ax.figure.savefig("calibration.png", dpi=150, bbox_inches="tight")
API reference
DPVScan
@dataclass
class DPVScan:
v: np.ndarray # voltage array, shape (N,), volts
i: np.ndarray # current array, shape (N,), µA (must be consistent across the calibration set)
concentration_M: float # analyte concentration, mol/L (must be > 0)
Plain data container. No processing occurs at construction. v and i must be the same length.
PredictionResult
@dataclass
class PredictionResult:
concentration_M: float # predicted analyte concentration, mol/L
status: str # detection classification (see below)
ci: tuple # (lower_M, upper_M) — 95% confidence interval, mol/L
status values:
| Value | Condition | Meaning |
|---|---|---|
"below_lod" |
concentration_M < model.lod_M |
Signal is indistinguishable from blank noise. Do not report a concentration. |
"detected" |
lod_M ≤ concentration_M < loq_M |
Analyte is present but cannot be quantified accurately. Use for presence/absence decisions only. |
"quantifiable" |
concentration_M ≥ loq_M |
Result is reliable for quantitative reporting. |
ci is a (lower_M, upper_M) tuple in mol/L. Both bounds are back-transformed to linear molar units even when the model was fitted in log space. The interval widens when the predicted concentration is far from the centre of the calibration range or when n is small.
CalibrationModel
CalibrationModel(feature: str = "Height", log_x: bool | None = None)
Constructor parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
feature |
str |
"Height" |
dpv-monopeak feature used as the calibration signal (y). Valid values: "Height", "Area", "Width", "X", "YOffset", "MaxSlope", "MinSlope", "SumSlope". |
log_x |
bool | None |
None |
Whether to fit in log₁₀(concentration) space. None = auto: set to True if max(concentration) / min(concentration) > 1000, else False. Can be forced by passing True or False explicitly. |
Public attributes — available after .fit():
| Attribute | Type | Unit | Description |
|---|---|---|---|
feature |
str |
— | Feature name passed at construction. |
log_x |
bool |
— | Whether the fit used log₁₀ concentration space. Resolved from None after fit. |
r2 |
float |
— | Coefficient of determination (R²) of the OLS fit on the training data. |
rmsecv |
float |
log₁₀(M) or M | Root-mean-square error of leave-one-out cross-validation, in the same units as the transformed x (log₁₀ M if log_x=True, M if log_x=False). |
lod_M |
float |
mol/L | Limit of detection. Concentration below which the signal cannot be distinguished from blank noise (3σ rule). |
loq_M |
float |
mol/L | Limit of quantification. Concentration below which quantitative accuracy is insufficient (10σ rule). Always > lod_M. |
__repr__ example (fitted):
CalibrationModel(feature='Height', log_x=True, R²=0.9821, RMSECV=0.3104,
LOD=3.14e-13 M, LOQ=1.05e-12 M)
__repr__ example (not fitted):
CalibrationModel(feature='Height', not fitted)
CalibrationModel.fit(scans)
model.fit(scans: list[DPVScan]) -> CalibrationModel
Fit the calibration model. Returns self (supports chaining: model.fit(scans).predict(v, i)).
- Calls
dpv_monopeak.extract_dpv_features(scan.v, scan.i)for each scan. Scans where no valid Faradaic peak is found (Nonereturn) are silently skipped. - Resolves
log_xfromNoneif not set explicitly. - Fits OLS in the (optionally log-transformed) concentration × feature space.
- Computes
r2,rmsecv(leave-one-out CV),lod_M,loq_M.
Multiple replicates at the same concentration level are supported — each DPVScan is treated as an independent observation.
CalibrationModel.predict(v, i, m=1, alpha=0.05)
model.predict(
v: np.ndarray, # voltage array of the unknown scan, volts
i: np.ndarray, # current array, µA
m: int = 1, # number of replicates averaged to produce this scan
alpha: float = 0.05, # CI significance level (0.05 → 95%, 0.01 → 99%)
) -> PredictionResult
Predict concentration from a raw DPV scan using inverse regression.
Raises:
RuntimeError— if called before.fit().ValueError— ifextract_dpv_featuresfinds no valid peak in the scan.
m (replicates): if the scan passed to predict is the average of m replicate measurements, set m accordingly. The CI width scales as 1/√m. Pass m=1 for a single scan.
alpha: significance level for the confidence interval. alpha=0.05 gives a 95% CI, alpha=0.01 gives 99%.
How inverse regression works: standard calibration predicts signal from concentration (y = b₀ + b₁x). To go from a measured signal y₀ back to concentration, the equation is inverted: x̂₀ = (y₀ − b₀) / b₁. This is not a separate regression of x on y — it correctly propagates calibration uncertainty. When log_x=True, x̂₀ is in log₁₀(M) and is back-transformed via 10^x̂₀ before being stored in PredictionResult.concentration_M.
plot_calibration
from dpv_calibration import plot_calibration # requires [plot] extra
plot_calibration(
model: CalibrationModel,
ax: matplotlib.axes.Axes | None = None,
alpha: float = 0.05,
) -> matplotlib.axes.Axes
Plot a fitted model. Creates a new figure if ax is None.
What it draws:
| Element | Description |
|---|---|
| Scatter | Calibration points (_x_train, _y_train) — x is in transformed space |
| OLS line | Fitted line over the calibration range ± 10% padding |
| Confidence band | Shaded band for the mean response at each x (Lavagnini & Magno 2007, Eq. 9b) |
| Orange dashed line | LOD position: log₁₀(lod_M) if log_x=True, else lod_M |
| Red dashed line | LOQ position: log₁₀(loq_M) if log_x=True, else loq_M |
| Text annotation | R² and RMSECV in the bottom-right corner |
X-axis label: "log₁₀(Concentration / M)" when model.log_x=True; "Concentration (M)" when False.
Y-axis label: the value of model.feature.
Raises:
ImportError— if matplotlib is not installed (pip install "dpv-calibration[plot]").RuntimeError— if the model has not been fitted.
Loading your data
The snippet below is a complete, copy-pasteable loader for PSTrace multi-curve Excel exports (PalmSens instrument software). Adapt it to your own file format — the only requirement for the rest of the API is a list of DPVScan objects with correct v, i, and concentration_M values.
import re
import numpy as np
import pandas as pd
from dpv_calibration import DPVScan
def load_palmsens_xlsx(
path: str,
keyword: str = "cort",
conc_pattern: str = r"(\d+(?:\.\d+)?)\s*(pM|nM|uM|mM)",
) -> list[DPVScan]:
"""
Load DPV scans from a PSTrace multi-curve Excel export.
Expected file layout
--------------------
Row 0 — scan names (e.g. "10nM Cort 5mM redox det")
Row 1 — column units (e.g. "V", "µA")
Row 2+ — numeric data, 81 rows per scan
Columns come in pairs: (voltage, current), (voltage, current), ...
Parameters
----------
path : path to .xlsx file
keyword : case-insensitive substring — only column-pairs whose name
contains this string are loaded (use "" to load all)
conc_pattern : regex with two capture groups (value, unit)
applied to each scan name to extract concentration;
columns with no match are skipped
"""
_units = {"pm": 1e-12, "nm": 1e-9, "um": 1e-6, "mm": 1e-3}
def _parse_conc(label):
m = re.search(conc_pattern, label, re.IGNORECASE)
if not m:
return None
value, unit = float(m.group(1)), m.group(2).lower()
return value * _units[unit]
raw = pd.read_excel(path, header=None)
scans = []
for col in range(0, raw.shape[1], 2):
name = str(raw.iloc[0, col])
if keyword.lower() not in name.lower():
continue
data = raw.iloc[2:, col:col + 2].dropna()
data.columns = ["V", "I"]
data = data.apply(pd.to_numeric, errors="coerce").dropna()
if len(data) != 81: # PSTrace DPV exports exactly 81 points
continue
conc = _parse_conc(name)
if conc is None:
continue
scans.append(DPVScan(
v=data["V"].values,
i=data["I"].values,
concentration_M=conc,
))
return scans
Concentration labels recognised by the default pattern (case-insensitive, first match in the label):
| Example label | Parsed concentration |
|---|---|
"1pM Cort 5mM redox det" |
1e-12 M |
"10nM cort" |
1e-8 M |
"0.5uM Cortisol" |
5e-7 M |
"1mM Cort 5mM redox 10mM PBS det" |
1e-3 M |
Mathematical reference
All equations follow Lavagnini & Magno (2007) unless noted.
OLS calibration line
Fit: y = b₀ + b₁x, where y is the peak feature value and x is concentration (or log₁₀ concentration when log_x=True).
| Quantity | Formula | Eq. |
|---|---|---|
| Slope | $b_1 = \dfrac{\sum(x_i - \bar{x})(y_i - \bar{y})}{\sum(x_i - \bar{x})^2}$ | 3–4 |
| Intercept | $b_0 = \bar{y} - b_1\bar{x}$ | 3–4 |
| Residual SD | $s_{y/x} = \sqrt{\dfrac{\sum(y_i - \hat{y}_i)^2}{n - 2}}$ | 5 |
Confidence band for mean response (used in plot_calibration)
$$\hat{y}(x) \pm t_{n-2,,\alpha/2} \cdot s_{y/x} \sqrt{\frac{1}{n} + \frac{(x - \bar{x})^2}{S_{xx}}}$$
where $S_{xx} = \sum(x_i - \bar{x})^2$. (Eq. 9b)
Inverse regression (used in predict)
$$\hat{x}_0 = \frac{y_0 - b_0}{b_1}$$
95% CI on $\hat{x}_0$:
$$\hat{x}0 \pm t{n-2,,\alpha/2} \cdot \frac{s_{y/x}}{|b_1|} \sqrt{\frac{1}{m} + \frac{1}{n} + \frac{(\hat{x}0 - \bar{x})^2}{S{xx}}}$$
where $m$ = number of replicates in the unknown measurement. (Eq. 10–11)
When log_x=True, $\hat{x}_0$ is in log₁₀(M). All reported concentrations and CI bounds are back-transformed: $c = 10^{\hat{x}_0}$.
LOD and LOQ
Computed in transformed x-space, then back-transformed:
$$\text{LOD} = \frac{3,s_{y/x}}{b_1}, \qquad \text{LOQ} = \frac{10,s_{y/x}}{b_1}$$
(Lavagnini & Magno 2007, Section V; Miller & Miller 1988)
- LOD — signal is 3 standard deviations above blank; the smallest concentration that can be reliably detected.
- LOQ — signal is 10 standard deviations above blank; the smallest concentration that can be reliably quantified (≤ 10% RSD).
Citation
Lavagnini, I.; Magno, F. "A statistical overview on univariate calibration, inverse regression, and detection limits: Application to gas chromatography/mass spectrometry technique." Mass Spectrometry Reviews 2007, 26, 1–18. https://doi.org/10.1002/mas.20100
License
MIT © 2026 Dan Vu
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 dpv_calibration-0.1.0.tar.gz.
File metadata
- Download URL: dpv_calibration-0.1.0.tar.gz
- Upload date:
- Size: 45.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08e4a900346dd99c040ab60c30908e8b633dd239ff8627a48f9745f3f0175ae4
|
|
| MD5 |
b14a40910be37ea5d9f77b0688607473
|
|
| BLAKE2b-256 |
1f7da07fab6b1372a3f9975c022cb708114cf3717648f549f53fb7f04b742f48
|
File details
Details for the file dpv_calibration-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dpv_calibration-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12369a56ca0fad5ec617fef4d08eadce4c0df3eb6b943c790879b8852387507a
|
|
| MD5 |
38198b80c4425f0d45f7e07c30b04d01
|
|
| BLAKE2b-256 |
9acefe224be12261571576c6262febc31e07705de93f5d15b54dfbcdaf2f50a7
|