El objetivo básico de los ensayos acelerados de vida útil es realizar estudios a temperaturas elevadas, donde el deterioro sucede a mayor velocidad, para luego predecir el deterioro a temperaturas de almacenamiento menores.
La cinética de la pérdida de la calidad de un alimento se representa de la siguiente forma:
\[ -\frac{dA}{dt} = k A^n \] Donde:
Para \(n = 0\), se dice que la reacción es de orden cero, y se tiene que
\[ -\frac{dA}{dt} = k A^n\Big|_{n = 0} \overset{\text{Integración}}{\Rightarrow} A = A_o - kt \]
Algunos parámetros de calidad cuya pérdida en función del tiempo es orden cero son la degradación enzimática en frutas frescas y vegetales, alimentos congelados y pastas refrigeradas; pardeamiento no enzimático en cereales y en productos lácteos deshidratados; y oxidación de lípidos en alimentos congelados y deshidratados.
Para \(n = 1\), se dice que la reacción es de orden uno, y se tiene que
\[ -\frac{dA}{dt} = k A^n\Big|_{n = 1} \overset{\text{Integración}}{\Rightarrow} A = A_o e^{-kt} \overset{\text{Linealización}}{\Rightarrow}ln(A) = ln(A_o) - kt \] Algunos parámetros de calidad cuya pérdida en función del tiempo es orden uno es la rancidez en aceites o alimentos deshidratados; el crecimiento de microorganismos y sus defectos (aparición de mucílagos y sabores extraños); pérdida de vitaminas en alimentos enlatados y deshidratados; o pérdida en la calidad de proteínas de alimentos deshidratados.
Uno de los aspectos más importantes a recalcar es que cuando hay menos del 50% de conversión de reacción (el deterioro no ha concluído), es difícil distinguir entre orden cero y primer orden (Figura 1), se requiere un mínimo de 6 puntos (tiempos) para determinar el modelo.
Las reacciones que poseen tiempo de demora o incubación (log phase) pueden inducir a un error en el cálculo.
#Gráfico que simula qué sucede en orden cero y orden 1 para parámetros cualesquiera.
# Parámetros
dias <- seq(0, 300, by = 1)
A0 <- 100
k0 <- 0.77
k1 <- 0.01
# Crear dataframe con ambas curvas
datos <- tibble(
dias = dias,
orden0 = pmax(0, A0 - k0 * dias),
orden1 = A0 * exp(-k1 * dias)
) %>%
pivot_longer(cols = c(orden0, orden1), names_to = "orden", values_to = "calidad") %>%
filter(calidad > 0)
# Renombrar para etiquetas más limpias
datos$orden <- recode(datos$orden,
orden0 = "Orden cero",
orden1 = "Primer orden")
# Graficar
ggplot(datos, aes(x = dias, y = calidad, color = orden, linetype = orden)) +
geom_line(size = 0.5) +
geom_hline(yintercept = 50, color = "red", linetype = "dotted",
linewidth = 0.5, alpha = 0.7) +
scale_color_manual(values = c("black", "black")) +
# scale_linetype_manual(values = c("solid", "dashed")) +
labs(
title = "Desplome de A vs tiempo de almacenamiento",
caption = "Figura 1: Se muestra cómo se comporta la curva de cinética de orden 0 y 1 para datos arbitrarios. \n
Notar que por encima de la linea roja (conversión del 50%) \n no tenemos forma de decidir a qué modelo ajustan nuestros datos.",
x = "Almacenamiento (días)",
y = "A (% de calidad)",
color = NULL,
linetype = NULL
) +
theme_minimal(base_size = 14) +
theme(legend.position = "top") +
coord_cartesian(ylim = c(0, 100))
El parámetro \(k\) de la cinética de reacción es una función de la temperatura (\(k = f(T) \sim k(T)\)), en particular, en la gran mayoría de las reacciones de pérdida de calidad alimentaria, el valor de \(k\) puede modelarse según la ecuación de Arrhenius:
\[ k(T) = k_{\text{ref}} \ e ^{-\frac{E_A}{R}(\frac{1}{T} - \frac{1}{T_{\text{ref}}})} \overset{\text{Linealización}}{\Rightarrow} ln(k) = ln(k_{ref}) - \frac{E_A}{R}(\frac{1}{T} - \frac{1}{T_{\text{ref}}}) \]
Donde:
La temperatura de referencia se debe seleccionar en función del intervalo de temperaturas de trabajo. Por ejemplo, si se está realizando un estudio acelerado en el intervalo 15ºC - 40ºC, se puede seleccionar como referencia 27ºC (300ºK).
Para poder inferir sobre las temperaturas más bajas, será necesario aplicar la ecuación de Arrhenius, cuyo parámetro clave es la energía de activación.
En este ejemplo, se trabaja sobre un caso donde existen datos a temperatura ambiente, lo cual no es usual, pero permite entender la metodología de forma inductiva.
## Cargar datos
df <- data.frame(
Temperatura = c(rep(20, 7), rep(35, 7), rep(45, 6)),
Dias = c(122,145,164,183,201,224,245,
11,20,30,39,48,56,61,
7,14,21,28,35,42),
Sabor_oxidado = c(4.0, 8.1, 6.6,16.9,19.3,21.2,28.2,
8.1,13.9,19,23,31.6,33.2,37.2,
2.3,7.5,15.3,24.4,36.9,43.4)
)
Los datos cargados tienen este formato:
Tabla 1: Desarrollo del sabor oxidado de una muestra comercial de mayonesa en función del tiempo, a tres temperaturas de almacenamiento.
| 20°C | 35°C | 45°C | |||
|---|---|---|---|---|---|
| Días | Sabor oxidado¹ | Días | Sabor oxidado¹ | Días | Sabor oxidado¹ |
| 122 | 4,0 | 11 | 8,1 | 7 | 2,3 |
| 145 | 8,1 | 20 | 13,9 | 14 | 7,5 |
| 164 | 6,6 | 30 | 19,0 | 21 | 15,3 |
| 183 | 16,9 | 39 | 23,0 | 28 | 24,4 |
| 201 | 19,3 | 48 | 31,6 | 35 | 36,9 |
| 224 | 21,2 | 56 | 33,2 | 42 | 43,4 |
| 245 | 28,2 | 61 | 37,2 | - | - |
¹ Medido con una escala sensorial en la que 0 = “nada oxidado” y 100 = “muy oxidado”.
Se debe decidir para nuestros datos de estudio si el modelo cinético para la característica deseada es de orden cero o uno, o lo que es lo mismo, decidir cuál de los siguientes pares de datos poseen un mejor ajuste lineal:
Si el comportamiento del Sabor oxidado (A) en función del tiempo posee cinética de orden cero, entonces el modelo \(A = A_o - kt\) posee un buen ajuste. Como k depende de la temperatura, se debe tener en cuenta cada una de las temperaturas de trabajo por separado (Figura 2).
#Gráfico de Sabor oxidado en función de los días de almacenamiento para las tres temperaturas.
ggplot(df, aes(x = Dias, y = Sabor_oxidado, color = as.factor(Temperatura))) +
geom_smooth(method = "lm") +
geom_point() +
geom_label(
x = 100,
y = 30,
label = "A == A[0] - k*t",
parse = TRUE,
inherit.aes = FALSE) +
labs(
title = "Sabor oxidado vs tiempo",
caption = "Figura 2: Comportamiento del sabor oxidado en función del tiempo para las muestras de mayonesa en estudio.\n La recta graficada corresponde al modelo cinético de orden cero.",
x = "Almacenamiento (días)",
y = "Sabor oxidado",
color = "Temperatura (ºC)",
linetype = NULL
) +
theme_minimal(base_size = 14) +
theme(legend.position = "top",
plot.caption = element_text(hjust = 0, size = 9))
# Ajustar modelos lineales de orden cero y extraer coeficientes + R²
r2_data_cero <- df %>%
group_by(Temperatura) %>%
nest() %>%
mutate(
model = map(data, ~ lm(Sabor_oxidado ~ Dias, data = .x)),
coef = map(model, ~ coef(.x)),
A_o = map_dbl(coef, 1),
k = map_dbl(coef, 2),
r2 = map_dbl(model, ~ glance(.x)$r.squared)
) %>%
select(Temperatura, A_o, k, r2)
# Mostrar tabla
kable(r2_data_cero, format = "markdown")
| Temperatura | A_o | k | r2 |
|---|---|---|---|
| 20 | -21.275397 | 0.1972179 | 0.9335263 |
| 35 | 1.868343 | 0.5770626 | 0.9895385 |
| 45 | -8.646667 | 1.2359184 | 0.9862011 |
Para el modelo de orden cero, las tres condiciones poseen un muy buen ajuste, y en la práctica esto puede interpretarse de dos formas: O bien no es necesario complejizar la situación y plantear un modelo cinético de primer orden dado que el orden cero ajusta las condiciones del problema; o bien no tenemos datos suficientes para tomar esta decisión y el modelo puede confundirse con órdenes superiores.
En este punto es también necesario recalcar que el punto de corte del estudio de vida útil lo decide el investigador. Es decir, si un sabor oxidado mayor a 2 finaliza la vida útil, entonces para todas las temperaturas, el modelo de orden cero cubre el 100% del desplome de calidad sensorial.
A modo de ejercicio, estimaremos el modelo orden uno en la siguiente sección.
Si el comportamiento del Sabor oxidado (A) en función del tiempo posee cinética de orden uno, entonces el modelo \(ln(A) = ln(A_o) - kt\) posee un buen ajuste. Como k depende de la temperatura, se debe tener en cuenta cada una de las temperaturas de trabajo por separado (Figura 3).
#Gráfico de Sabor oxidado en función de los días de almacenamiento para las tres temperaturas.
ggplot(df, aes(x = Dias, y = log(Sabor_oxidado), color = as.factor(Temperatura))) +
geom_smooth(method = "lm") +
geom_point() +
geom_label(
x = 100,
y = 3,
label = "ln(A) == ln(A[0]) - k*t",
parse = TRUE,
inherit.aes = FALSE) +
labs(
title = "ln(Sabor oxidado) vs tiempo",
caption = "Figura 3: Comportamiento del sabor oxidado en función del tiempo para las muestras de mayonesa en estudio. \n La recta graficada corresponde al modelo cinético de primer orden.",
x = "Almacenamiento (días)",
y = "ln(Sabor oxidado)",
color = "Temperatura (ºC)",
linetype = NULL
) +
theme_minimal(base_size = 14) +
theme(legend.position = "top",
plot.caption = element_text(hjust = 0, size = 9))
# Ajustar modelos lineales de orden uno y extraer coeficientes + R²
r2_data_uno <- df %>%
group_by(Temperatura) %>%
nest() %>%
mutate(
model = map(data, ~ lm(log(Sabor_oxidado) ~ Dias, data = .x)),
coef = map(model, ~ coef(.x)),
`log(A_o)` = map_dbl(coef, 1),
k = map_dbl(coef, 2),
r2 = map_dbl(model, ~ glance(.x)$r.squared)
) %>%
select(Temperatura, `log(A_o)`, k, r2)
# Mostrar tabla
kable(r2_data_uno, format = "markdown")
| Temperatura | log(A_o) | k | r2 |
|---|---|---|---|
| 20 | -0.3658069 | 0.0156594 | 0.8960661 |
| 35 | 1.9653748 | 0.0287475 | 0.9511734 |
| 45 | 0.6980458 | 0.0813649 | 0.9199158 |
El ajuste del modelo cinético de primer orden presenta un peor ajuste para todas las temperaturas de trabajo, por ende, trabajaremos sobre el orden cero para este caso.
Existen tres formas de estimar la energía de activación, a continuación se realizará de las tres formas posibles a partir de estos datos: Regresión lineal básica, regresión lineal con intervalos y regresión no lineal.
Este método sencillo retoma directamente desde los resultados de la Sección 4.1.1.
El mismo consiste en tomar para cada temperatura de trabajo la pendiente de la regresión lineal para el modelo cinético seleccionado. Recordando que \(A = A_o + kt\), y la ecuación de Arrhenius (Sección 3), se tiene que \(ln(k) = f(\frac{1}{T})\) con \(f\) una función lineal cuya pendiente es \(-\frac{E_A}{R}\). Por lo tanto, calculamos dichas transformaciones y obtenemos la pendiente de la recta que ajusta \(ln(k)\) vs \(\frac{1}{T}\), con T en ºK.
k_method1 <- r2_data_cero %>%
select(Temperatura, k) %>%
mutate(`1/T (1/ºK)` = (273.15+Temperatura)^(-1),
`ln(k)` = log(k))
kable(k_method1, format = "markdown")
| Temperatura | k | 1/T (1/ºK) | ln(k) |
|---|---|---|---|
| 20 | 0.1972179 | 0.0034112 | -1.6234461 |
| 35 | 0.5770626 | 0.0032452 | -0.5498045 |
| 45 | 1.2359184 | 0.0031432 | 0.2118143 |
lm_Ea <- lm(`ln(k)` ~ `1/T (1/ºK)`, data = k_method1)
summary(lm_Ea)
##
## Call:
## lm(formula = `ln(k)` ~ `1/T (1/ºK)`, data = k_method1)
##
## Residuals:
## 1 2 3
## 0.01575 -0.04138 0.02563
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 21.5905 0.8739 24.71 0.0258 *
## `1/T (1/ºK)` -6809.7868 267.3731 -25.47 0.0250 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.05116 on 1 degrees of freedom
## Multiple R-squared: 0.9985, Adjusted R-squared: 0.9969
## F-statistic: 648.7 on 1 and 1 DF, p-value: 0.02498
Es el modelo lineal significativo (p = 0.025) con pendiente (\(-k\)) de -6809.79. Sin embargo, el intervalo de confianza (95%) de este parámetro es:
as.data.frame(confint(lm_Ea, level = 0.95)) %>%
mutate(`Mean value` = rowMeans(across(1:2))) #Mostrar también el promedio del intervalo de confianza.
## 2.5 % 97.5 % Mean value
## (Intercept) 10.48682 32.6942 21.59051
## `1/T (1/ºK)` -10207.08362 -3412.4901 -6809.78684
Por lo tanto, tenemos un valor de \(E_AR^{-1}\) de \((6809.8 \pm 3397.3)ºK^{-1}\). Como se puede ver, el problema de este método de cálculo es su error. Al utilizar este camino estaremos cometiendo un error de casi el 50% el 95% de las veces.
La forma trivial de mejorar el resultado anterior es agregar una mayor cantidad de valores de temperatura al estudio, sin embargo, esto suele ser costoso. Una estrategia matemática que mejora el error es tomar los extremos del intervalo de confianza para la variable k despejada en la Sección 4.1.1 anteriormente calculado como puntos de una misma temperatura y recalcular. Es decir, utilizar los siguientes datos para repetir la lógica de la Sección 4.2.1.
k_method2 <- df %>%
group_by(Temperatura) %>%
nest() %>%
mutate(
modelo = map(data, ~ lm(Sabor_oxidado ~ Dias, data = .x)),
coef = map(modelo, ~ coef(.x)["Dias"]),
conf = map(modelo, ~ confint(.x)["Dias", ])
) %>%
mutate(
k = map_dbl(coef, identity),
k_min = map_dbl(conf, 1),
k_max = map_dbl(conf, 2)
) %>%
select(Temperatura, k, k_min, k_max)
# Mostrar tabla
kable(k_method2, format = "markdown")
| Temperatura | k | k_min | k_max |
|---|---|---|---|
| 20 | 0.1972179 | 0.1367181 | 0.2577177 |
| 35 | 0.5770626 | 0.5088524 | 0.6452728 |
| 45 | 1.2359184 | 1.0329687 | 1.4388680 |
Realizamos las transformaciones para graficar y determinar el modelo lineal de \(ln(k)\) vs \(\frac{1}{T}\).
k_method2long <- k_method2 %>% pivot_longer(-1,
values_to = "k",
names_to = "origen") %>%
mutate(`1/T (1/ºK)` = (273.15+Temperatura)^(-1),
`ln(k)` = log(k))
kable(k_method2long, format = "markdown")
| Temperatura | origen | k | 1/T (1/ºK) | ln(k) |
|---|---|---|---|---|
| 20 | k | 0.1972179 | 0.0034112 | -1.6234461 |
| 20 | k_min | 0.1367181 | 0.0034112 | -1.9898345 |
| 20 | k_max | 0.2577177 | 0.0034112 | -1.3558903 |
| 35 | k | 0.5770626 | 0.0032452 | -0.5498045 |
| 35 | k_min | 0.5088524 | 0.0032452 | -0.6755972 |
| 35 | k_max | 0.6452728 | 0.0032452 | -0.4380821 |
| 45 | k | 1.2359184 | 0.0031432 | 0.2118143 |
| 45 | k_min | 1.0329687 | 0.0031432 | 0.0324369 |
| 45 | k_max | 1.4388680 | 0.0031432 | 0.3638567 |
#Gráfico de Sabor oxidado en función de los días de almacenamiento para las tres temperaturas.
ggplot(k_method2long, aes(x = `1/T (1/ºK)`,
y = `ln(k)`)) +
geom_smooth(method = "lm", width = 0.5,
alpha = 0.2) +
geom_point(aes(color = origen),
size = 2) +
geom_label(
x = 100,
y = 3,
label = "ln(A) == ln(A[0]) - k*t",
parse = TRUE,
inherit.aes = FALSE) +
labs(
title = "ln(k) vs 1/T",
caption = "Figura 4: Ajuste de ln(k) vs 1/T en el método de regresión lineal con intervalos.",
x = "ln(k)",
y = "1/T",
color = NULL,
linetype = NULL
) +
theme_minimal(base_size = 14) +
theme(legend.position = "none",
plot.caption = element_text(hjust = 0, size = 9))
lm_Ea2 <- lm(`ln(k)` ~ `1/T (1/ºK)`, data = k_method2long)
summary(lm_Ea2)
##
## Call:
## lm(formula = `ln(k)` ~ `1/T (1/ºK)`, data = k_method2long)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.32106 -0.15010 0.02928 0.08386 0.31289
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 21.89 2.01 10.89 1.21e-05 ***
## `1/T (1/ºK)` -6906.56 614.93 -11.23 9.90e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2038 on 7 degrees of freedom
## Multiple R-squared: 0.9474, Adjusted R-squared: 0.9399
## F-statistic: 126.1 on 1 and 7 DF, p-value: 9.902e-06
Es el modelo lineal significativo (p = 0.025) con pendiente (\(-k\)) de -6809.79. Sin embargo, el intervalo de confianza (95%) de este parámetro es:
as.data.frame(confint(lm_Ea2, level = 0.95)) %>% mutate(`Mean value` = rowMeans(across(1:2))) #Mostrar el promedio también
## 2.5 % 97.5 % Mean value
## (Intercept) 17.13855 26.64355 21.89105
## `1/T (1/ºK)` -8360.64459 -5452.48429 -6906.56444
Por tanto, en este segundo método, tenemos un valor de \(E_AR^{-1}\) de \((6906.6 \pm 1454.0)ºK^{-1}\). Se tiene un error menor que en el caso anterior.
Este método de cálculo suele ser el más exacto, pero requiere la utilización de un software que permita realizar la correlación no lineal.
El mismo utiliza los resultados de la sección 4.1.1 y la ecuación de Arrhenius explícita (Sección 3).
\[ \begin{array}{rl} \left\{ \begin{array}{l} \text{A}(t) = \text{A}_o + kt \quad \text{(Es orden cero)} \\ k = k_{\text{ref}} \, e^{-\frac{E_A}{R} \left( \frac{1}{T} - \frac{1}{T_{\text{ref}}} \right)} \quad \text{(Arrhenius)} \end{array} \right. & \Rightarrow \quad \text{A}(t) = \text{A}_o + k_{\text{ref}} \, e^{-\frac{E_A}{R} \left( \frac{1}{T} - \frac{1}{T_{\text{ref}}} \right)} t \end{array} \]
En programas estadísticos como R o Genstat, es posible utilizar como input la formula anterior con una serie de valores que lo cumplen. El programa itera sobre los valores y devuelve una salida como en el siguiente ejemplo para determinar las constantes \(E_AR^{-1}\), \(k_{ref}\) y \(A_o\).
#Convertir datos de temperatura a kelvin
df$Temp_K <- df$Temperatura + 273.15
#Fijar Tref en Kelvin
Tref <- 300
#Ajuste no lineal
nls_fit <- nls(
Sabor_oxidado ~ A_o + k_ref * exp(-EA_R * (1 / Temp_K - 1 / Tref)) * Dias,
data = df,
start = list(A_o = -10, k_ref = 0.56, EA_R = 6800) # valores iniciales razonables
)
#ver resultados:
summary(nls_fit)
##
## Formula: Sabor_oxidado ~ A_o + k_ref * exp(-EA_R * (1/Temp_K - 1/Tref)) *
## Dias
##
## Parameters:
## Estimate Std. Error t value Pr(>|t|)
## A_o -1.19882 4.03246 -0.297 0.769844
## k_ref 0.21240 0.04321 4.915 0.000131 ***
## EA_R 8735.22606 658.10872 13.273 2.12e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 6.883 on 17 degrees of freedom
##
## Number of iterations to convergence: 7
## Achieved convergence tolerance: 8.689e-06
Los intervalos de confianza de los parámetros determinados son:
as.data.frame(confint(nls_fit, level = 0.95)) %>% mutate(`Mean value` = rowMeans(across(1:2)))
## 2.5% 97.5% Mean value
## A_o -9.6042956 7.167514e+00 -1.218391
## k_ref 0.1236815 3.006964e-01 0.212189
## EA_R 7586.4135818 1.041201e+04 8999.209295
Por tanto, en este último método tenemos un valor de \(E_AR^{-1}\) de \((8999.2 \pm 1412.8)\). Si bien es un valor distinto a los anteriores, es el más riguroso desde un punto de vista estadístico/matemático y el que presenta un menor error de determinación.
La siguiente tabla resume los cálculos obtenidos por los tres métodos propuestos para estimar la \(E_AR^{-1}\). En la misma se observa que el método con el que se obtiene la mejor estimación es el que parte de la regresión no lineal.
| Método | \(E_AR^{-1}\) (1/°K) | Intervalo de confianza (±) |
|---|---|---|
| Lineal (3 puntos) | 6809.8 | ±3397.3 |
| Lineal con intervalos de confianza (9 puntos) | 6906.6 | ±1454.0 |
| No lineal | 8999.2 | ±1412.8 |