Three-parameter logistic model fitted to plant height as a function of accumulated growing degree-days (GDD), with ANOVA on model parameters and all derivative-based critical points.
Overview
The same logistic framework applied to leaf area is used here for plant height (stem length, cm). Parameters β₁–β₃ are estimated independently for each season × cultivar × block combination and subjected to two-factor ANOVA in randomised complete blocks (FAT2DBC). Critical points derived from the first and second derivatives are also computed at the block level and analysed by ANOVA.
\[
y = \frac{\beta_1}{1 + e^{\;\beta_3(\beta_2 - x)}}
\]
Parameter
Interpretation
β₁
Upper asymptote (maximum plant height, cm)
β₂
Inflection point (GDD at maximum elongation rate)
β₃
Steepness coefficient (elongation rate)
Required packages
Show code
library(rio) # Data importlibrary(tidyverse) # Data wrangling and visualisationlibrary(metan) # mean_by(), doo()library(broom) # tidy() – NLS summary as data framelibrary(AgroR) # FAT2DBC() – two-factor ANOVA in RCBDlibrary(patchwork) # Combine ggplot panelslibrary(hydroGOF) # gof() – goodness-of-fit metricslibrary(gt) # Professional tables
Load data
Show code
# Import the pre-computed plot-mean dataset.# Each row = one season × cultivar × block × date mean (already averaged across plants).# This dataset is used both for model fitting and for the ANOVA on critical points.df_model <-import("../df_model_cresc_medias.xlsx")
Fit logistic models – plant height
Show code
# Response variable: 'cp' (comprimento de planta = plant height, cm)formula_ap <- cp_mean ~ b1 / (1+exp((b3 * (b2 - gda))))# Starting values:# b1 = 100 → expected maximum height (~100 cm)# b2 = 400 → inflection point around 400 GDD# b3 = 0.01 → moderate steepnessstart_ap <-list(b1 =100, b2 =400, b3 =0.01)# Fit one model per season × cultivar × block combination.# The date "04/11" falls outside the vegetative growth window (post-flowering)# and is excluded to preserve the sigmoid shape of the curve.mod_ap <- df_model |>filter(data !="04/11") |>group_by(epoca, cultivar, bloco) |>doo(~nls(formula_ap, data = ., start = start_ap))
Goodness of fit
Show code
# Helper function: extract R² and AIC from a single NLS model objectget_gof <-function(model) { aic <-AIC(model) bic <-BIC(model) fit <- model$m$fitted() res <- model$m$resid() obs <- fit + res g <- hydroGOF::gof(sim = fit, obs = obs, digits =4) r2 <- g["R2", ] rmse <- g["RMSE", ] mae <- g["MAE", ] d <- g["d", ]data.frame(aic = aic, bic = bic, r2 = r2, rmse = rmse, mae = mae, d = d)}# Apply to all modelsgof_ap <- mod_ap |>mutate(map_dfr(.x = data, .f =~get_gof(.))) |>select(-data) |>mutate(variable ="plant_height")# Display goodness of fit table with gtgof_ap |>gt() |>tab_header(title ="Supplementary Table S4",subtitle ="Goodness-of-fit metrics for Plant Height logistic models (per block)" ) |>fmt_number(columns =where(is.numeric), decimals =4)
Supplementary Table S4
Goodness-of-fit metrics for Plant Height logistic models (per block)
epoca
bloco
cultivar
aic
bic
r2
rmse
mae
d
variable
E1
B1
D
66.3935
68.3331
0.9785
2.7569
2.3200
0.9946
plant_height
E1
B2
D
61.7407
63.6803
0.9889
2.2711
1.7191
0.9972
plant_height
E1
B3
D
72.5145
74.4541
0.9684
3.5579
2.8977
0.9921
plant_height
E1
B1
M
76.0310
77.9706
0.9713
4.1193
3.3420
0.9928
plant_height
E1
B2
M
83.5905
85.5302
0.9499
5.6444
4.5812
0.9876
plant_height
E1
B3
M
81.8164
83.7560
0.9551
5.2422
3.9575
0.9888
plant_height
E2
B1
D
60.7096
61.9200
0.9675
3.3755
2.5272
0.9918
plant_height
E2
B2
D
48.5308
49.7412
0.9880
1.8360
1.5194
0.9970
plant_height
E2
B3
D
74.0980
75.3084
0.9032
6.5927
5.4324
0.9757
plant_height
E2
B1
M
68.6819
69.8922
0.9411
5.0287
3.2778
0.9848
plant_height
E2
B2
M
59.2642
60.4745
0.9600
3.1401
2.7115
0.9900
plant_height
E2
B3
M
63.7990
65.0093
0.9472
3.9393
2.3528
0.9863
plant_height
Show code
saveRDS(gof_ap, "../results/gof_ap.rds")
Extract estimated parameters and compute critical points
Show code
# broom::tidy() converts each NLS object into a tidy data frame;# pivot_wider() places b1, b2, b3 as separate columns.parameters_ap <- mod_ap |>mutate(data =map(data, ~.x |>tidy())) |>unnest(data) |>select(epoca, cultivar, bloco, term, estimate) |>pivot_wider(names_from = term,values_from = estimate)
Logistic curves fitted to plant height (cm) as a function of accumulated GDD, faceted by sowing season. Points = treatment means. The semi-transparent point (04/11) was excluded from fitting.
First (left) and second (right) derivatives of the fitted logistic curves for plant height. Points mark the inflection (●), maximum acceleration (▲), and maximum deceleration (▽) thresholds per cultivar × season combination.