# Bio Applied Biochemistry

> 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.

- Skill: `pavel-kravchenko/bio-applied-biochemistry` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-biochemistry`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-biochemistry/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-biochemistry

---


# 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
```bash
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.

```python
epsilon_NADH = 6220   # M^-1 cm^-1 at 340 nm
path_length = 1        # cm

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")  # A=0.622 -> 100 uM
```

## 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.

```python
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:                       # fallback: first 15 points
        mask = np.arange(len(time_s)) < 15
    slope, intercept = np.polyfit(time_s[mask], product_uM[mask], 1)
    return slope, mask                       # slope = v0 in uM/s
```

## 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).

```python
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   # vmax, km, [ci_vmax, ci_km]
```

## 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 |

```python
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),   # best of the three linear methods
    }
# Always prefer fit_mm() (nonlinear regression) over these for reported values.
```

## 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.

```python
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.

```python
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

