Get started with EC50 workflows

Kaique S Alves

2026-05-24

Goal

This article shows the recommended workflow for most users:

  1. Check the data before fitting.
  2. Fit candidate dose-response models.
  3. Inspect model quality and failed fits.
  4. Choose the best-supported model within each isolate and stratum.
  5. Plot fitted curves and export a reporting table.

The package keeps the fitted result as a data frame, but it also stores the model metadata needed for plotting, prediction, diagnostics, and reporting.

Packages

library(ec50estimator)
library(drc)
library(ggplot2)

Example Data

multi_isolate is a simulated mycelial-growth dataset with repeated dose measurements for fungal isolates. The examples use a small subset so the workflow is easy to inspect.

data(multi_isolate)

example_data <- subset(
  multi_isolate,
  isolate %in% 1:5 & fungicida == "Fungicide A"
)

head(example_data)
##   isolate   field   fungicida  dose     growth
## 1       1 Organic Fungicide A 0e+00 20.2082399
## 2       1 Organic Fungicide A 1e-05 20.1168279
## 3       1 Organic Fungicide A 1e-04 19.2479678
## 4       1 Organic Fungicide A 1e-03 15.8123455
## 5       1 Organic Fungicide A 1e-02  7.3206757
## 6       1 Organic Fungicide A 1e-01  0.6985264

Check the Data

Run check_ec50_data() before fitting. It returns one row per isolate and stratum, with flags for common problems such as missing values, too few doses, nonpositive doses for log-scale plots, and response groups with no variation.

data_check <- check_ec50_data(
  example_data,
  response = "growth",
  dose = "dose",
  isolate = "isolate",
  strata = "field"
)

data_check
##   ID        field n_obs n_doses missing_response missing_dose nonpositive_dose
## 1  1      Organic    35       7                0            0                5
## 2  2 Conventional    35       7                0            0                5
## 3  3      Organic    35       7                0            0                5
## 4  4 Conventional    35       7                0            0                5
## 5  5      Organic    35       7                0            0                5
##   duplicated_rows no_response_variation too_few_observations too_few_doses
## 1               0                 FALSE                FALSE         FALSE
## 2               0                 FALSE                FALSE         FALSE
## 3               0                 FALSE                FALSE         FALSE
## 4               0                 FALSE                FALSE         FALSE
## 5               0                 FALSE                FALSE         FALSE

Nonpositive dose values are flagged because log-scaled curves cannot draw them. They can still be useful as untreated controls, but they are omitted from the log-scale prediction grid used by plot_EC50_curves().

Fit Candidate Models

Use ec50_multimodel() when you want to compare more than one drc model. The same model list is fitted within each isolate and field stratum.

fit <- ec50_multimodel(
  growth ~ dose,
  data = example_data,
  isolate_col = "isolate",
  strata_col = "field",
  fct = list(drc::LL.3(), drc::LL.4(), drc::W2.3()),
  interval = "delta"
)

head(fit)
##     ID        field    Estimate   Std..Error       Lower       Upper    logLik
## 1    1      Organic 0.006072082 0.0005740341 0.004902813 0.007241351 -45.15079
## 36   2 Conventional 0.101455765 0.0076364691 0.085900787 0.117010744 -43.53183
## 71   3      Organic 0.003776957 0.0002432571 0.003281459 0.004272456 -36.09845
## 106  4 Conventional 0.079971237 0.0055655891 0.068634503 0.091307971 -38.42154
## 141  5      Organic 0.006122508 0.0004575060 0.005190599 0.007054418 -41.41058
## 11   1      Organic 0.006364103 0.0007031475 0.004930024 0.007798182 -44.69257
##           IC Lack.of.fit   Res.var model
## 1   98.30158   0.7271292 0.8451681  LL.3
## 36  95.06365   0.9866424 0.7704874  LL.3
## 71  80.19689   0.3897432 0.5038400  LL.3
## 106 84.84309   0.8328859 0.5753664  LL.3
## 141 90.82115   0.9744433 0.6825317  LL.3
## 11  99.38515   0.7370811 0.8498845  LL.4

The returned object is data-frame-like for compatibility. It also stores the original formula, grouping columns, fitted models, and fit diagnostics.

Inspect Fit Quality

Use fit_quality() to see whether each isolate/model combination had enough observations and dose levels. Use fit_failures() to get failed fits as data, instead of searching warning text.

