The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

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.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.