# ELISA standard curve: fitting a 4PL curve, reading off concentrations, and values outside the range

> Fit an ELISA standard curve with a four-parameter logistic (4PL) model, weighted for the larger scatter at high signal, read concentrations only inside the range of the standards that pass, and average replicate wells first. Python and R code.

Canonical: https://www.licklider.ai/guides/elisa-standard-curve/
Section: Guides › Standard curves, detection limits and omics
Author: Licklider · Published 2026-11-11 · Last reviewed 2026-11-11 (UTC)

> **Quick answer.** ELISA standard curves are S-shaped, so fit a **four-parameter logistic (4PL)** curve (or a 5PL if the curve is clearly asymmetric) rather than a straight line through all standards; this works when the top and bottom standards approach the plateaus. Give less weight to the high-signal standards, whose scatter is larger. Check the fit by **back-calculating each standard**: its concentration read from the curve should be within ±20% of the nominal value (±25% at the lowest and highest standards), and at least 75% of the standards (not counting anchor points), with at least six levels including the lowest and highest, should pass. Read sample concentrations **only between the lowest and highest standards that pass** (the LLOQ and ULOQ), and only where the curve is still steep: report samples below as "below the LLOQ" and dilute and re-assay samples above, rather than extrapolating. Average replicate wells for each sample, and use **one value per animal** in the group comparison.

**Terms used below.** *Standard curve*: the signal (optical density, OD) of known concentrations, used to read off unknowns. *4PL model*: an S-shaped curve with four parameters: the bottom and top plateaus, the EC50 (the concentration halfway between them) and the slope. *5PL model*: a 4PL with a fifth parameter for asymmetry. *Weighting*: giving each point an influence inversely related to its expected scatter; here each well is weighted by 1/OD², so that relative errors count equally. *Back-calculated concentration*: a standard's concentration read from the fitted curve, compared with its nominal value. *LLOQ and ULOQ*: the lower and upper limits of quantification, the lowest and highest concentrations measured with acceptable accuracy and precision. *Interpolation*: reading a concentration from the curve inside the range of the standards; *extrapolation* is reading outside it. *Technical replicate*: a repeat well of the same sample. *Plateau*: a flat part of the curve, where large changes in concentration give small changes in OD. *Nominal concentration*: the concentration a standard was made up to. *CV (coefficient of variation)*: the SD of replicate wells as a percentage of their mean. *BQL*: below the limit of quantification (the FDA guidance's label for a result below the LLOQ). *Anchor points*: extra standards outside the reportable range, used only to help fit the curve's plateaus. *Reportable range*: the concentrations between the lowest and highest standards that pass. *Censored*: known only to lie below (or above) a limit. *Inverse prediction*: reading a concentration from a fitted curve. *Block*: a grouping, such as a plate, included in the analysis so that differences between plates do not bias the comparison. *QC samples*: samples of known concentration run on each plate to check the run. *Hook effect*: a falsely low signal at very high concentrations.

## What the guidelines expect

These guidelines were written for regulated bioanalysis; for research assays they are a sensible benchmark rather than a rule.

| Step | Do | Basis |
| --- | --- | --- |
| Model | 4PL (5PL for an asymmetric curve), weighted, when standards reach near both plateaus | Findlay & Dillard 2007; ICH M10 §4.2.3 |
| Check the curve | Back-calculate every standard: within ±20% of nominal, ±25% at the LLOQ and ULOQ; at least 75% of standards (excluding anchor points) and at least six levels, including the LLOQ and ULOQ, passing | ICH M10 §4.2.3 |
| Check the run | QC samples on each plate (ICH M10: at least two-thirds within ±20%); dilution linearity and no hook effect for samples above the ULOQ | ICH M10 §4.3.2, §4.2.6 |
| Range | Report only concentrations between the LLOQ and ULOQ; in a research assay, the lowest and highest standards that pass on each plate give that plate's reportable range, which is narrower than a validated range | FDA 2018; ICH M10 |
| Below the LLOQ | Report as below the LLOQ; analyse as described in [values below the detection limit](/guides/values-below-detection-limit/) | FDA 2018 |
| Above the ULOQ | Dilute and re-assay (or extend and revalidate the curve) | FDA 2018 |
| Replicate wells | Average the replicate ODs and read the concentration from the mean (as here), or average the concentrations read from each well; ICH M10 allows either, so say which | ICH M10 §4.2 |