fit_quality(fit)
##    ID        field model fit_status n_obs n_doses dose_min dose_max
## 1   1      Organic  LL.3         ok    35       7        0        1
## 2   2 Conventional  LL.3         ok    35       7        0        1
## 3   3      Organic  LL.3         ok    35       7        0        1
## 4   4 Conventional  LL.3         ok    35       7        0        1
## 5   5      Organic  LL.3         ok    35       7        0        1
## 6   1      Organic  LL.4         ok    35       7        0        1
## 7   2 Conventional  LL.4         ok    35       7        0        1
## 8   3      Organic  LL.4         ok    35       7        0        1
## 9   4 Conventional  LL.4         ok    35       7        0        1
## 10  5      Organic  LL.4         ok    35       7        0        1
## 11  1      Organic  W2.3         ok    35       7        0        1
## 12  2 Conventional  W2.3         ok    35       7        0        1
## 13  3      Organic  W2.3         ok    35       7        0        1
## 14  4 Conventional  W2.3         ok    35       7        0        1
## 15  5      Organic  W2.3         ok    35       7        0        1
##    response_min response_max message
## 1    0.07631550     20.66667    <NA>
## 2    1.24394259     20.75420    <NA>
## 3    0.06118528     20.78938    <NA>
## 4    1.64277882     20.99232    <NA>
## 5    0.10539854     20.54952    <NA>
## 6    0.07631550     20.66667    <NA>
## 7    1.24394259     20.75420    <NA>
## 8    0.06118528     20.78938    <NA>
## 9    1.64277882     20.99232    <NA>
## 10   0.10539854     20.54952    <NA>
## 11   0.07631550     20.66667    <NA>
## 12   1.24394259     20.75420    <NA>
## 13   0.06118528     20.78938    <NA>
## 14   1.64277882     20.99232    <NA>
## 15   0.10539854     20.54952    <NA>
fit_failures(fit)
## [1] ID      field   model   message
## <0 linhas> (ou row.names de comprimento 0)

Select Models

model_selection() ranks candidate models within each isolate and stratum. Lower information-criterion values are better. The delta column measures the difference from the best-supported model in that group, and weight gives the relative information-criterion weight.

selection <- model_selection(fit)

selection[, c("ID", "field", "model", "IC", "delta", "weight", "rank")]
##    ID        field model        IC      delta      weight rank
## 1   2 Conventional  LL.3  95.06365  0.0000000 0.707505444    1
## 2   2 Conventional  LL.4  96.95829  1.8946342 0.274356466    2
## 3   2 Conventional  W2.3 102.39112  7.3274622 0.018138091    3
## 4   4 Conventional  LL.3  84.84309  0.0000000 0.543149336    1
## 5   4 Conventional  LL.4  85.70322  0.8601343 0.353299860    2
## 6   4 Conventional  W2.3  88.15773  3.3146439 0.103550803    3
## 7   1      Organic  LL.3  98.30158  0.0000000 0.629782044    1
## 8   1      Organic  LL.4  99.38515  1.0835703 0.366349817    2
## 9   1      Organic  W2.3 108.48678 10.1852005 0.003868139    3
## 10  3      Organic  W2.3  76.96774  0.0000000 0.688512844    1
## 11  3      Organic  LL.4  79.71307  2.7453326 0.174490044    2
## 12  3      Organic  LL.3  80.19689  3.2291482 0.136997113    3
## 13  5      Organic  LL.3  90.82115  0.0000000 0.665093865    1
## 14  5      Organic  LL.4  92.72275  1.9015974 0.257013723    2
## 15  5      Organic  W2.3  95.11035  4.2891993 0.077892411    3

Use best_model() when you want one selected model per isolate and stratum.

best <- best_model(fit)

best[, c("ID", "field", "model", "Estimate", "Lower", "Upper", "IC")]
##   ID        field model    Estimate       Lower       Upper       IC
## 1  2 Conventional  LL.3 0.101455765 0.085900787 0.117010744 95.06365
## 2  4 Conventional  LL.3 0.079971237 0.068634503 0.091307971 84.84309
## 3  1      Organic  LL.3 0.006072082 0.004902813 0.007241351 98.30158
## 4  3      Organic  W2.3 0.003233688 0.002949048 0.003518329 76.96774
## 5  5      Organic  LL.3 0.006122508 0.005190599 0.007054418 90.82115

Plot Curves

plot_EC50_curves() reads the fitted object directly. You do not need to repeat the formula, data, isolate column, strata column, or model functions.

plot_EC50_curves(fit, models = "best")

Faceted dose-response plot showing raw observations and the best-supported fitted curve for each isolate and field.

