---
title: "Lab 29: Dose-Response Curves & IC50 Calculation"
subtitle: "BI 255 · Bethune-Cookman University · Fall 2026"
author: "Dr. Rosie Stanbrook-Buyer"
date: "November 6, 2026"
format:
html:
theme: cosmo
toc: true
toc-location: left
toc-title: "In This Lab"
toc-depth: 3
number-sections: false
code-fold: false
code-tools: true
highlight-style: github
smooth-scroll: true
embed-resources: true
callout-appearance: default
execute:
warning: false
message: false
echo: true
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```
------------------------------------------------------------------------
## From Drug Concentration to Biological Response
One of the most fundamental experiments in pharmacology, toxicology, and drug discovery is the **dose-response experiment**: you expose cells, organisms, or enzymes to increasing concentrations of a drug and measure the effect. The result is a sigmoidal curve, and the most important summary of that curve is the **IC50** — the concentration that produces 50% of the maximum effect.
::: callout-note
## Learning Objectives
By the end of this lab you will be able to:
1. Explain the shape of a sigmoidal dose-response curve and its four key parameters
2. Plot dose-response data on a log-concentration scale
3. Fit a four-parameter log-logistic model using the `drc` package
4. Extract and interpret the IC50 with confidence intervals
5. Compare IC50 values between two drugs or conditions
6. Understand the Hill equation and its biological meaning
:::
------------------------------------------------------------------------
## Part 1: The Shape of a Dose-Response Curve
The **four-parameter log-logistic (4PL) model** describes most biological dose-response relationships:
y = Bottom + (Top − Bottom) / (1 + (IC50 / x)^Hill)
Where:
- **Bottom**: minimum response (e.g., 0% inhibition)
- **Top**: maximum response (e.g., 100% inhibition)
- **IC50**: concentration producing 50% of maximum effect
- **Hill slope (n)**: steepness of the curve (also called the Hill coefficient)
```{r}
# Simulate a dose-response experiment
# Drug: a cytotoxic compound tested against a cancer cell line
# Response: cell viability (% of untreated control)
set.seed(42)
concentrations <- c(0.001, 0.003, 0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100) # uM
# True parameters
bottom <- 5 # minimum viability (%)
top <- 100 # maximum viability (%)
ic50 <- 1.2 # true IC50 (uM)
hill <- 1.5 # Hill slope
# 4PL model
viability_true <- bottom + (top - bottom) / (1 + (ic50 / concentrations)^hill)
# Add biological noise (3 replicates per concentration)
viability_obs <- sapply(viability_true,
function(mu) rnorm(3, mean = mu, sd = 4))
viability_mean <- colMeans(viability_obs)
viability_sd <- apply(viability_obs, 2, sd)
# Also include a control (0 drug)
dr_df <- data.frame(
conc = concentrations,
viability = viability_mean,
sd = viability_sd
)
head(dr_df, 6)
```
```{r}
library(ggplot2)
ggplot(dr_df, aes(x = conc, y = viability)) +
geom_errorbar(aes(ymin = viability - sd, ymax = viability + sd),
width = 0.05, colour = "grey60") +
geom_point(colour = "#2C5F8A", size = 3) +
scale_x_log10(breaks = c(0.001, 0.01, 0.1, 1, 10, 100),
labels = c("0.001", "0.01", "0.1", "1", "10", "100")) +
geom_hline(yintercept = 50, linetype = "dashed", colour = "#C8102E") +
labs(title = "Cytotoxic Drug — Dose-Response Data",
subtitle = "Dashed line = 50% viability (IC50 reference)",
x = "Concentration (uM, log scale)",
y = "Cell Viability (% of control)") +
theme_classic(base_size = 13)
```
::: callout-important
## Why Log Scale on x-axis?
Concentrations in dose-response experiments span orders of magnitude (0.001 to 100 uM here). On a linear scale, the low concentrations are invisible. On a **log scale**, each doubling of concentration is equally spaced — and the sigmoidal curve becomes clearly visible as an S-shape.
Always plot dose-response data on a log concentration scale.
:::
------------------------------------------------------------------------
## Part 2: Fitting the 4PL Model with the `drc` Package
```{r}
# install.packages("drc")
library(drc)
# Fit the four-parameter log-logistic model
# LL.4() = 4-parameter log-logistic
# Parms: e (EC50/IC50), b (Hill slope), c (bottom), d (top)
dr_model <- drm(viability ~ conc,
data = dr_df,
fct = LL.4(names = c("Hill", "Bottom", "Top", "IC50")))
summary(dr_model)
```
```{r}
# Extract IC50 with confidence interval
IC50_est <- ED(dr_model, respLev = 50, interval = "delta", type = "relative")
IC50_est
```
```{r}
# Plot fitted curve with confidence band
plot(dr_model,
type = "all",
col = "#2C5F8A",
pch = 16,
cex = 1.3,
log = "x",
xlab = "Concentration (uM)",
ylab = "Cell Viability (%)",
main = "4PL Dose-Response Fit",
broken = FALSE)
abline(h = 50, col = "#C8102E", lty = 2)
```
------------------------------------------------------------------------
## Part 3: Manual Curve Plotting with ggplot2
For publication-quality figures, plot the fitted curve manually over the data:
```{r}
# Generate smooth prediction line from fitted model
conc_seq <- data.frame(conc = exp(seq(log(0.001), log(100), length.out = 300)))
fitted_vals <- predict(dr_model, newdata = conc_seq, interval = "confidence")
plot_df <- data.frame(
conc = conc_seq$conc,
fit = fitted_vals[, 1],
lower = fitted_vals[, 2],
upper = fitted_vals[, 3]
)
ic50_val <- IC50_est[1, "Estimate"]
ggplot() +
geom_ribbon(data = plot_df, aes(x = conc, ymin = lower, ymax = upper),
fill = "#A8C4D9", alpha = 0.5) +
geom_line(data = plot_df, aes(x = conc, y = fit),
colour = "#2C5F8A", linewidth = 1.2) +
geom_errorbar(data = dr_df,
aes(x = conc, ymin = viability - sd, ymax = viability + sd),
width = 0.05, colour = "grey50") +
geom_point(data = dr_df, aes(x = conc, y = viability),
colour = "#2C5F8A", size = 3) +
geom_hline(yintercept = 50, linetype = "dashed", colour = "#C8102E") +
geom_vline(xintercept = ic50_val, linetype = "dashed", colour = "#C8102E") +
annotate("text", x = ic50_val * 1.3, y = 5,
label = paste0("IC50 = ", round(ic50_val, 2), " uM"),
colour = "#C8102E", hjust = 0, size = 4) +
scale_x_log10() +
labs(title = "Cytotoxic Drug: Fitted 4PL Dose-Response Curve",
subtitle = "Shaded region = 95% confidence band",
x = "Concentration (uM, log scale)",
y = "Cell Viability (% of control)") +
theme_classic(base_size = 13)
```
------------------------------------------------------------------------
## Part 4: Comparing Two Drugs
```{r}
set.seed(77)
# Drug B: same mechanism, but less potent (higher IC50)
viability_B <- bottom + (top - bottom) / (1 + (4.8 / concentrations)^1.2) +
rnorm(length(concentrations), 0, 4)
viability_B <- pmin(pmax(viability_B, 0), 105)
# Combine data
dr_df_B <- data.frame(conc = concentrations, viability = viability_B, drug = "Drug B")
dr_df_A <- data.frame(conc = concentrations, viability = dr_df$viability, drug = "Drug A")
dr_combined <- rbind(dr_df_A, dr_df_B)
# Fit separate models for each drug
model_A <- drm(viability ~ conc, data = dr_df_A[, 1:2], fct = LL.4())
model_B <- drm(viability ~ conc, data = dr_df_B[, 1:2], fct = LL.4())
cat("Drug A IC50:", round(ED(model_A, 50, type = "relative")[1], 3), "uM\n")
cat("Drug B IC50:", round(ED(model_B, 50, type = "relative")[1], 3), "uM\n")
```
::: callout-note
## Interpreting IC50 in Drug Comparison
A **lower IC50** means greater potency — the drug achieves the same effect at a lower concentration.
Drug A IC50 ≈ 1.2 uM vs Drug B IC50 ≈ 4.8 uM: Drug A is approximately 4-fold more potent than Drug B. In drug discovery, potency differences of this magnitude are highly meaningful — a 4-fold difference affects dosing, side effect profile, and manufacturing cost.
Note that IC50 alone does not determine clinical utility — selectivity (activity against the target vs. off-target effects), bioavailability, and toxicity are equally important.
:::
------------------------------------------------------------------------
## Part 5: The Hill Equation in Biochemistry
The dose-response equation is mathematically equivalent to the **Hill equation** from enzyme kinetics:
v = Vmax × [S]^n / (Km^n + [S]^n)
When n = 1, this is the Michaelis-Menten equation. When n > 1, it describes cooperative binding (e.g., haemoglobin-oxygen binding, allosteric enzymes).
```{r}
# Simulate cooperative enzyme (haemoglobin-like)
# Oxygen partial pressure (mmHg) vs oxygen saturation (%)
pO2 <- seq(0, 150, by = 2)
hill_n <- 2.7 # real Hill coefficient for haemoglobin ~2.7
P50 <- 26 # P50 for haemoglobin (pO2 at 50% saturation) ≈ 26 mmHg
saturation <- 100 * pO2^hill_n / (P50^hill_n + pO2^hill_n)
saturation_obs <- saturation + rnorm(length(pO2), 0, 1.5)
saturation_obs <- pmax(0, pmin(100, saturation_obs))
hb_df <- data.frame(pO2 = pO2, sat = saturation_obs)
ggplot(hb_df, aes(x = pO2, y = sat)) +
geom_point(colour = "#C8102E", size = 1.5, alpha = 0.7) +
geom_line(data = data.frame(pO2 = pO2, sat = saturation),
colour = "#C8102E", linewidth = 1.3) +
geom_hline(yintercept = 50, linetype = "dashed", colour = "grey40") +
geom_vline(xintercept = P50, linetype = "dashed", colour = "grey40") +
annotate("text", x = 30, y = 10,
label = paste0("Hill n = ", hill_n, "\nP50 = ", P50, " mmHg"),
hjust = 0, size = 4) +
labs(title = "Haemoglobin Oxygen Dissociation Curve",
subtitle = "Cooperative binding: Hill coefficient n = 2.7",
x = "Partial Pressure of Oxygen (mmHg)",
y = "Oxygen Saturation (%)") +
theme_classic(base_size = 13)
```
------------------------------------------------------------------------
## 3-Minute Knowledge Check
*Close your notes. Answer these on your own — you have 3 minutes. We'll go through the answers together after.*
::: callout-caution
## Knowledge Check Questions
**1.** In the 4PL dose-response model, what does the Hill slope (Hill coefficient) control?
**2.** Drug X has IC50 = 0.05 uM and Drug Y has IC50 = 12 uM. Which drug is more potent, and by approximately how many fold?
**3.** Why must dose-response data be plotted on a logarithmic x-axis?
**4.** In the `drc` package, what does `ED(model, 50, type = "relative")` calculate?
a) The dose that produces 50 units of response b) The dose that produces 50% of the maximum response c) The dose where 50 cells survive d) The effective dose at the 50th percentile of the population
**5.** True or False: A Hill coefficient of exactly 1 in a dose-response curve indicates non-cooperative binding consistent with simple Michaelis-Menten kinetics.
:::
::: {.callout-caution collapse="true"}
## Answers (Only reveal if you have answered the above questions)
**1.** The Hill slope controls the **steepness** of the sigmoidal curve. A Hill slope of 1 produces a gradual S-curve spanning about 2 log units of concentration from 10% to 90% effect. Slopes greater than 1 produce a steeper curve (effects switch on more abruptly), and slopes less than 1 produce a shallower curve. In receptor pharmacology, the Hill slope can indicate cooperativity or the presence of multiple binding sites.
**2.** Drug X is more potent by approximately **240-fold** (12 / 0.05 = 240). Lower IC50 = higher potency. Drug X achieves 50% inhibition at 0.05 uM, while Drug Y requires 12 uM to produce the same effect. In medicinal chemistry, a 240-fold difference in potency is extremely large and would strongly favour Drug X as a lead compound.
**3.** Concentrations tested in dose-response experiments span many orders of magnitude (e.g., 0.001 to 100 uM = 100,000-fold range). On a linear scale, all low concentrations collapse into a tiny region near zero and the sigmoidal shape is invisible. The log scale spreads each concentration decade equally, making the sigmoidal curve visible and enabling the IC50 to be read directly from the midpoint of the curve.
**4.** b) The dose that produces 50% of the maximum response — `ED(model, respLev = 50, type = "relative")` calculates the Effective Dose at 50% of the response span. "Relative" means 50% of the fitted top-bottom span, not 50 absolute units. This is the IC50 (for inhibition) or EC50 (for stimulation).
**5.** True — A Hill coefficient of exactly 1 indicates simple hyperbolic (Michaelis-Menten) kinetics: non-cooperative binding where ligand binding at one site does not influence other sites. Values greater than 1 indicate positive cooperativity (binding makes subsequent binding easier — haemoglobin is the classic example). Values less than 1 can indicate negative cooperativity or heterogeneous receptor populations.
:::
------------------------------------------------------------------------
## Lab 29 Checklist
Before you leave, make sure you can:
- [ ] Explain the four parameters of the 4PL model: Bottom, Top, IC50, Hill slope
- [ ] Plot dose-response data with error bars on a log x-axis in ggplot2
- [ ] Fit a 4PL model with `drm(y ~ x, fct = LL.4())`
- [ ] Extract IC50 with confidence intervals using `ED(model, 50, type = "relative")`
- [ ] Compare IC50 values between two drug conditions
- [ ] Explain the biological connection between the 4PL curve and the Hill equation in enzyme kinetics
::: callout-tip
## Bonus Challenge
The `drc` package includes the built-in dataset `ryegrass` (herbicide dose-response on ryegrass root length). Run `data(ryegrass)` and explore it. Fit a 4PL model and extract the IC50 with 95% CI. Then fit a 3PL model (fixing Bottom = 0 with `LL.3()`) and compare both models with `AIC()`. Which model fits better? Produce a publication-quality ggplot2 figure with the fitted curve and both the data and IC50 annotated.
:::
------------------------------------------------------------------------
## Before Next Class (Monday, Week 13)
- Monday (Lab 30): Spatial mapping in R — species occurrence data, the `sf` package, and ggplot2 maps
- **Quiz 7** is Monday of Week 13 — covers Labs 25–29
------------------------------------------------------------------------