Findlay and Dillard describe the typical response as "a sigmoidal relationship with concentration" and state that "the currently accepted reference model for these calibration curves is the 4-parameter logistic (4-PL) model" ([Findlay & Dillard 2007](https://doi.org/10.1208/aapsj0902029)). The FDA guidance notes that the scatter of ligand-binding standard curves changes with the mean response, and that "study samples with concentrations listed below the LLOQ should be reported as below the LLOQ (BQL). Study samples with concentrations above the ULOQ should be diluted and re-analyzed, or the standard curve should be extended and revalidated" ([FDA 2018](https://www.fda.gov/files/drugs/published/Bioanalytical-Method-Validation-Guidance-for-Industry.pdf); for drug development FDA now refers to ICH M10). ICH M10 adds that a 4PL or 5PL is used "if there are data points near the lower and upper asymptotes" and sets the acceptance limits: back-calculated standards "within ±25% of the nominal concentration at the LLOQ and ULOQ, and within ±20% at all other levels" ([ICH M10](https://database.ich.org/sites/default/files/M10_Guideline_Step4_2022_0524.pdf)).

## Worked example: one plate, 8 standards and 6 samples in duplicate

Constructed data: standards from 7.8 to 1000 pg/mL and six samples, each in two wells. By construction, the true curve is a 4PL (EC50 150 pg/mL) with 4% well-to-well noise, and the samples' true concentrations are 5, 20, 80, 300, 700 and 1500 pg/mL. The random seed was the first one tried; it was not selected. Stored code and data: [plate.csv](/guides/elisa-standard-curve/plate.csv), [make_data.py](/guides/elisa-standard-curve/make_data.py), [analysis.py](/guides/elisa-standard-curve/analysis.py), [analysis.R](/guides/elisa-standard-curve/analysis.R) and [the analysis output](/guides/elisa-standard-curve/analysis_output.txt); analysis.py and analysis.R print the same output.

| Standard (pg/mL) | 1000 | 500 | 250 | 125 | 62.5 | 31.25 | 15.6 | 7.8 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Weighted 4PL, back-calculated % of nominal | 122.1 | 89.1 | 104.3 | 98.5 | 100.7 | 101.1 | 98.4 | 100.9 |
| Straight line, back-calculated % of nominal | 83 | 135 | 213 | 227 | 124 | −217 | −962 | −2395 |

All standards pass with the weighted 4PL (judged on the mean of duplicate wells; the top standard read 122.1%, inside its ±25% limit), and the duplicate CVs were 0.2% to 4.7%. A straight line through the same standards misreads nearly every level, from 83% at the top standard to negative concentrations at the bottom.

| Sample | True (pg/mL) | Mean OD | Read from the 4PL |
| --- | --- | --- | --- |
| S1 | 5 | 0.095 | Below the LLOQ (< 7.8 pg/mL) |
| S2 | 20 | 0.269 | 19.6 pg/mL |
| S3 | 80 | 0.925 | 78.6 pg/mL |
| S4 | 300 | 2.057 | 333.7 pg/mL |
| S5 | 700 | 2.203 | 423.6 pg/mL, flagged as on the upper plateau: dilute and re-assay rather than report |
| S6 | 1500 | 2.605 | Above the ULOQ: dilute and re-assay |

S2 and S3, on the steep middle of the curve, are read well. S5 lies inside the range of the standards but on the upper plateau, where small OD differences correspond to large concentration differences: it is read as 424 pg/mL against a true 700. Passing back-calculated standards are necessary but not sufficient: the top standard passed here (122.1%, CV 1.3%), yet a sample near it was misread. So set a rule in advance for the flat top of the curve; analysis.py and analysis.R flag any sample whose OD lies in the upper quarter of the range between the fitted plateaus (an example rule, which caught S5), and such samples are diluted so that they land in the steep part.

```python
# Condensed: analysis.py also back-calculates the standards and compares a straight line
import numpy as np, pandas as pd
from scipy import optimize

d = pd.read_csv("plate.csv")                         # columns: type (standard/sample), id, conc_pg_ml, od; one row per well
st = d[d.type == "standard"]
def f4(x, bottom, top, ec50, slope): return top + (bottom - top) / (1 + (x / ec50) ** slope)
def inv(y, bottom, top, ec50, slope): return ec50 * ((bottom - top) / (y - top) - 1) ** (1 / slope)
# starting values in the order bottom, top, EC50, slope: set EC50 near your middle standard
p, _ = optimize.curve_fit(f4, st.conc_pg_ml, st.od, p0=[0.05, 3, 100, 1], sigma=st.od, maxfev=20000)   # weights 1/OD^2
od = d[d.type == "sample"].groupby("id").od.mean()  # average duplicate wells first
lo, hi = f4(st.conc_pg_ml.min(), *p), f4(st.conc_pg_ml.max(), *p)   # OD at the lowest and highest standard
flat = p[0] + 0.75 * (p[1] - p[0])                  # example rule: upper quarter of the OD range = plateau
conc = inv(od.clip(lo, hi), *p).round(1).astype(str)
conc[od > flat] = "upper plateau: dilute"; conc[od < lo] = "below LLOQ"; conc[od > hi] = "above ULOQ: dilute"
print(100 * inv(st.groupby("conc_pg_ml").od.mean(), *p) / st.groupby("conc_pg_ml").od.mean().index)   # back-calculated % of nominal
print(conc)
```

```r
library(drc)                                         # dose-response package (Ritz et al. 2015)
d <- read.csv("plate.csv"); st <- d[d$type == "standard", ]
fit <- drm(od ~ conc_pg_ml, data = st, fct = LL.4(), weights = st$od)   # drc takes weights on the SD scale: 1/OD^2 weighting
od <- tapply(d$od[d$type == "sample"], d$id[d$type == "sample"], mean)
conc <- ED(fit, od, type = "absolute", display = FALSE)[, "Estimate"]   # concentrations read from the curve
rng <- range(st$conc_pg_ml); conc[conc < rng[1]] <- NA; conc[conc > rng[2]] <- NA   # outside the standards: not reported
conc
```

## Before you report concentrations

1. **Use enough standards across the range**, at least six levels including the LLOQ and ULOQ, plus a blank that is not used in the fit; if your kit's protocol subtracts the blank OD, subtract it from standards and samples alike before fitting, and say so.
2. **Inspect the back-calculated standards** on every plate; drop a failing standard only by a rule written in advance, and refit.
3. **Keep each sample inside the steep part of the curve.** Dilute samples that read near the top, and multiply the concentration obtained from the curve by the dilution factor (a sample diluted 1:4 that reads 210 pg/mL contains 840 pg/mL).
4. **Average technical replicate wells** and check their agreement (for example a coefficient of variation limit set in advance); the animal, not the well, is the unit ([what counts as n](/guides/what-counts-as-n/)).
5. **Fit each plate separately** with its own standards, and include plate as a block in the group comparison if animals are spread over plates.
6. **Values below the LLOQ** are censored, not zero: see [values below the detection limit](/guides/values-below-detection-limit/).
7. **Then compare groups** on one concentration per animal, usually on the log scale because concentrations are skewed ([statistics on fold change](/guides/statistics-on-fold-change/)).

## Check what an AI assistant ran

1. **The assistant fitted a straight line (linear regression) through all standards.** Ask for a 4PL fit and the back-calculated standards.
2. **The assistant extrapolated concentrations beyond the highest or lowest standard.** Ask for them to be flagged as above the ULOQ or below the LLOQ.
3. **The assistant treated each well as a separate data point** in the group comparison. Ask for one value per animal.
4. **The assistant set values below the LLOQ to zero.** They are censored, not zero; ask how they were handled and point to the censoring methods.

## Report it

> Cytokine concentrations were measured by ELISA in duplicate wells. Standard curves (8 levels, [7.8] to [1000] pg/mL) were fitted on each plate with a four-parameter logistic model weighted by 1/OD² (R 4.3.3; replace with your software); back-calculated standards were [89.1]% to [122.1]% of nominal, within ±20% at the intermediate levels and ±25% at the lowest and highest. Sample concentrations were read from the mean OD of duplicate wells; [n] samples below the lower limit of quantification (LLOQ; [7.8] pg/mL) were [reported as below the LLOQ / analysed as censored values]; samples above [1000] pg/mL were diluted and re-assayed.

Replace the bracketed values with your own.

## References

- Findlay JWA, Dillard RF (2007). Appropriate calibration curve fitting in ligand binding assays. *The AAPS Journal* 9:E260–E267. [doi:10.1208/aapsj0902029](https://doi.org/10.1208/aapsj0902029)
- US Food and Drug Administration (2018). Bioanalytical Method Validation: Guidance for Industry. [fda.gov](https://www.fda.gov/files/drugs/published/Bioanalytical-Method-Validation-Guidance-for-Industry.pdf) (checked 11 November 2026)
- ICH (2022). M10: Bioanalytical method validation and study sample analysis. [database.ich.org](https://database.ich.org/sites/default/files/M10_Guideline_Step4_2022_0524.pdf) (checked 11 November 2026)
- Ritz C, Baty F, Streibig JC, Gerhard D (2015). Dose-response analysis using R. *PLOS ONE* 10:e0146021. [doi:10.1371/journal.pone.0146021](https://doi.org/10.1371/journal.pone.0146021)

## Related

- [Values below the detection limit](https://www.licklider.ai/guides/values-below-detection-limit/)
- [Comparing IC50 values](https://www.licklider.ai/guides/compare-ic50-dose-response-curves/)
- [What counts as n?](https://www.licklider.ai/guides/what-counts-as-n/)
- [Statistics on fold change](https://www.licklider.ai/guides/statistics-on-fold-change/)

## How this page was made

- Written and published by Licklider. Recommendations cite the ICH M10 and FDA bioanalytical guidance and peer-reviewed methods papers listed under References; cited passages were checked against the source text.
- The worked example uses constructed data. Its numbers were produced by stored code (Python 3.13 with SciPy 1.18.1; R 4.3.3, base R), and the two languages print identical output; the R page snippet uses drc 4.0-0 and gives the same concentrations for the samples inside the range ([drc_check.R](/guides/elisa-standard-curve/drc_check.R), [output](/guides/elisa-standard-curve/drc_check_output.txt)).
- Separate review passes checked the statistics, the citations and readability before publication.