To compare all candidate curves, use the default models = "all". Multiple models are drawn with different line types.

plot_EC50_curves(fit)

Faceted dose-response plot showing raw observations and all candidate fitted curves for each isolate and field.

Report and Reuse Results

Use report_ec50() for a plain data frame that is easy to export to a spreadsheet or use in a manuscript table.

report <- report_ec50(fit, models = "best")

report
##    ID        field    Estimate   Std..Error       Lower       Upper    logLik
## 1   1      Organic 0.006072082 0.0005740341 0.004902813 0.007241351 -45.15079
## 2   2 Conventional 0.101455765 0.0076364691 0.085900787 0.117010744 -43.53183
## 4   4 Conventional 0.079971237 0.0055655891 0.068634503 0.091307971 -38.42154
## 5   5      Organic 0.006122508 0.0004575060 0.005190599 0.007054418 -41.41058
## 13  3      Organic 0.003233688 0.0001397399 0.002949048 0.003518329 -34.48387
##          IC Lack.of.fit   Res.var model
## 1  98.30158   0.7271292 0.8451681  LL.3
## 2  95.06365   0.9866424 0.7704874  LL.3
## 4  84.84309   0.8328859 0.5753664  LL.3
## 5  90.82115   0.9744433 0.6825317  LL.3
## 13 76.96774   0.8355451 0.4594349  W2.3

Use predict_ec50() to predict response at doses chosen by the user.

predict_ec50(
  fit,
  dose = c(0.001, 0.01, 0.1),
  models = "best"
)
##    ID        field model  dose  predicted
## 1   1      Organic  LL.3 0.001 16.6397063
## 2   1      Organic  LL.3 0.010  7.7646682
## 3   1      Organic  LL.3 0.100  1.4846233
## 4   2 Conventional  LL.3 0.001 19.8239563
## 5   2 Conventional  LL.3 0.010 18.2773750
## 6   2 Conventional  LL.3 0.100 10.0752898
## 7   4 Conventional  LL.3 0.001 19.5825722
## 8   4 Conventional  LL.3 0.010 17.4554970
## 9   4 Conventional  LL.3 0.100  8.8966200
## 10  5      Organic  LL.3 0.001 17.4586665
## 11  5      Organic  LL.3 0.010  7.3837621
## 12  5      Organic  LL.3 0.100  0.9303844
## 13  3      Organic  W2.3 0.001 16.5443281
## 14  3      Organic  W2.3 0.010  4.8836421
## 15  3      Organic  W2.3 0.100  0.8690447

Use curve_data() when you want to build your own ggplot2 figure from the stored fitted curves.

curves <- curve_data(fit)

head(curves)
##       field isolate model         dose   growth   .curve_group
## 1   Organic       1  LL.3 1.000000e-05 19.86338 Organic.1.LL.3
## 1.1 Organic       1  LL.3 1.059560e-05 19.86006 Organic.1.LL.3
## 1.2 Organic       1  LL.3 1.122668e-05 19.85657 Organic.1.LL.3
## 1.3 Organic       1  LL.3 1.189534e-05 19.85289 Organic.1.LL.3
## 1.4 Organic       1  LL.3 1.260383e-05 19.84901 Organic.1.LL.3
## 1.5 Organic       1  LL.3 1.335452e-05 19.84493 Organic.1.LL.3
ggplot(curves, aes(dose, growth, color = factor(isolate), linetype = model)) +
  geom_line(linewidth = 1) +
  facet_wrap(~field) +
  scale_x_log10() +
  labs(x = "Dose", y = "Growth", color = "Isolate") +
  theme_light()

Custom ggplot2 dose-response curve figure built from curve_data output.

Diagnose Residuals

Residual plots help identify groups where the fitted curve may not describe the observed responses well.

head(residual_data(fit, models = "best"))
##   ID   field model  dose   observed    fitted   residual
## 1  1 Organic  LL.3 0e+00 20.2082399 19.925747  0.2824926
## 2  1 Organic  LL.3 1e-05 20.1168279 19.863383  0.2534450
## 3  1 Organic  LL.3 1e-04 19.2479678 19.441644 -0.1936758
## 4  1 Organic  LL.3 1e-03 15.8123455 16.639706 -0.8273608
## 5  1 Organic  LL.3 1e-02  7.3206757  7.764668 -0.4439924
## 6  1 Organic  LL.3 1e-01  0.6985264  1.484623 -0.7860970
plot_residuals(fit, models = "best")

Residual diagnostic plot for best-supported EC50 models.