| name | bio-applied-numerical-methods-for-bioinformatics |
| description | Interpolate missing time points (Newton/cubic spline), estimate derivatives, and compute AUC via trapezoidal/Simpson/curve_fit in SciPy. Use for missing qPCR points, PK dC/dt, dose-response/ROC AUC, or Michaelis-Menten/Hill fits. |
| tool_type | python |
| primary_tool | SciPy |
Numerical Methods for Bioinformatics
When to Use
- Reconstructing missing or unmeasured time points in a qPCR/microarray time series
- Estimating a rate of change (e.g. dC/dt in a pharmacokinetic profile) from noisy discrete measurements
- Computing area-under-curve for dose-response curves, ROC curves, or PK exposure (AUC)
- Fitting nonlinear kinetics models (Michaelis-Menten, Hill equation) to enzyme/receptor-binding data
- Choosing a sampling/interpolation strategy that avoids Runge-phenomenon oscillation artifacts
Version Compatibility
NumPy >= 1.24, SciPy >= 1.11, scikit-learn >= 1.3 (for roc_curve), Python >= 3.10.
Prerequisites
pip install numpy scipy scikit-learn matplotlib
Basic calculus (derivatives, integrals) and familiarity with scipy.optimize.curve_fit are helpful. See bio-applied-enzyme-kinetics for a deeper dive into kinetics model fitting and bio-applied-statistics-for-bioinformatics for goodness-of-fit tests.
Interpolation
Goal: Reconstruct a value at an unmeasured x (e.g. a missing qPCR time point, ZT hour) from a small set of measured (x, y) pairs without introducing artificial oscillation.
Approach: Newton's divided differences give the same polynomial as Lagrange but let you add points incrementally with one extra term. High-degree polynomials on a uniform grid suffer the Runge phenomenon (oscillation near interval endpoints); Chebyshev nodes minimize that error, and scipy.interpolate.CubicSpline (piecewise cubic, continuous 1st/2nd derivatives) is the robust default for smooth biological signals such as circadian expression.
import numpy as np
from scipy.interpolate import CubicSpline
def newton_divided_differences(x, y):
"""Build the divided-difference table; return Newton coefficients c0..c_{n-1}."""
x, y = np.asarray(x, float), np.asarray(y, float)
n = len(x)
table = np.zeros((n, n))
table[:, 0] = y
for j in range(1, n):
for i in range(n - j):
table[i, j] = (table[i + 1, j - 1] - table[i, j - 1]) / (x[i + j] - x[i])
return table[0, :]
def newton_eval(coeffs, x_nodes, x_eval):
"""Evaluate the Newton interpolation polynomial via a Horner-like recurrence."""
x_eval = np.asarray(x_eval, float)
result = np.full_like(x_eval, coeffs[-1])
for i in range(len(coeffs) - 2, -1, -1):
result = result * (x_eval - x_nodes[i]) + coeffs[i]
return result
def chebyshev_nodes(n, a=-1.0, b=1.0):
"""Return n Chebyshev nodes on [a, b] to avoid Runge-phenomenon oscillation."""
k = np.arange(1, n + 1)
* (b + a) + * (b - a) * np.cos(np.pi * ( * k - ) / ( * n))
t_measured = np.array([, , , , , ], dtype=)
expr = np.array([, , , , , ])
coeffs = newton_divided_differences(t_measured, expr)
missing_t = np.array([, ])
expr_pred = newton_eval(coeffs, t_measured, missing_t)
()
t_sampled = np.array([, , , , , , , , ], dtype=)
per1_expr = np.array([, , , , , , , , ])
cs = CubicSpline(t_sampled, per1_expr, bc_type=)
t_fine = np.linspace(, , )
peak_t = t_fine[np.argmax(cs(t_fine))]
()
Numerical Differentiation and Integration
Goal: Estimate an instantaneous rate (e.g. drug clearance dC/dt) from noisy discrete measurements, and compute a stable area-under-curve (dose-response, ROC, PK exposure).
Approach: Differentiation amplifies noise (total error ~ M2*h + eps/h, minimized near h* ~ sqrt(eps/M2)) — use central differences (O(h^2)) and a moderate step, never h -> 0 on noisy real data. Integration is stable (errors average out): trapezoidal is O(h^2), Simpson's rule is O(h^4) but requires an even number of intervals.
import numpy as np
def central_diff(f, x, h=1e-3):
"""Central difference derivative estimate, O(h^2) accurate."""
return (f(x + h) - f(x - h)) / (2 * h)
def trapezoid_rule(x, y):
"""Composite trapezoidal rule on a possibly non-uniform grid."""
x, y = np.asarray(x, float), np.asarray(y, float)
return np.sum(0.5 * (y[1:] + y[:-1]) * np.diff(x))
def simpsons_rule(x, y):
"""Composite Simpson's rule; requires a uniform grid with an even number of intervals."""
x, y = np.asarray(x, float), np.asarray(y, float)
n = len(x) - 1
if n % 2 != 0:
raise ValueError("Simpson's rule needs an even number of intervals")
h = (x[-1] - x[0]) / n
return h / 3 * (y[0] + y[-1] + 4 * np.sum(y[1:-1:2]) + 2 * np.sum(y[2:-2:2]))
t_pk = np.array([, , , , , , , ], dtype=)
C_pk = np.array([, , , , , , , ])
i (, (t_pk) - ):
dCdt = (C_pk[i + ] - C_pk[i - ]) / (t_pk[i + ] - t_pk[i - ])
()
dose_log = np.array([-, -, -, -, -, -, -], dtype=)
response = np.array([, , , , , , ])
auc = trapezoid_rule(dose_log, response)
()
Nonlinear Kinetics Curve Fitting
Goal: Fit an enzyme-kinetics or receptor-binding model to noisy data and pick the better model with an information criterion, not just R^2.
Approach: scipy.optimize.curve_fit (Levenberg-Marquardt) needs a sane initial guess (p0) — a bad guess converges to a local minimum or fails. Compare nested models (Michaelis-Menten vs Hill) with AIC; a lower AIC by >2 favors that model, >10 is decisive.
import numpy as np
from scipy.optimize import curve_fit
def michaelis_menten(S, Vmax, Km):
"""Michaelis-Menten kinetics: v = Vmax*S / (Km + S)."""
return Vmax * S / (Km + S)
def hill_equation(S, Vmax, K_half, n):
"""Hill equation: v = Vmax*S^n / (K_half^n + S^n); n>1 indicates cooperativity."""
return Vmax * S**n / (K_half**n + S**n)
def aic(n_params, y_obs, y_pred):
"""Akaike Information Criterion from residual sum of squares (Gaussian errors)."""
n = len(y_obs)
sse = np.sum((y_obs - y_pred) ** 2)
return n * np.log(sse / n) + 2 * n_params
S_conc = np.array([0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0])
v_obs = np.array([10.6, 18.5, 30.2, 51.8, 66.9, 77.5, 86.4, 90.1])
popt_mm, _ = curve_fit(michaelis_menten, S_conc, v_obs, p0=[np.max(v_obs), np.median(S_conc)])
popt_h, _ = curve_fit(hill_equation, S_conc, v_obs, p0=[np.max(v_obs), np.median(S_conc), 1.0], maxfev=5000)
aic_mm = aic(2, v_obs, michaelis_menten(S_conc, *popt_mm))
aic_h = aic(3, v_obs, hill_equation(S_conc, *popt_h))
()
()
Pitfalls
- Differentiation on noisy data: shrinking
h does not improve accuracy — noise dominates below h* ~ sqrt(eps/M2); use central differences with a moderate step, or smooth (spline) first.
- Runge phenomenon: a single high-degree polynomial on a uniform grid oscillates wildly near endpoints — use Chebyshev nodes or piecewise splines instead of raising the polynomial degree.
- Simpson's rule requires an even number of intervals (odd number of points) on a uniform grid; passing an odd interval count raises/produces wrong results.
- curve_fit initial guess (
p0): a poor p0 converges to a local minimum, hits maxfev, or silently returns a nonsensical fit — always plot the fit over the data and sanity-check parameter units.
- Coordinate systems: BED is 0-based half-open; VCF/GFF are 1-based inclusive — mixing them causes off-by-one errors when merging interpolated genomic coordinates.
- Multiple testing: when comparing many fitted curves/genes, apply FDR correction (Benjamini-Hochberg) rather than reading raw p-values.
See Also
bio-applied-enzyme-kinetics — deeper Michaelis-Menten/Hill/allosteric model fitting and validation
bio-applied-statistics-for-bioinformatics — goodness-of-fit, chi-squared, and model comparison tests
bio-applied-bayesian-statistics-python — Bayesian alternatives to MLE/curve_fit point estimates
bio-applied-machine-learning-for-biology — when a parametric curve no longer fits and you need a learned model