4-Parameter Logistic (4PL) Curve Fitting
A hands-on tutorial for biologists — no coding required.
When you test a drug at increasing concentrations, the biological response typically follows an S-shaped (sigmoidal) curve. At low doses, little happens. As the dose rises, response increases steeply. At high doses, the system saturates and response levels off.
The 4-Parameter Logistic (4PL) model captures this shape mathematically. It is the standard method for calculating IC50 and EC50 values in pharmacology and biochemical assays. This tutorial walks you through the equation, the fitting process, and lets you interact with real data.
The 4PL Equation
The 4PL model describes response as a function of concentration $x$:
$$f(x) = A + \frac{D - A}{1 + \left(\dfrac{C}{x}\right)^B}$$
There are four parameters — one for each part of the curve's shape:
Response at zero concentration (background / baseline signal).
Steepness of the curve. B > 1 = steep; B < 1 = shallow; B = 1 = classic Hill.
Concentration at half-maximal response. The key output of most assays.
Maximum response at saturating concentration (full inhibition / activation).
Interactive Curve Fitter
The plot below shows real inhibition data (red dots) for one compound from PubChem AID 1619, along with the best-fit 4PL curve (blue line). The IC50 is annotated with a dashed green line.
Data source: PubChem BioAssay AID 1619 (SID 842727). IC50 reported by PubChem: 13.82 µM.
Parameter Playground
Move the sliders to see how each parameter changes the shape of the 4PL curve in real time.
How the Fitting Works
The curve is fitted using the Nelder-Mead algorithm — a numerical optimizer that finds the parameter values minimizing the sum of squared differences between the model and your data. No matrix algebra, no derivatives needed.
Step 1: Define the 4PL function
// Ascending form: % inhibition increases with concentration
// A = bottom, B = Hill slope, C = IC50, D = top
function fourPL(x, A, B, C, D) {
return A + (D - A) / (1 + Math.pow(C / x, B));
}
Step 2: Measure the error (sum of squared residuals)
function sumSquares(params, conc, obs) {
const [A, B, C, D] = params;
if (C <= 0 || B <= 0) return 1e12; // reject impossible values
return obs.reduce((acc, y, i) => {
return acc + (y - fourPL(conc[i], A, B, C, D)) ** 2;
}, 0);
}
Step 3: Auto-generate a starting guess from the data
function initialGuess(conc, obs) {
const A = Math.min(...obs);
const D = Math.max(...obs);
const mid = (A + D) / 2;
let bestIdx = 0, bestDist = Infinity;
obs.forEach((y, i) => {
const d = Math.abs(y - mid);
if (d < bestDist) { bestDist = d; bestIdx = i; }
});
return [A, 1.0, conc[bestIdx], D]; // [A, B, C, D]
}
Step 4: Run Nelder-Mead to find the best parameters
// The optimizer tries many combinations of A, B, C, D
// and homes in on the values that minimize the error.
// Returns: { x: [A, B, C, D], fx: finalError }
const result = nelderMead(
params => sumSquares(params, conc, obs),
initialGuess(conc, obs)
);
Step 5: Plot with Plotly.js
Plotly.newPlot('plot', [
// Raw data points
{ x: conc, y: obs, mode: 'markers', name: 'Data',
marker: { size: 10, color: '#e74c3c' } },
// Smooth fitted curve
{ x: xSmooth, y: ySmooth, mode: 'lines', name: '4PL Fit',
line: { color: '#2980b9', width: 2.5 } }
], {
xaxis: { type: 'log', title: 'Concentration (µM)' },
yaxis: { title: '% Inhibition' }
});
References
- DeLean, A., Munson, P.J. & Rodbard, D. (1978). Simultaneous analysis of families of sigmoidal curves. Am J Physiol, 235(2):E97–102.
- Sebaugh, J.L. (2011). Guidelines for accurate EC50/IC50 estimation. Pharmaceutical Statistics, 10(2):128–134.
- Nelder, J.A. & Mead, R. (1965). A simplex method for function minimization. Computer Journal, 7(4):308–313.
- PubChem BioAssay AID 1619 — MLSCN: Inhibitors of P. falciparum M17LAP. Southern Research Institute.