The dynamic modelling helpers in predmicror are designed
for prediction and fitting when environmental conditions change over
time. This is useful, for example, when temperature varies during
storage, transport, processing, or abuse scenarios.
The basic workflow is:
dynamic_profile();predict_dynamic_growth() or
predict_dynamic_inactivation();fit_dynamic_growth() or
fit_dynamic_inactivation();A dynamic profile stores time-varying environmental conditions. At present, the most common use case is temperature as a function of time.
profile <- dynamic_profile(
time = c(0, 5, 10, 15, 20, 25, 30),
temperature = c(10, 4, 2, 15, 15, 2, 10)
)
profile
#> Dynamic environmental profile
#> points: 7
#> time range: [0, 30]
#> variables: temperature
#> time temperature
#> 1 0 10
#> 2 5 4
#> 3 10 2
#> 4 15 15
#> 5 20 15
#> 6 25 2
#> 7 30 10The Huang dynamic growth model can be combined with a secondary square-root model for the effect of temperature on the maximum specific growth rate.
growth_pred <- predict_dynamic_growth(
profile = profile,
model = "huang",
secondary = "huang_sqrt",
start = list(
logN0 = 2,
logNmax = 8.8,
a = 0.0886,
Tmin = 8.91,
lag = 2
),
times = seq(0, 30, by = 1)
)
head(growth_pred)
#> dynamic_growth prediction
#> model: huang
#> secondary: huang_sqrt
#> rows: 6
#> time response lnN logN temperature
#> 1 0 2.000000 4.605170 2.000000 10.0
#> 2 1 2.000002 4.605175 2.000002 8.8
#> 3 2 2.000002 4.605175 2.000002 7.6
#> 4 3 2.000002 4.605175 2.000002 6.4
#> 5 4 2.000002 4.605175 2.000002 5.2
#> 6 5 2.000002 4.605175 2.000002 4.0
tail(growth_pred)
#> dynamic_growth prediction
#> model: huang
#> secondary: huang_sqrt
#> rows: 6
#> time response lnN logN temperature
#> 26 25 2.352192 5.416123 2.352192 2.0
#> 27 26 2.352192 5.416123 2.352192 3.6
#> 28 27 2.352192 5.416123 2.352192 5.2
#> 29 28 2.352192 5.416123 2.352192 6.8
#> 30 29 2.352192 5.416123 2.352192 8.4
#> 31 30 2.353249 5.418557 2.353249 10.0plot(growth_pred$time, growth_pred$response,
type = "l",
lwd = 2,
xlab = "Time",
ylab = "Predicted log10 count"
)Dynamic fitting estimates selected parameters by repeatedly solving the dynamic model and minimizing residual error at the observation times. In practice, it is often wise to estimate only a small number of parameters and keep others fixed, especially when the data set is small.
Here we simulate observations from the dynamic growth prediction and
estimate the secondary-model parameter a.
obs_growth <- data.frame(
time = growth_pred$time[seq(1, nrow(growth_pred), by = 3)],
logN = growth_pred$response[seq(1, nrow(growth_pred), by = 3)]
)
growth_fit <- fit_dynamic_growth(
data = obs_growth,
profile = profile,
time = "time",
response = "logN",
model = "huang",
secondary = "huang_sqrt",
start = list(
logN0 = 2,
logNmax = 8.8,
a = 0.07,
Tmin = 8.91,
lag = 2
),
estimate = "a",
fixed = list(
logN0 = 2,
logNmax = 8.8,
Tmin = 8.91,
lag = 2
)
)
growth_fit
#> predmicror dynamic fit
#> type: dynamic_growth
#> model: huang
#> secondary: huang_sqrt
#> response: logN (log10)
#> SSE: 0.000000000012959
#> convergence: 0
coef(growth_fit)
#> logN0 logNmax a Tmin lag
#> 2.00000000 8.80000000 0.08859978 8.91000000 2.00000000
fit_metrics(growth_fit)
#> model type response response_scale n p SSE RMSE
#> 1 huang dynamic_growth logN log10 11 1 1.295925e-11 1.085409e-06
#> MAE bias RSE R2 adj_R2 logLik AIC BIC
#> 1 7.395177e-07 7.395177e-07 1.138387e-06 1 1 135.4608 -268.9215 -268.5236
#> converged
#> 1 TRUEplot(obs_growth$time, obs_growth$logN,
pch = 16,
xlab = "Time",
ylab = "log10 count"
)
lines(growth_fit$prediction$time, growth_fit$prediction$response, lwd = 2)The fitted object supports familiar helper methods:
head(fitted(growth_fit))
#> [1] 2.000000 2.000002 2.000002 2.000002 2.000002 2.048006
head(residuals(growth_fit))
#> [1] 0.000000e+00 9.289902e-12 9.289902e-12 9.289902e-12 9.289902e-12
#> [6] 2.370187e-07
head(predmicror_augment(growth_fit))
#> time logN .fitted .resid .model .type
#> 1 0 2.000000 2.000000 0.000000e+00 huang dynamic_growth
#> 2 3 2.000002 2.000002 9.289902e-12 huang dynamic_growth
#> 3 6 2.000002 2.000002 9.289902e-12 huang dynamic_growth
#> 4 9 2.000002 2.000002 9.289902e-12 huang dynamic_growth
#> 5 12 2.000002 2.000002 9.289902e-12 huang dynamic_growth
#> 6 15 2.048006 2.048006 2.370187e-07 huang dynamic_growthDynamic inactivation can be simulated with the Weibull-Peleg formulation. In the simplest case, the rate is kept constant.
inact_profile <- dynamic_profile(
time = c(0, 10),
temperature = c(60, 60)
)
inact_pred <- predict_dynamic_inactivation(
profile = inact_profile,
model = "weibull_peleg",
secondary = "constant",
start = list(
logN0 = 7,
b = 0.2,
n = 1
),
times = seq(0, 10, by = 1)
)
head(inact_pred)
#> dynamic_inactivation prediction
#> model: weibull_peleg
#> secondary: constant
#> rows: 6
#> time response logN log_survival temperature
#> 1 0 7.0 7.0 0.0 60
#> 2 1 6.8 6.8 -0.2 60
#> 3 2 6.6 6.6 -0.4 60
#> 4 3 6.4 6.4 -0.6 60
#> 5 4 6.2 6.2 -0.8 60
#> 6 5 6.0 6.0 -1.0 60
tail(inact_pred)
#> dynamic_inactivation prediction
#> model: weibull_peleg
#> secondary: constant
#> rows: 6
#> time response logN log_survival temperature
#> 6 5 6.0 6.0 -1.0 60
#> 7 6 5.8 5.8 -1.2 60
#> 8 7 5.6 5.6 -1.4 60
#> 9 8 5.4 5.4 -1.6 60
#> 10 9 5.2 5.2 -1.8 60
#> 11 10 5.0 5.0 -2.0 60plot(inact_pred$time, inact_pred$response,
type = "l",
lwd = 2,
xlab = "Time",
ylab = "Predicted log10 count"
)The same framework can fit selected inactivation parameters. Here we
estimate the Weibull-Peleg rate parameter b.
obs_inact <- data.frame(
time = c(0, 5, 10),
logN = c(7, 6, 5)
)
inact_fit <- fit_dynamic_inactivation(
data = obs_inact,
profile = inact_profile,
time = "time",
response = "logN",
model = "weibull_peleg",
secondary = "constant",
start = list(
logN0 = 7,
b = 0.1,
n = 1
),
estimate = "b",
fixed = list(
logN0 = 7,
n = 1
)
)
inact_fit
#> predmicror dynamic fit
#> type: dynamic_inactivation
#> model: weibull_peleg
#> secondary: constant
#> response: logN (log10)
#> SSE: 0.000000000000000000000000051711
#> convergence: 0
coef(inact_fit)
#> logN0 b n
#> 7.0 0.2 1.0
fit_metrics(inact_fit)
#> model type response response_scale n p SSE
#> 1 weibull_peleg dynamic_inactivation logN log10 3 1 5.171141e-26
#> RMSE MAE bias RSE R2 adj_R2 logLik
#> 1 1.312903e-13 1.030287e-13 -1.030287e-13 1.607971e-13 1 1 84.72728
#> AIC BIC converged
#> 1 -167.4546 -168.356 TRUEplot(obs_inact$time, obs_inact$logN,
pch = 16,
xlab = "Time",
ylab = "log10 count"
)
lines(inact_fit$prediction$time, inact_fit$prediction$response, lwd = 2)Finite-difference sensitivity analysis helps identify which parameters most influence the model output. The scaled sensitivity coefficients are useful because they are expressed on the response scale.
sens <- dynamic_sensitivity(
type = "growth",
profile = profile,
model = "huang",
secondary = "huang_sqrt",
start = list(
logN0 = 2,
logNmax = 8.8,
a = 0.0886,
Tmin = 8.91,
lag = 2
),
parameters = c("a", "Tmin"),
times = seq(0, 30, by = 2)
)
head(sens)
#> time parameter prediction sensitivity scaled_sensitivity
#> 1 0 a 2.000000 0.000000e+00 0.000000e+00
#> 2 2 a 2.000002 4.249422e-05 3.764988e-06
#> 3 4 a 2.000002 4.249422e-05 3.764988e-06
#> 4 6 a 2.000002 4.249422e-05 3.764988e-06
#> 5 8 a 2.000002 4.249422e-05 3.764988e-06
#> 6 10 a 2.000002 4.249422e-05 3.764988e-06plot(sens$time, sens$scaled_sensitivity,
type = "n",
xlab = "Time",
ylab = "Scaled sensitivity"
)
for (p in unique(sens$parameter)) {
z <- sens[sens$parameter == p, ]
lines(z$time, z$scaled_sensitivity, lwd = 2)
}
legend("topleft", legend = unique(sens$parameter), lty = 1, lwd = 2, bty = "n")Dynamic fitting can be computationally more demanding than fitting
explicit isothermal models because each objective-function evaluation
requires solving the dynamic trajectory. For small data sets, avoid
estimating too many parameters at once. Use fixed to keep
parameters at known or literature-supported values and estimate only the
parameters that are informed by the available observations.