paquetes <- c("ggplot2", "dplyr", "tidyr", "readr", "knitr")
faltantes <- paquetes[
!paquetes %in% rownames(installed.packages())
]
if (length(faltantes) > 0) {
install.packages(
faltantes,
dependencies = TRUE,
repos = "https://cloud.r-project.org"
)
}
library(ggplot2)
library(dplyr)
library(tidyr)
library(readr)
library(knitr)
set.seed(2026)
theme_set(theme_minimal(base_size = 12))
Se comparan cuatro modelos para convertir el voltaje de un termistor en temperatura:
Las métricas principales son:
\[ MSE = \frac{1}{n}\sum_{i=1}^{n}(T_i-\widehat{T}_i)^2 \]
\[ RMSE = \sqrt{MSE} \]
\[ MAE = \frac{1}{n}\sum_{i=1}^{n}|T_i-\widehat{T}_i| \]
usar_datos_simulados <- TRUE
if (usar_datos_simulados) {
temperatura_C <- seq(-5, 120, by = 5)
voltaje_ideal <- 1.96 +
(4.15 - 1.96) *
(1 - exp(-(temperatura_C + 15) / 62)) /
(1 - exp(-(120 + 15) / 62))
datos <- data.frame(
voltaje_V = voltaje_ideal +
rnorm(length(temperatura_C), mean = 0, sd = 0.012),
temperatura_C = temperatura_C
)
} else {
if (!file.exists("datos_termistor.csv")) {
stop(
"No se encontró datos_termistor.csv en la misma carpeta del archivo Rmd."
)
}
datos <- read_csv(
"datos_termistor.csv",
show_col_types = FALSE
)
}
columnas_requeridas <- c("voltaje_V", "temperatura_C")
if (!all(columnas_requeridas %in% names(datos))) {
stop(
"El archivo debe contener las columnas voltaje_V y temperatura_C."
)
}
datos <- datos |>
select(voltaje_V, temperatura_C) |>
filter(complete.cases(voltaje_V, temperatura_C)) |>
arrange(voltaje_V)
if (nrow(datos) < 6) {
stop("Se requieren al menos seis observaciones.")
}
if (anyDuplicated(datos$voltaje_V) > 0) {
stop("Hay valores duplicados de voltaje.")
}
kable(
datos,
digits = 4,
caption = "Datos de calibración"
)
| voltaje_V | temperatura_C |
|---|---|
| 2.3342 | -5 |
| 2.4778 | 0 |
| 2.6427 | 5 |
| 2.7786 | 10 |
| 2.8995 | 15 |
| 2.9952 | 20 |
| 3.1254 | 25 |
| 3.2224 | 30 |
| 3.3286 | 35 |
| 3.4070 | 40 |
| 3.4866 | 45 |
| 3.5554 | 50 |
| 3.6286 | 55 |
| 3.6905 | 60 |
| 3.7197 | 65 |
| 3.8191 | 70 |
| 3.8589 | 75 |
| 3.8989 | 80 |
| 3.9280 | 85 |
| 3.9841 | 90 |
| 4.0070 | 95 |
| 4.0415 | 100 |
| 4.0567 | 105 |
| 4.1186 | 110 |
| 4.1271 | 115 |
| 4.1729 | 120 |
grafica_datos <- ggplot(
datos,
aes(x = voltaje_V, y = temperatura_C)
) +
geom_point(size = 3) +
labs(
title = "Datos de calibración del termistor",
x = "Voltaje del sensor (V)",
y = "Temperatura patrón (°C)",
caption = ifelse(
usar_datos_simulados,
"Datos simulados únicamente para verificar el algoritmo.",
"Datos experimentales del estudio."
)
)
print(grafica_datos)
\[ T(V)=\beta_0+\beta_1V \]
modelo_lineal <- lm(
temperatura_C ~ voltaje_V,
data = datos
)
summary(modelo_lineal)
##
## Call:
## lm(formula = temperatura_C ~ voltaje_V, data = datos)
##
## Residuals:
## Min 1Q Median 3Q Max
## -10.804 -8.176 -1.910 6.599 17.877
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -179.513 12.609 -14.24 3.37e-13 ***
## voltaje_V 67.492 3.549 19.02 5.61e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 9.738 on 24 degrees of freedom
## Multiple R-squared: 0.9378, Adjusted R-squared: 0.9352
## F-statistic: 361.6 on 1 and 24 DF, p-value: 5.611e-16
\[ T(V)=\beta_0+\beta_1V+\beta_2V^2 \]
modelo_cuadratico <- lm(
temperatura_C ~ poly(voltaje_V, 2, raw = TRUE),
data = datos
)
summary(modelo_cuadratico)
##
## Call:
## lm(formula = temperatura_C ~ poly(voltaje_V, 2, raw = TRUE),
## data = datos)
##
## Residuals:
## Min 1Q Median 3Q Max
## -5.4709 -2.3941 -0.1686 2.5769 5.4944
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 180.069 26.777 6.725 7.36e-07 ***
## poly(voltaje_V, 2, raw = TRUE)1 -154.989 16.398 -9.451 2.19e-09 ***
## poly(voltaje_V, 2, raw = TRUE)2 33.411 2.456 13.604 1.74e-12 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 3.307 on 23 degrees of freedom
## Multiple R-squared: 0.9931, Adjusted R-squared: 0.9925
## F-statistic: 1660 on 2 and 23 DF, p-value: < 2.2e-16
\[ T(V)=\beta_0+\beta_1V+\beta_2V^2+\beta_3V^3 \]
modelo_cubico <- lm(
temperatura_C ~ poly(voltaje_V, 3, raw = TRUE),
data = datos
)
summary(modelo_cubico)
##
## Call:
## lm(formula = temperatura_C ~ poly(voltaje_V, 3, raw = TRUE),
## data = datos)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.76855 -1.08855 0.08359 0.85429 2.90568
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -553.445 79.967 -6.921 5.99e-07 ***
## poly(voltaje_V, 3, raw = TRUE)1 538.986 75.131 7.174 3.43e-07 ***
## poly(voltaje_V, 3, raw = TRUE)2 -181.230 23.147 -7.829 8.43e-08 ***
## poly(voltaje_V, 3, raw = TRUE)3 21.742 2.342 9.284 4.58e-09 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.525 on 22 degrees of freedom
## Multiple R-squared: 0.9986, Adjusted R-squared: 0.9984
## F-statistic: 5235 on 3 and 22 DF, p-value: < 2.2e-16
modelo_spline <- splinefun(
x = datos$voltaje_V,
y = datos$temperatura_C,
method = "natural"
)
calcular_metricas <- function(real, estimado) {
error <- real - estimado
mse <- mean(error^2, na.rm = TRUE)
data.frame(
MSE = mse,
RMSE = sqrt(mse),
MAE = mean(abs(error), na.rm = TRUE),
Error_maximo_absoluto = max(abs(error), na.rm = TRUE),
R2_predictivo = 1 -
sum(error^2, na.rm = TRUE) /
sum((real - mean(real, na.rm = TRUE))^2, na.rm = TRUE)
)
}
predecir_loocv <- function(datos, metodo) {
n <- nrow(datos)
predicciones <- rep(NA_real_, n)
for (i in seq_len(n)) {
entrenamiento <- datos[-i, , drop = FALSE]
prueba <- datos[i, , drop = FALSE]
if (metodo == "lineal") {
modelo <- lm(
temperatura_C ~ voltaje_V,
data = entrenamiento
)
predicciones[i] <- predict(
modelo,
newdata = prueba
)
} else if (metodo == "cuadratico") {
modelo <- lm(
temperatura_C ~ poly(voltaje_V, 2, raw = TRUE),
data = entrenamiento
)
predicciones[i] <- predict(
modelo,
newdata = prueba
)
} else if (metodo == "cubico") {
modelo <- lm(
temperatura_C ~ poly(voltaje_V, 3, raw = TRUE),
data = entrenamiento
)
predicciones[i] <- predict(
modelo,
newdata = prueba
)
} else if (metodo == "spline") {
funcion_spline <- splinefun(
entrenamiento$voltaje_V,
entrenamiento$temperatura_C,
method = "natural"
)
predicciones[i] <- funcion_spline(
prueba$voltaje_V
)
} else {
stop("Método no reconocido.")
}
}
predicciones
}
pred_lineal <- predecir_loocv(datos, "lineal")
pred_cuadratica <- predecir_loocv(datos, "cuadratico")
pred_cubica <- predecir_loocv(datos, "cubico")
pred_spline <- predecir_loocv(datos, "spline")
tabla_predicciones <- datos |>
mutate(
Prediccion_lineal_C = pred_lineal,
Prediccion_cuadratica_C = pred_cuadratica,
Prediccion_cubica_C = pred_cubica,
Prediccion_spline_C = pred_spline
)
kable(
tabla_predicciones,
digits = 4,
caption = "Predicciones mediante LOOCV"
)
| voltaje_V | temperatura_C | Prediccion_lineal_C | Prediccion_cuadratica_C | Prediccion_cubica_C | Prediccion_spline_C |
|---|---|---|---|---|---|
| 2.3342 | -5 | -26.8400 | 5.0410 | -9.0196 | -4.0863 |
| 2.4778 | 0 | -14.9853 | 1.5977 | 0.2000 | -0.5211 |
| 2.6427 | 5 | -2.1450 | 3.6102 | 6.8765 | 5.3980 |
| 2.7786 | 10 | 7.7755 | 7.0485 | 11.7252 | 9.5788 |
| 2.8995 | 15 | 16.2925 | 11.1952 | 15.8813 | 15.7168 |
| 2.9952 | 20 | 22.8525 | 15.1076 | 19.1542 | 18.9873 |
| 3.1254 | 25 | 31.8263 | 21.6990 | 24.5331 | 26.0009 |
| 3.2224 | 30 | 38.3889 | 27.2934 | 28.9017 | 29.1575 |
| 3.3286 | 35 | 45.5962 | 34.2866 | 34.4537 | 35.6534 |
| 3.4070 | 40 | 50.8656 | 39.8339 | 38.9629 | 39.5685 |
| 3.4866 | 45 | 56.2375 | 45.9279 | 44.1188 | 45.2430 |
| 3.5554 | 50 | 60.8715 | 51.4929 | 49.0228 | 50.1307 |
| 3.6286 | 55 | 65.8259 | 57.7943 | 54.8657 | 53.4583 |
| 3.6905 | 60 | 69.9891 | 63.3457 | 60.2269 | 62.2073 |
| 3.7197 | 65 | 71.8376 | 65.8950 | 62.6290 | 62.0146 |
| 3.8191 | 70 | 78.6863 | 75.7882 | 73.0465 | 72.6281 |
| 3.8589 | 75 | 81.2720 | 79.7788 | 77.2944 | 73.6292 |
| 3.8989 | 80 | 83.8572 | 83.9138 | 81.8243 | 81.5299 |
| 3.9280 | 85 | 85.6335 | 86.9008 | 85.1676 | 82.5092 |
| 3.9841 | 90 | 89.3337 | 93.1491 | 92.3988 | 92.9342 |
| 4.0070 | 95 | 90.6176 | 95.5305 | 95.2694 | 92.2131 |
| 4.0415 | 100 | 92.6997 | 99.3478 | 99.9477 | 103.4023 |
| 4.0567 | 105 | 93.3738 | 100.7360 | 101.7511 | 100.0848 |
| 4.1186 | 110 | 97.3535 | 108.2438 | 111.4675 | 114.3549 |
| 4.1271 | 115 | 97.4749 | 108.5889 | 111.9345 | 110.8013 |
| 4.1729 | 120 | 100.2134 | 114.0393 | 119.6608 | 144.1469 |
metricas_lineal <- calcular_metricas(
datos$temperatura_C,
pred_lineal
) |>
mutate(Modelo = "Regresión lineal")
metricas_cuadratica <- calcular_metricas(
datos$temperatura_C,
pred_cuadratica
) |>
mutate(Modelo = "Regresión cuadrática")
metricas_cubica <- calcular_metricas(
datos$temperatura_C,
pred_cubica
) |>
mutate(Modelo = "Regresión cúbica")
metricas_spline <- calcular_metricas(
datos$temperatura_C,
pred_spline
) |>
mutate(Modelo = "Spline cúbico natural")
resultados_metricas <- bind_rows(
metricas_lineal,
metricas_cuadratica,
metricas_cubica,
metricas_spline
) |>
select(
Modelo,
MSE,
RMSE,
MAE,
Error_maximo_absoluto,
R2_predictivo
) |>
arrange(RMSE)
kable(
resultados_metricas,
digits = 4,
caption = "Comparación de modelos"
)
| Modelo | MSE | RMSE | MAE | Error_maximo_absoluto | R2_predictivo |
|---|---|---|---|---|---|
| Regresión cúbica | 3.0908 | 1.7581 | 1.3639 | 4.0196 | 0.9978 |
| Regresión cuadrática | 14.5335 | 3.8123 | 3.0817 | 10.0410 | 0.9897 |
| Spline cúbico natural | 27.3328 | 5.2281 | 2.6453 | 24.1469 | 0.9806 |
| Regresión lineal | 108.4472 | 10.4138 | 8.8523 | 21.8400 | 0.9229 |
mejor_modelo <- resultados_metricas |>
slice_min(
order_by = RMSE,
n = 1,
with_ties = FALSE
)
kable(
mejor_modelo,
digits = 4,
caption = "Modelo con menor RMSE"
)
| Modelo | MSE | RMSE | MAE | Error_maximo_absoluto | R2_predictivo |
|---|---|---|---|---|---|
| Regresión cúbica | 3.0908 | 1.7581 | 1.3639 | 4.0196 | 0.9978 |
El modelo con menor RMSE fue Regresión cúbica, con un RMSE de 1.7581 °C.
malla <- data.frame(
voltaje_V = seq(
min(datos$voltaje_V),
max(datos$voltaje_V),
length.out = 500
)
)
malla$Regresion_lineal <- predict(
modelo_lineal,
newdata = malla
)
malla$Regresion_cuadratica <- predict(
modelo_cuadratico,
newdata = malla
)
malla$Regresion_cubica <- predict(
modelo_cubico,
newdata = malla
)
malla$Spline_cubico_natural <- modelo_spline(
malla$voltaje_V
)
curvas_largas <- malla |>
pivot_longer(
cols = -voltaje_V,
names_to = "Modelo",
values_to = "Temperatura_estimada_C"
) |>
mutate(
Modelo = recode(
Modelo,
Regresion_lineal = "Regresión lineal",
Regresion_cuadratica = "Regresión cuadrática",
Regresion_cubica = "Regresión cúbica",
Spline_cubico_natural = "Spline cúbico natural"
)
)
grafica_modelos <- ggplot() +
geom_point(
data = datos,
aes(x = voltaje_V, y = temperatura_C),
size = 3
) +
geom_line(
data = curvas_largas,
aes(
x = voltaje_V,
y = Temperatura_estimada_C,
linetype = Modelo
),
linewidth = 0.9
) +
labs(
title = "Comparación de modelos de calibración",
x = "Voltaje del sensor (V)",
y = "Temperatura estimada (°C)",
linetype = "Modelo"
) +
theme(legend.position = "bottom")
print(grafica_modelos)
tabla_errores <- tabla_predicciones |>
mutate(
Error_lineal_C =
temperatura_C - Prediccion_lineal_C,
Error_cuadratico_C =
temperatura_C - Prediccion_cuadratica_C,
Error_cubico_C =
temperatura_C - Prediccion_cubica_C,
Error_spline_C =
temperatura_C - Prediccion_spline_C
)
kable(
tabla_errores,
digits = 4,
caption = "Errores de predicción"
)
| voltaje_V | temperatura_C | Prediccion_lineal_C | Prediccion_cuadratica_C | Prediccion_cubica_C | Prediccion_spline_C | Error_lineal_C | Error_cuadratico_C | Error_cubico_C | Error_spline_C |
|---|---|---|---|---|---|---|---|---|---|
| 2.3342 | -5 | -26.8400 | 5.0410 | -9.0196 | -4.0863 | 21.8400 | -10.0410 | 4.0196 | -0.9137 |
| 2.4778 | 0 | -14.9853 | 1.5977 | 0.2000 | -0.5211 | 14.9853 | -1.5977 | -0.2000 | 0.5211 |
| 2.6427 | 5 | -2.1450 | 3.6102 | 6.8765 | 5.3980 | 7.1450 | 1.3898 | -1.8765 | -0.3980 |
| 2.7786 | 10 | 7.7755 | 7.0485 | 11.7252 | 9.5788 | 2.2245 | 2.9515 | -1.7252 | 0.4212 |
| 2.8995 | 15 | 16.2925 | 11.1952 | 15.8813 | 15.7168 | -1.2925 | 3.8048 | -0.8813 | -0.7168 |
| 2.9952 | 20 | 22.8525 | 15.1076 | 19.1542 | 18.9873 | -2.8525 | 4.8924 | 0.8458 | 1.0127 |
| 3.1254 | 25 | 31.8263 | 21.6990 | 24.5331 | 26.0009 | -6.8263 | 3.3010 | 0.4669 | -1.0009 |
| 3.2224 | 30 | 38.3889 | 27.2934 | 28.9017 | 29.1575 | -8.3889 | 2.7066 | 1.0983 | 0.8425 |
| 3.3286 | 35 | 45.5962 | 34.2866 | 34.4537 | 35.6534 | -10.5962 | 0.7134 | 0.5463 | -0.6534 |
| 3.4070 | 40 | 50.8656 | 39.8339 | 38.9629 | 39.5685 | -10.8656 | 0.1661 | 1.0371 | 0.4315 |
| 3.4866 | 45 | 56.2375 | 45.9279 | 44.1188 | 45.2430 | -11.2375 | -0.9279 | 0.8812 | -0.2430 |
| 3.5554 | 50 | 60.8715 | 51.4929 | 49.0228 | 50.1307 | -10.8715 | -1.4929 | 0.9772 | -0.1307 |
| 3.6286 | 55 | 65.8259 | 57.7943 | 54.8657 | 53.4583 | -10.8259 | -2.7943 | 0.1343 | 1.5417 |
| 3.6905 | 60 | 69.9891 | 63.3457 | 60.2269 | 62.2073 | -9.9891 | -3.3457 | -0.2269 | -2.2073 |
| 3.7197 | 65 | 71.8376 | 65.8950 | 62.6290 | 62.0146 | -6.8376 | -0.8950 | 2.3710 | 2.9854 |
| 3.8191 | 70 | 78.6863 | 75.7882 | 73.0465 | 72.6281 | -8.6863 | -5.7882 | -3.0465 | -2.6281 |
| 3.8589 | 75 | 81.2720 | 79.7788 | 77.2944 | 73.6292 | -6.2720 | -4.7788 | -2.2944 | 1.3708 |
| 3.8989 | 80 | 83.8572 | 83.9138 | 81.8243 | 81.5299 | -3.8572 | -3.9138 | -1.8243 | -1.5299 |
| 3.9280 | 85 | 85.6335 | 86.9008 | 85.1676 | 82.5092 | -0.6335 | -1.9008 | -0.1676 | 2.4908 |
| 3.9841 | 90 | 89.3337 | 93.1491 | 92.3988 | 92.9342 | 0.6663 | -3.1491 | -2.3988 | -2.9342 |
| 4.0070 | 95 | 90.6176 | 95.5305 | 95.2694 | 92.2131 | 4.3824 | -0.5305 | -0.2694 | 2.7869 |
| 4.0415 | 100 | 92.6997 | 99.3478 | 99.9477 | 103.4023 | 7.3003 | 0.6522 | 0.0523 | -3.4023 |
| 4.0567 | 105 | 93.3738 | 100.7360 | 101.7511 | 100.0848 | 11.6262 | 4.2640 | 3.2489 | 4.9152 |
| 4.1186 | 110 | 97.3535 | 108.2438 | 111.4675 | 114.3549 | 12.6465 | 1.7562 | -1.4675 | -4.3549 |
| 4.1271 | 115 | 97.4749 | 108.5889 | 111.9345 | 110.8013 | 17.5251 | 6.4111 | 3.0655 | 4.1987 |
| 4.1729 | 120 | 100.2134 | 114.0393 | 119.6608 | 144.1469 | 19.7866 | 5.9607 | 0.3392 | -24.1469 |
errores_largos <- tabla_errores |>
select(
temperatura_C,
Error_lineal_C,
Error_cuadratico_C,
Error_cubico_C,
Error_spline_C
) |>
pivot_longer(
cols = starts_with("Error"),
names_to = "Modelo",
values_to = "Error_C"
) |>
mutate(
Modelo = recode(
Modelo,
Error_lineal_C = "Regresión lineal",
Error_cuadratico_C = "Regresión cuadrática",
Error_cubico_C = "Regresión cúbica",
Error_spline_C = "Spline cúbico natural"
)
)
grafica_errores <- ggplot(
errores_largos,
aes(
x = temperatura_C,
y = Error_C,
shape = Modelo
)
) +
geom_hline(yintercept = 0, linewidth = 0.5) +
geom_point(size = 2.7) +
labs(
title = "Errores de predicción mediante LOOCV",
x = "Temperatura patrón (°C)",
y = "Error de predicción (°C)",
shape = "Modelo"
) +
theme(legend.position = "bottom")
print(grafica_errores)
write_csv(
datos,
"datos_utilizados.csv"
)
write_csv(
tabla_predicciones,
"predicciones_loocv.csv"
)
write_csv(
tabla_errores,
"errores_modelos.csv"
)
write_csv(
resultados_metricas,
"metricas_modelos.csv"
)
ggsave(
"datos_calibracion.png",
plot = grafica_datos,
width = 8,
height = 5,
dpi = 300
)
ggsave(
"comparacion_modelos.png",
plot = grafica_modelos,
width = 8,
height = 5,
dpi = 300
)
ggsave(
"errores_validacion.png",
plot = grafica_errores,
width = 8,
height = 5,
dpi = 300
)
cat("\nProceso finalizado correctamente.\n")
##
## Proceso finalizado correctamente.
La validación cruzada LOOCV permite comparar la capacidad predictiva de los modelos sin evaluarlos únicamente sobre los mismos datos utilizados para ajustarlos.
El spline cúbico natural interpola exactamente los puntos de calibración, por lo que calcular el error sobre esos mismos puntos podría producir valores artificialmente bajos. La validación cruzada ofrece una comparación más rigurosa.
La regresión puede ser favorable cuando los datos contienen ruido experimental, mientras que el spline puede representar mejor variaciones locales. La selección final debe considerar el RMSE, el MAE, el error absoluto máximo y la estabilidad de los residuos.