1 1. Configuración inicial

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))

2 2. Objetivo

Se comparan cuatro modelos para convertir el voltaje de un termistor en temperatura:

  1. Regresión lineal.
  2. Regresión cuadrática.
  3. Regresión cúbica.
  4. Spline cúbico natural.

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| \]

3 3. Carga y validación de datos

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"
)
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)

4 4. Ajuste de modelos

4.1 4.1 Regresión lineal

\[ 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

4.2 4.2 Regresión cuadrática

\[ 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

4.3 4.3 Regresión cúbica

\[ 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

4.4 4.4 Spline cúbico natural

modelo_spline <- splinefun(
  x = datos$voltaje_V,
  y = datos$temperatura_C,
  method = "natural"
)

5 5. Funciones de validación

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
}

6 6. Validación cruzada LOOCV

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"
)
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

7 7. Métricas comparativas

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"
)
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 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.

8 8. Curvas de calibración

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)

9 9. Errores de predicción

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"
)
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)

10 10. Exportación

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.

11 11. Análisis técnico

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.

12 12. Limitaciones

  1. Los datos simulados solo verifican el funcionamiento del código.
  2. La conclusión final requiere datos experimentales reales.
  3. No se incorporó la incertidumbre del instrumento patrón.
  4. Los modelos no deben extrapolarse fuera del intervalo de calibración.
  5. El spline puede ser sensible a valores atípicos.
  6. Se recomienda validar el modelo con datos independientes.