| name | bio-applied-biochemistry |
| description | Fit Michaelis-Menten Vmax/Km with scipy curve_fit, convert absorbance to concentration via Beer-Lambert, and model enzyme inhibition. Use when analyzing enzyme assays or estimating Km, Vmax, kcat, or Ki. |
| tool_type | python |
| primary_tool | scipy.optimize.curve_fit |
Biochemistry: Assays and Enzyme Kinetics
When to Use
- Converting spectrophotometric absorbance (e.g., NADH at 340 nm) to molar concentration
- Extracting an initial velocity (v0) from an enzyme progress curve
- Fitting Michaelis-Menten parameters (Vmax, Km, kcat) from substrate-velocity data
- Comparing linearization methods (Lineweaver-Burk, Eadie-Hofstee, Hanes-Woolf) against nonlinear regression
- Modeling competitive/uncompetitive/noncompetitive/mixed inhibition and estimating Ki
Version Compatibility
Python >= 3.10, NumPy >= 1.24, SciPy >= 1.11 (scipy.optimize.curve_fit), matplotlib >= 3.7 (optional, for plots).
Prerequisites
pip install numpy scipy matplotlib
Basic familiarity with steady-state enzyme kinetics (rate laws, the Michaelis-Menten steady-state assumption).
Beer-Lambert Law
A = ε · l · c
| Symbol | Meaning | Typical value |
|---|
| A | Absorbance (dimensionless) | 0.1–1.5 (linear range) |
| ε | Molar absorption coefficient (M⁻¹ cm⁻¹) | NADH at 340 nm: 6,220 |
| l | Path length (cm) | 1 cm (standard cuvette) |
| c | Concentration (M) | — |
Goal: Convert a measured absorbance into a molar concentration.
Approach: Invert Beer-Lambert; keep ε and l explicit arguments so the function works for any chromophore/cuvette.
epsilon_NADH = 6220
path_length = 1
def abs_to_conc(A, eps=epsilon_NADH, l=path_length):
"""Beer-Lambert inverse: absorbance -> molar concentration."""
return A / (eps * l)
print(f"{abs_to_conc(0.622) * 1e6:.1f} uM NADH")
Extracting Initial Velocity from a Progress Curve
Goal: Get v0 (µM/s) from a raw product-vs-time trace before product inhibition or substrate depletion bend the curve.
Approach: Fit a line only to the region where product is < 10% of the eventual plateau (Pmax); the slope is v0.
import numpy as np
def initial_velocity(time_s, product_uM, p_max_uM, frac=0.10):
"""Linear-region initial velocity: slope of product(t) below frac*Pmax."""
mask = product_uM < frac * p_max_uM
if mask.sum() < 3:
mask = np.arange(len(time_s)) < 15
slope, intercept = np.polyfit(time_s[mask], product_uM[mask], 1)
return slope, mask
Michaelis-Menten Equation
v = (Vmax · [S]) / (Km + [S])
| Parameter | Meaning |
|---|
| Vmax | Maximum velocity = kcat · [E]_total |
| Km | [S] at half-maximal velocity = (k₋₁ + k₂) / k₁ |
| kcat | Turnover number = Vmax / [E]_total |
| kcat/Km | Catalytic efficiency; diffusion limit ~10⁸–10⁹ M⁻¹s⁻¹ |
Km ≈ Ks (true dissociation constant) only when k₂ << k₋₁ (rapid equilibrium assumption).
Goal: Estimate Vmax and Km from a substrate-velocity table.
Approach: Nonlinear least squares (curve_fit) directly on v vs [S] — never fit a linearized transform as the primary method (see Pitfalls).
import numpy as np
from scipy.optimize import curve_fit
def michaelis_menten(S, vmax, km):
"""Michaelis-Menten steady-state rate law."""
return (vmax * S) / (km + S)
def fit_mm(substrate_uM, observed_v, p0=(80.0, 3.0)):
"""Fit Vmax/Km by nonlinear regression; returns (vmax, km, 95% CI half-widths)."""
popt, pcov = curve_fit(
michaelis_menten, substrate_uM, observed_v,
p0=p0, bounds=([0, 0], [np.inf, np.inf]), maxfev=5000,
)
se = np.sqrt(np.diag(pcov))
ci_95 = 1.96 * se
return popt[0], popt[1], ci_95
Linearization Methods (Diagnostic Use Only)
| Method | Plot | Slope | Intercept | Error distortion |
|---|
| Lineweaver-Burk | 1/v vs 1/[S] | Km/Vmax | 1/Vmax | Severe (amplifies low-[S] noise) |
| Eadie-Hofstee | v vs v/[S] | −Km | Vmax | Moderate |
| Hanes-Woolf | [S]/v vs [S] | 1/Vmax | Km/Vmax | Most uniform |
def linearizations(substrate_uM, observed_v):
"""Return (vmax, km) estimates from all three classical linearizations."""
inv_s, inv_v = 1 / substrate_uM, 1 / observed_v
lb_slope, lb_int = np.polyfit(inv_s, inv_v, 1)
lb_vmax, lb_km = 1.0 / lb_int, lb_slope * (1.0 / lb_int)
v_over_s = observed_v / substrate_uM
eh_slope, eh_int = np.polyfit(v_over_s, observed_v, 1)
eh_km, eh_vmax = -eh_slope, eh_int
s_over_v = substrate_uM / observed_v
hw_slope, hw_int = np.polyfit(substrate_uM, s_over_v, 1)
hw_vmax, hw_km = 1.0 / hw_slope, hw_int * (1.0 / hw_slope)
return {
"lineweaver_burk": (lb_vmax, lb_km),
"eadie_hofstee": (eh_vmax, eh_km),
"hanes_woolf": (hw_vmax, hw_km),
}
Modeling Enzyme Inhibition
Goal: Distinguish inhibition mechanism (competitive/uncompetitive/noncompetitive/mixed) and estimate Ki.
Approach: Fit velocity vs [S] at a fixed [I] with the mechanism-specific rate law; competitive inhibition raises Km_app without changing Vmax, uncompetitive lowers both proportionally, noncompetitive lowers Vmax only.
def competitive_inhibition(S, vmax, km, I, ki):
"""Inhibitor competes for the active site: Km_app = km*(1+I/ki), Vmax unchanged."""
alpha = 1 + I / ki
return (vmax * S) / (alpha * km + S)
def noncompetitive_inhibition(S, vmax, km, I, ki):
"""Inhibitor binds E and ES equally: Vmax_app = vmax/(1+I/ki), Km unchanged."""
alpha = 1 + I / ki
return (vmax / alpha * S) / (km + S)
def uncompetitive_inhibition(S, vmax, km, I, ki_prime):
"""Inhibitor binds only ES: both Vmax and Km scaled down by the same factor."""
alpha_p = 1 + I / ki_prime
return (vmax / alpha_p * S) / (km / alpha_p + S)
def mixed_inhibition(S, vmax, km, I, ki, ki_prime):
"""General case: alpha scales Km, alpha_prime scales Vmax independently."""
alpha, alpha_p = 1 + I / ki, 1 + I / ki_prime
return (vmax * S) / (alpha * km + alpha_p * S)
Goal: Estimate Ki for competitive inhibition from a series of Km_app values measured at different [I].
Approach: Fit Michaelis-Menten separately at each [I] to get Km_app, then use the linear relation Km_app = Km·(1 + [I]/Ki) — a Dixon-style secondary plot where the x-intercept is −Ki.
def ki_from_apparent_km(inhibitor_concs, apparent_kms):
"""Secondary (Dixon-style) plot: Km_app = Km*(1+[I]/Ki) -> slope=Km/Ki, intercept=Km."""
slope, intercept = np.polyfit(inhibitor_concs, apparent_kms, 1)
km_est = intercept
ki_est = intercept / slope
return km_est, ki_est
Experimental Design for Km Estimation
- Use substrate concentrations spanning 0.1·Km to 10·Km (if Km unknown, pilot with a wide range)
- Minimum 8–10 concentrations; include duplicates/triplicates
- Measure initial velocities only (linear phase); avoid >10% substrate depletion
- Include a negative control (no enzyme) and a blank (no substrate)
Pitfalls
- Km ≠ affinity unless rapid equilibrium holds: Km = (k₋₁ + k₂)/k₁; only when k₂ << k₋₁ does Km ≈ Ks — report "Km", not "binding affinity"
- Lineweaver-Burk distorts errors: low-[S] points (right side of the plot) dominate the fit because the reciprocal amplifies small-velocity noise — use nonlinear regression (
fit_mm) as the primary method, linearizations for visualization only
- curve_fit needs a reasonable p0: a bad initial guess converges to the wrong local minimum; plot the data first, take observed max as Vmax guess and [S] at ~half that as the Km guess
- Absorbance must stay in the linear range: Beer-Lambert deviates for A > 1.5; dilute samples if needed (inner-filter effects distort fluorescence assays even above A ≈ 0.1)
- Progress-curve non-linearity: if product inhibition or substrate depletion sets in early, the truly linear region may be only the first few time points — verify by repeating at a different enzyme concentration and confirming v0 scales linearly
- Units: keep [S] and Km in the same units (µM vs mM mismatch is a common bug); Vmax units must match the velocity axis
See Also
bio-machine-learning-model-validation — bootstrap/CI methodology for fitted parameters
bio-chemoinformatics-admet-prediction — downstream small-molecule property prediction after Ki characterization
statistical-analysis — general nonlinear regression and model comparison (AIC/BIC) background