Exploracion Inicial del Sistema Hidraulico

Verificacion, Visualizacion de Señales y Extraccion de Features — Dataset UCI

Author

Sergio Andres Beltran Gonzalez

Published

July 15, 2026


1 Contexto del Proyecto

NoteDataset UCI: Condition Monitoring of Hydraulic Systems

Este informe documenta la exploracion inicial del dataset publico de monitoreo de condicion de un banco de pruebas hidraulico (UCI Machine Learning Repository). El sistema contiene un circuito de trabajo primario y un circuito de enfriamiento-filtracion secundario conectados por un tanque de aceite.

Objetivo: Construir una vision completa del dataset antes de cualquier modelado — entender la estructura, las señales crudas, y que features discriminan mejor cada componente.

El dataset contiene 2,205 ciclos de operacion de 60 segundos cada uno, medidos por 17 sensores a diferentes frecuencias de muestreo. Cuatro componentes son evaluados con diferentes grados de degradacion:

Componente Variable Estados Dificultad esperada
Enfriador (Cooler) Eficiencia (%) 3, 20, 100 Facil
Valvula Conmutacion (%) 73, 80, 90, 100 Facil
Bomba Fuga interna 0, 1, 2 Dificil
Acumulador Presion (bar) 90, 100, 115, 130 Dificil

2 Paso 1 — Cargar y Verificar la Forma de los Datos

TipPor que es importante

Antes de graficar o calcular nada, confirmamos que entendemos la estructura: cuantas filas tiene cada sensor, cuantas columnas, si hay nulos, y si todas las dimensiones coinciden. Esto previene errores silenciosos en todo el analisis posterior.

2.1 Carga de los 17 archivos de sensores

Ver codigo
# Diccionario de sensores
sensores_info <- data.table(
  archivo    = c("PS1","PS2","PS3","PS4","PS5","PS6",
                 "EPS1","FS1","FS2",
                 "TS1","TS2","TS3","TS4",
                 "VS1","CE","CP","SE"),
  nombre     = c("Presion 1","Presion 2","Presion 3","Presion 4",
                 "Presion 5","Presion 6",
                 "Potencia Motor","Flujo Vol. 1","Flujo Vol. 2",
                 "Temperatura 1","Temperatura 2","Temperatura 3","Temperatura 4",
                 "Vibracion","Efic. Enfriamiento","Pot. Enfriamiento","Factor Eficiencia"),
  unidad     = c(rep("bar",6),"W",rep("l/min",2), rep("C",4),"mm/s","%","kW","%"),
  frecuencia = c(rep(100,6),100,rep(10,2), rep(1,4),1,1,1,1),
  cols_esperadas = c(rep(6000,6),6000,rep(600,2), rep(60,4),60,60,60,60)
)

# Cargar todos los sensores
datos_sensores <- list()
inventario <- data.table()

for (i in 1:nrow(sensores_info)) {
  sensor <- sensores_info$archivo[i]
  ruta   <- file.path(DIR_DATASET, paste0(sensor, ".txt"))
  if (!file.exists(ruta)) next

  dt <- fread(ruta, header = FALSE, sep = "\t")
  datos_sensores[[sensor]] <- dt

  inventario <- rbind(inventario, data.table(
    Sensor       = sensor,
    Nombre       = sensores_info$nombre[i],
    Unidad       = sensores_info$unidad[i],
    `Hz`         = sensores_info$frecuencia[i],
    Filas        = nrow(dt),
    Columnas     = ncol(dt),
    Esperadas    = sensores_info$cols_esperadas[i],
    Coincide     = ifelse(ncol(dt) == sensores_info$cols_esperadas[i], "OK", "ERROR"),
    Nulos        = sum(is.na(dt)),
    `Tam (MB)`   = round(file.size(ruta) / 1024^2, 1)
  ))
}

2.2 Inventario completo de archivos

Ver codigo
tabla_hyd(inventario,
          caption = "Inventario de Archivos de Sensores del Dataset",
          align   = c("l","l","c","r","r","r","r","c","r","r"),
          nota    = "Todos los archivos deben tener 2,205 filas (ciclos). Las columnas dependen de la frecuencia de muestreo.")
Inventario de Archivos de Sensores del Dataset
Sensor Nombre Unidad Hz Filas Columnas Esperadas Coincide Nulos Tam (MB)
PS1 Presion 1 bar 100 2205 6000 6000 OK 0 87.2
PS2 Presion 2 bar 100 2205 6000 6000 OK 0 78.6
PS3 Presion 3 bar 100 2205 6000 6000 OK 0 66.7
PS4 Presion 4 bar 100 2205 6000 6000 OK 0 44.5
PS5 Presion 5 bar 100 2205 6000 6000 OK 0 74.2
PS6 Presion 6 bar 100 2205 6000 6000 OK 0 74.3
EPS1 Potencia Motor W 100 2205 6000 6000 OK 0 83.4
FS1 Flujo Vol. 1 l/min 10 2205 600 600 OK 0 7.2
FS2 Flujo Vol. 2 l/min 10 2205 600 600 OK 0 7.9
TS1 Temperatura 1 C 1 2205 60 60 OK 0 0.9
TS2 Temperatura 2 C 1 2205 60 60 OK 0 0.9
TS3 Temperatura 3 C 1 2205 60 60 OK 0 0.9
TS4 Temperatura 4 C 1 2205 60 60 OK 0 0.9
VS1 Vibracion mm/s 1 2205 60 60 OK 0 0.7
CE Efic. Enfriamiento % 1 2205 60 60 OK 0 0.9
CP Pot. Enfriamiento kW 1 2205 60 60 OK 0 0.7
SE Factor Eficiencia % 1 2205 60 60 OK 0 0.8
Nota. Todos los archivos deben tener 2,205 filas (ciclos). Las columnas dependen de la frecuencia de muestreo.

2.3 Carga y verificacion de etiquetas

Ver codigo
profile <- fread(file.path(DIR_DATASET, "profile.txt"), header = FALSE, sep = "\t")
colnames(profile) <- c("cooler", "valve", "pump", "accumulator", "stable")

# Factores con etiquetas descriptivas
profile[, cooler_label := factor(cooler,
  levels = c(3, 20, 100),
  labels = c("Falla total (3%)", "Efic. reducida (20%)", "Plena efic. (100%)")
)]
profile[, valve_label := factor(valve,
  levels = c(73, 80, 90, 100),
  labels = c("Falla total (73%)", "Retraso severo (80%)", "Retraso leve (90%)", "Optimo (100%)")
)]
profile[, pump_label := factor(pump,
  levels = c(0, 1, 2),
  labels = c("Sin fuga (0)", "Fuga debil (1)", "Fuga severa (2)")
)]
profile[, accumulator_label := factor(accumulator,
  levels = c(90, 100, 115, 130),
  labels = c("Falla total (90)", "Pres. muy baja (100)", "Pres. baja (115)", "Optimo (130)")
)]
profile[, stable_label := factor(stable, levels = c(0, 1), labels = c("Estable", "Inestable"))]

# Verificacion de consistencia
n_ciclos <- nrow(profile)
filas_ok <- all(sapply(datos_sensores, nrow) == n_ciclos)
nulos_total <- sum(inventario$Nulos)
ImportantResultado de la Verificacion
  • Total de ciclos: 2,205
  • Consistencia de filas en todos los sensores: TODAS COINCIDEN
  • Valores nulos totales: 0

2.4 Distribucion de estados por componente

Ver codigo
p_d1 <- ggplot(profile, aes(x = cooler_label, fill = cooler_label)) +
  geom_bar(alpha = 0.85, width = 0.7) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = c(pal$fail, pal$warn, pal$ok)) +
  labs(title = "Enfriador (Cooler)", x = NULL, y = "Ciclos") +
  theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1, size = 9)) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

p_d2 <- ggplot(profile, aes(x = valve_label, fill = valve_label)) +
  geom_bar(alpha = 0.85, width = 0.7) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = c(pal$fail, pal$warn, pal$blue_mid, pal$ok)) +
  labs(title = "Valvula", x = NULL, y = "Ciclos") +
  theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1, size = 9)) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

p_d3 <- ggplot(profile, aes(x = pump_label, fill = pump_label)) +
  geom_bar(alpha = 0.85, width = 0.7) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = c(pal$ok, pal$warn, pal$fail)) +
  labs(title = "Bomba (Pump)", x = NULL, y = "Ciclos") +
  theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1, size = 9)) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

p_d4 <- ggplot(profile, aes(x = accumulator_label, fill = accumulator_label)) +
  geom_bar(alpha = 0.85, width = 0.7) +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = c(pal$fail, pal$warn, pal$blue_mid, pal$ok)) +
  labs(title = "Acumulador", x = NULL, y = "Ciclos") +
  theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1, size = 9)) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)))

(p_d1 + p_d2) / (p_d3 + p_d4) +
  plot_annotation(
    title    = "Distribucion de Estados por Componente",
    subtitle = "Verificar desbalance de clases antes de modelar",
    theme = theme(
      plot.title = element_text(face = "bold", size = 16, color = pal$blue_deep),
      plot.subtitle = element_text(color = pal$gray_mid, size = 12)
    )
  )

NoteQue observamos
  • Cooler: Distribucion relativamente balanceada (~730 ciclos por estado).
  • Valve: Desbalance moderado — el estado optimo (100%) domina con 1,125 ciclos.
  • Pump: Desbalance notable — el estado “sin fuga” (0) tiene 1,221 ciclos vs 492 en cada nivel de fuga.
  • Accumulator: Fuerte desbalance — la “falla total” (90 bar) acumula 808 ciclos, mientras que los estados intermedios solo tienen ~400.

Estos desbalances deben considerarse al evaluar metricas de clasificacion.


3 Paso 2 — Visualizar Señales Crudas

TipPor que señales crudas primero

Los papers de Helwig et al. (2015) muestran que la forma de la señal dentro de un ciclo de 60 segundos cambia de manera diferente segun la fase del ciclo (transitorios vs. estacionarios). Mirar 3-5 ciclos coloreados por estado de un componente nos da intuicion fisica antes de calcular estadisticos.

3.1 Señal de Presion (PS1) por estado del Enfriador

Ver codigo
# Funcion para extraer un ciclo como serie temporal
extraer_ciclo <- function(sensor_name, ciclo_idx) {
  dt <- datos_sensores[[sensor_name]]
  valores <- as.numeric(dt[ciclo_idx, ])
  tiempo <- seq(0, 60, length.out = length(valores))
  data.table(tiempo = tiempo, valor = valores, ciclo = ciclo_idx, sensor = sensor_name)
}
Ver codigo
ciclos_cooler <- c(
  profile[cooler == 3,   which = TRUE][1],
  profile[cooler == 20,  which = TRUE][1],
  profile[cooler == 100, which = TRUE][1]
)

datos_ps1 <- rbindlist(lapply(ciclos_cooler, function(c) extraer_ciclo("PS1", c)))
datos_ps1[, estado := profile$cooler_label[ciclo]]

ggplot(datos_ps1, aes(x = tiempo, y = valor, color = estado)) +
  geom_line(linewidth = 0.5, alpha = 0.85) +
  scale_color_manual(values = c(pal$fail, pal$warn, pal$ok)) +
  labs(
    title    = "Señal Cruda: Presion PS1 (100 Hz) por Estado del Enfriador",
    subtitle = "Cada linea es un ciclo completo de 60 segundos (6,000 puntos)",
    x = "Tiempo (s)", y = "Presion (bar)", color = "Estado Cooler"
  )

NoteInterpretacion

La señal de presion PS1 muestra el mismo patron ciclico para los tres estados del enfriador, con diferencias sutiles en los niveles de presion — especialmente en las fases estacionarias del ciclo. Esto sugiere que la media por segmento puede ser un buen discriminador.

3.2 Señal de Flujo (FS1) por estado del Enfriador

Ver codigo
datos_fs1 <- rbindlist(lapply(ciclos_cooler, function(c) extraer_ciclo("FS1", c)))
datos_fs1[, estado := profile$cooler_label[ciclo]]

ggplot(datos_fs1, aes(x = tiempo, y = valor, color = estado)) +
  geom_line(linewidth = 0.6, alpha = 0.85) +
  scale_color_manual(values = c(pal$fail, pal$warn, pal$ok)) +
  labs(
    title    = "Señal Cruda: Flujo Volumetrico FS1 (10 Hz) por Estado del Enfriador",
    subtitle = "600 puntos por ciclo — las diferencias pueden ser mas evidentes en fase estacionaria",
    x = "Tiempo (s)", y = "Flujo (l/min)", color = "Estado Cooler"
  )

3.3 Señal de Presion por estado de la Valvula

Ver codigo
ciclos_valve <- c(
  profile[valve == 73,  which = TRUE][1],
  profile[valve == 90,  which = TRUE][1],
  profile[valve == 100, which = TRUE][1]
)

datos_ps1_v <- rbindlist(lapply(ciclos_valve, function(c) extraer_ciclo("PS1", c)))
datos_ps1_v[, estado := profile$valve_label[ciclo]]

ggplot(datos_ps1_v, aes(x = tiempo, y = valor, color = estado)) +
  geom_line(linewidth = 0.5, alpha = 0.85) +
  scale_color_manual(values = c(pal$fail, pal$blue_mid, pal$ok)) +
  labs(
    title    = "Señal Cruda: Presion PS1 por Estado de la Valvula",
    subtitle = "Las diferencias en la dinamica de conmutacion son visibles en los transitorios",
    x = "Tiempo (s)", y = "Presion (bar)", color = "Estado Valvula"
  )

3.4 Vibracion por estado de la Bomba

Ver codigo
ciclos_pump <- c(
  profile[pump == 0, which = TRUE][1],
  profile[pump == 1, which = TRUE][1],
  profile[pump == 2, which = TRUE][1]
)

datos_vs1 <- rbindlist(lapply(ciclos_pump, function(c) extraer_ciclo("VS1", c)))
datos_vs1[, estado := profile$pump_label[ciclo]]

ggplot(datos_vs1, aes(x = tiempo, y = valor, color = estado)) +
  geom_line(linewidth = 0.6, alpha = 0.85) +
  scale_color_manual(values = c(pal$ok, pal$warn, pal$fail)) +
  labs(
    title    = "Señal Cruda: Vibracion VS1 (1 Hz) por Estado de la Bomba",
    subtitle = "La vibracion puede revelar fugas internas — componente 'dificil' segun la literatura",
    x = "Tiempo (s)", y = "Vibracion (mm/s)", color = "Estado Bomba"
  )

WarningHallazgo clave de las señales crudas

Las señales de cooler y valve muestran diferencias visibles a simple vista entre estados. En cambio, las señales de pump son practicamente identicas entre estados — confirmando lo que reportan Helwig et al. (2015): pump y accumulator son componentes dificiles que requieren features mas sofisticadas para separar las clases.


4 Paso 3 — Extraccion de Features por Ciclo

ImportantDecision de diseño

Cada ciclo tiene entre 60 y 6,000 puntos por sensor. No es practico trabajar con las columnas crudas. Siguiendo la metodologia de Schneider et al. (2017) y Kim & Heo (2022), cada ciclo se divide en 4 segmentos y se calculan 8 metricas estadisticas por segmento:

  • Media, mediana, desviacion estandar
  • Cuartiles Q1 y Q3
  • Asimetria (skewness) y curtosis
  • Rango (max - min)

Esto produce \(10\ sensores \times 4\ segmentos \times 8\ metricas = 320\ features\) por ciclo.

Ver codigo
# Funcion: calcular features para un segmento
calcular_features_segmento <- function(x) {
  x <- as.numeric(x)
  list(
    media   = mean(x, na.rm = TRUE),
    mediana = median(x, na.rm = TRUE),
    sd      = sd(x, na.rm = TRUE),
    q25     = quantile(x, 0.25, na.rm = TRUE, names = FALSE),
    q75     = quantile(x, 0.75, na.rm = TRUE, names = FALSE),
    skew    = skewness(x, na.rm = TRUE),
    kurt    = kurtosis(x, na.rm = TRUE),
    rango   = diff(range(x, na.rm = TRUE))
  )
}

# Funcion: extraer features de un sensor completo
features_un_sensor <- function(sensor_name, n_seg = 4) {
  dt <- datos_sensores[[sensor_name]]
  n_cols <- ncol(dt)
  cortes <- round(seq(1, n_cols + 1, length.out = n_seg + 1))

  resultados <- list()
  for (i in 1:nrow(dt)) {
    fila <- as.numeric(dt[i, ])
    feats_ciclo <- list(ciclo = i)

    for (s in 1:n_seg) {
      ini <- cortes[s]; fin <- cortes[s + 1] - 1
      feats <- calcular_features_segmento(fila[ini:fin])
      for (nm in names(feats)) {
        feats_ciclo[[paste0(sensor_name, "_s", s, "_", nm)]] <- feats[[nm]]
      }
    }
    resultados[[i]] <- feats_ciclo
  }
  rbindlist(resultados)
}

# Sensores a procesar (10 prioritarios)
sensores_explorar <- c("PS1","PS2","FS1","FS2","EPS1","TS1","TS2","VS1","CE","SE")

features_lista <- list()
for (sensor in sensores_explorar) {
  features_lista[[sensor]] <- features_un_sensor(sensor, n_seg = 4)
}

# Unir todo
tabla_features <- features_lista[[1]]
for (i in 2:length(features_lista)) {
  tabla_features <- merge(tabla_features, features_lista[[i]], by = "ciclo")
}
tabla_features <- cbind(tabla_features, profile)

n_features <- ncol(tabla_features) - ncol(profile) - 1  # menos etiquetas y 'ciclo'
TipResultado del Paso 3

Tabla construida: 2,205 ciclos x 320 features + 10 columnas de etiquetas.

Esta tabla es la base para todo el analisis posterior: boxplots, correlaciones, PCA, y eventualmente modelos de clasificacion. Tambien es exportable a SQL o Power BI.

Ver codigo
fwrite(tabla_features, file.path(getwd(), "tabla_features_ciclo.csv"))

5 Paso 4 — Analisis Visual de Features

5.1 Boxplots: Features vs. Estado del Enfriador Facil

Ver codigo
feats_plot <- c("PS1_s1_media","PS1_s3_media","FS1_s1_media","FS1_s3_media",
                "VS1_s1_media","VS1_s3_media","CE_s1_media","SE_s1_media")
feats_plot <- feats_plot[feats_plot %in% names(tabla_features)]

plots_cooler <- lapply(feats_plot, function(feat) {
  ggplot(tabla_features, aes(x = cooler_label, y = .data[[feat]], fill = cooler_label)) +
    geom_boxplot(alpha = 0.75, outlier.size = 0.5, width = 0.6) +
    scale_fill_manual(values = c(pal$fail, pal$warn, pal$ok)) +
    labs(title = gsub("_", " ", feat), x = NULL, y = NULL) +
    theme(legend.position = "none",
          axis.text.x = element_text(angle = 25, hjust = 1, size = 7),
          plot.title = element_text(size = 9))
})

wrap_plots(plots_cooler, ncol = 4) +
  plot_annotation(
    title    = "Features por Estado del Enfriador (Cooler)",
    subtitle = "Componente 'facil' — se espera buena separacion visual entre estados",
    theme = theme(
      plot.title = element_text(face = "bold", size = 15, color = pal$blue_deep),
      plot.subtitle = element_text(color = pal$gray_mid, size = 11)
    )
  )

5.2 Boxplots: Features vs. Estado del Acumulador Dificil

Ver codigo
plots_accum <- lapply(feats_plot, function(feat) {
  ggplot(tabla_features, aes(x = accumulator_label, y = .data[[feat]], fill = accumulator_label)) +
    geom_boxplot(alpha = 0.75, outlier.size = 0.5, width = 0.6) +
    scale_fill_manual(values = c(pal$fail, pal$warn, pal$blue_mid, pal$ok)) +
    labs(title = gsub("_", " ", feat), x = NULL, y = NULL) +
    theme(legend.position = "none",
          axis.text.x = element_text(angle = 25, hjust = 1, size = 7),
          plot.title = element_text(size = 9))
})

wrap_plots(plots_accum, ncol = 4) +
  plot_annotation(
    title    = "Features por Estado del Acumulador",
    subtitle = "Componente 'dificil' — menor separacion visual, solapamiento de distribuciones",
    theme = theme(
      plot.title = element_text(face = "bold", size = 15, color = pal$blue_deep),
      plot.subtitle = element_text(color = pal$gray_mid, size = 11)
    )
  )

5.3 Top 20 Features mas correlacionadas con cada componente

Ver codigo
cols_features <- grep("_s[0-9]+_", names(tabla_features), value = TRUE)

cor_cooler <- sapply(cols_features, function(f) cor(tabla_features[[f]], tabla_features$cooler, use = "complete.obs"))
cor_valve  <- sapply(cols_features, function(f) cor(tabla_features[[f]], tabla_features$valve,  use = "complete.obs"))
cor_pump   <- sapply(cols_features, function(f) cor(tabla_features[[f]], tabla_features$pump,   use = "complete.obs"))
cor_accum  <- sapply(cols_features, function(f) cor(tabla_features[[f]], tabla_features$accumulator, use = "complete.obs"))

cor_resumen <- data.table(
  feature     = cols_features,
  Cooler      = abs(cor_cooler),
  Valve       = abs(cor_valve),
  Pump        = abs(cor_pump),
  Accumulator = abs(cor_accum)
)
cor_resumen[, max_cor := pmax(Cooler, Valve, Pump, Accumulator)]
top20 <- cor_resumen[order(-max_cor)][1:min(20, nrow(cor_resumen))]

top20_long <- melt(top20, id.vars = c("feature", "max_cor"),
                   measure.vars = c("Cooler", "Valve", "Pump", "Accumulator"),
                   variable.name = "componente", value.name = "correlacion")

ggplot(top20_long, aes(x = reorder(feature, max_cor), y = correlacion, fill = componente)) +
  geom_col(position = "dodge", alpha = 0.85) +
  coord_flip() +
  scale_fill_manual(values = c(pal$blue_deep, pal$blue_main, pal$blue_mid, pal$blue_soft)) +
  labs(
    title    = "Top 20 Features: Correlacion Absoluta con cada Componente",
    subtitle = "Features que mejor discriminan el estado de degradacion",
    x = NULL, y = "|Correlacion de Pearson|", fill = "Componente"
  )

5.4 Tabla: Top 5 features por componente

Ver codigo
top5 <- data.table(
  Rank = 1:5,
  Cooler = names(sort(abs(cor_cooler), decreasing = TRUE)[1:5]),
  `|r| Cooler` = round(sort(abs(cor_cooler), decreasing = TRUE)[1:5], 3),
  Valve = names(sort(abs(cor_valve), decreasing = TRUE)[1:5]),
  `|r| Valve` = round(sort(abs(cor_valve), decreasing = TRUE)[1:5], 3),
  Pump = names(sort(abs(cor_pump), decreasing = TRUE)[1:5]),
  `|r| Pump` = round(sort(abs(cor_pump), decreasing = TRUE)[1:5], 3),
  Accum = names(sort(abs(cor_accum), decreasing = TRUE)[1:5]),
  `|r| Accum` = round(sort(abs(cor_accum), decreasing = TRUE)[1:5], 3)
)

tabla_hyd(top5,
          caption = "Top 5 Features mas Correlacionadas por Componente",
          align = c("c", rep(c("l","r"), 4)),
          nota = "Correlacion de Pearson en valor absoluto. Las features con mayor |r| son las mas informativas para clasificacion.")
Top 5 Features mas Correlacionadas por Componente
Rank Cooler &#124;r&#124; Cooler Valve &#124;r&#124; Valve Pump &#124;r&#124; Pump Accum &#124;r&#124; Accum
1 CE_s4_mediana 0.993 PS2_s1_skew 0.983 FS1_s1_q75 0.860 FS1_s1_kurt 0.576
2 CE_s4_media 0.993 PS2_s1_kurt 0.971 SE_s1_q75 0.858 FS1_s1_skew 0.561
3 CE_s4_q75 0.993 PS1_s1_kurt 0.929 FS1_s2_q75 0.836 SE_s2_kurt 0.523
4 CE_s4_q25 0.993 PS1_s1_skew 0.897 SE_s1_rango 0.682 FS1_s1_rango 0.456
5 CE_s3_q25 0.992 EPS1_s1_kurt 0.867 SE_s1_sd 0.636 FS1_s1_sd 0.409
Nota. Correlacion de Pearson en valor absoluto. Las features con mayor |r| son las mas informativas para clasificacion.

5.5 Proyeccion PCA coloreada por componente

Ver codigo
features_mat <- as.matrix(tabla_features[, ..cols_features])
features_scaled <- scale(features_mat)
vars <- apply(features_scaled, 2, var, na.rm = TRUE)
features_scaled <- features_scaled[, !is.na(vars) & vars > 0]

pca_result <- prcomp(features_scaled, center = FALSE, scale. = FALSE)
var_exp <- summary(pca_result)$importance[2, 1:5]

pca_dt <- data.table(
  PC1 = pca_result$x[, 1], PC2 = pca_result$x[, 2],
  cooler = profile$cooler_label, valve = profile$valve_label,
  pump = profile$pump_label, accumulator = profile$accumulator_label
)

p_pca1 <- ggplot(pca_dt, aes(PC1, PC2, color = cooler)) +
  geom_point(alpha = 0.35, size = 1) +
  scale_color_manual(values = c(pal$fail, pal$warn, pal$ok)) +
  labs(title = "Cooler",
       x = sprintf("PC1 (%.1f%%)", var_exp[1]*100),
       y = sprintf("PC2 (%.1f%%)", var_exp[2]*100), color = "Estado") +
  theme(legend.position = "right", legend.text = element_text(size = 7))

p_pca2 <- ggplot(pca_dt, aes(PC1, PC2, color = valve)) +
  geom_point(alpha = 0.35, size = 1) +
  scale_color_manual(values = c(pal$fail, pal$warn, pal$blue_mid, pal$ok)) +
  labs(title = "Valvula",
       x = sprintf("PC1 (%.1f%%)", var_exp[1]*100),
       y = sprintf("PC2 (%.1f%%)", var_exp[2]*100), color = "Estado") +
  theme(legend.position = "right", legend.text = element_text(size = 7))

p_pca3 <- ggplot(pca_dt, aes(PC1, PC2, color = pump)) +
  geom_point(alpha = 0.35, size = 1) +
  scale_color_manual(values = c(pal$ok, pal$warn, pal$fail)) +
  labs(title = "Bomba",
       x = sprintf("PC1 (%.1f%%)", var_exp[1]*100),
       y = sprintf("PC2 (%.1f%%)", var_exp[2]*100), color = "Estado") +
  theme(legend.position = "right", legend.text = element_text(size = 7))

p_pca4 <- ggplot(pca_dt, aes(PC1, PC2, color = accumulator)) +
  geom_point(alpha = 0.35, size = 1) +
  scale_color_manual(values = c(pal$fail, pal$warn, pal$blue_mid, pal$ok)) +
  labs(title = "Acumulador",
       x = sprintf("PC1 (%.1f%%)", var_exp[1]*100),
       y = sprintf("PC2 (%.1f%%)", var_exp[2]*100), color = "Estado") +
  theme(legend.position = "right", legend.text = element_text(size = 7))

(p_pca1 + p_pca2) / (p_pca3 + p_pca4) +
  plot_annotation(
    title    = "Proyeccion PCA (2 Componentes) Coloreada por Componente",
    subtitle = sprintf("PC1 explica %.1f%% y PC2 %.1f%% de la varianza total de %d features",
                       var_exp[1]*100, var_exp[2]*100, ncol(features_scaled)),
    theme = theme(
      plot.title = element_text(face = "bold", size = 16, color = pal$blue_deep),
      plot.subtitle = element_text(color = pal$gray_mid, size = 12)
    )
  )

NoteQue nos dice el PCA
  • Cooler: Se observa separacion clara entre los tres estados en el plano PC1-PC2. Confirma que es un componente “facil”.
  • Valve: Separacion visible aunque con algo de solapamiento en los estados intermedios (80% y 90%).
  • Pump y Accumulator: Las nubes de puntos se superponen mucho — confirma que estos componentes requieren mas features o tecnicas mas avanzadas (como LDA o features espectrales).

6 Paso 5 — Jerarquia de Dificultad

Ver codigo
dificultad <- data.table(
  Componente = c("Cooler", "Valve", "Pump", "Accumulator"),
  cor_top5 = c(
    mean(sort(abs(cor_cooler), decreasing = TRUE)[1:5]),
    mean(sort(abs(cor_valve),  decreasing = TRUE)[1:5]),
    mean(sort(abs(cor_pump),   decreasing = TRUE)[1:5]),
    mean(sort(abs(cor_accum),  decreasing = TRUE)[1:5])
  )
)
dificultad[, Dificultad := ifelse(cor_top5 > 0.9, "Facil",
                             ifelse(cor_top5 > 0.7, "Moderado", "Dificil"))]
dificultad <- dificultad[order(-cor_top5)]

ggplot(dificultad, aes(x = reorder(Componente, cor_top5), y = cor_top5, fill = Dificultad)) +
  geom_col(alpha = 0.85, width = 0.55) +
  geom_text(aes(label = sprintf("%.3f", cor_top5)), hjust = -0.15, size = 5, fontface = "bold") +
  coord_flip() +
  scale_fill_manual(values = c("Facil" = pal$ok, "Moderado" = pal$warn, "Dificil" = pal$fail)) +
  scale_y_continuous(limits = c(0, 1)) +
  labs(
    title    = "Jerarquia de Dificultad de Clasificacion por Componente",
    subtitle = "Basado en |correlacion| promedio de las 5 features mas informativas (Pearson)",
    x = NULL, y = "Correlacion promedio (Top-5 features)", fill = "Clasificacion"
  )

Ver codigo
tabla_hyd(dificultad,
          col.names = c("Componente", "Corr. promedio Top-5", "Dificultad"),
          caption = "Resumen de Dificultad de Clasificacion",
          align = c("l", "r", "c"),
          nota = "La dificultad coincide con lo reportado en Helwig et al. (2015): Cooler y Valve son faciles; Pump y Accumulator requieren mas features y sensores combinados.")
Resumen de Dificultad de Clasificacion
Componente Corr. promedio Top-5 Dificultad
Cooler 0.9930551 Facil
Valve 0.9293390 Facil
Pump 0.7743542 Moderado
Accumulator 0.5051263 Dificil
Nota. La dificultad coincide con lo reportado en Helwig et al. (2015): Cooler y Valve son faciles; Pump y Accumulator requieren mas features y sensores combinados.

7 Conclusiones y Dirección Estratégica

ImportantHallazgos principales de la exploración
  1. Dataset limpio: 2,205 ciclos, 17 sensores, cero nulos. Todas las dimensiones coinciden.
  2. Señales crudas confirman la literatura: Las diferencias entre estados son visibles a simple vista para cooler y valve, pero no para pump y accumulator.
  3. 320 features extraídas (10 sensores x 4 segmentos x 8 métricas) son suficientes para la primera iteración.
  4. Jerarquía de dificultad confirmada: Cooler > Valve >> Pump > Accumulator.
  5. Tabla tabla_features_ciclo.csv exportada — lista para SQL, Power BI o modelado.
NotePreguntas Abiertas y Dirección Futura (Fase 2)

Tener este EDA sólido es el momento exacto para pasar de “explorar” a “decidir con intención”. Las siguientes preguntas orientarán los próximos pasos metodológicos:

Sobre segmentación de la señal - ¿Con 13 segmentos (como Helwig) mejora realmente la separación de Pump y Accumulator, o el aumento de features solo añade ruido y overfitting? - ¿Qué pasa si dividimos por fase física del ciclo (carga, transición, estable) en vez de partes iguales? ¿Cambia la señal de manera discriminatoria?

Sobre Pump y Accumulator (los componentes difíciles) - ¿Ayuda agregar features en el dominio de frecuencia (FFT) como hicieron Helwig & Klein con la vibración? (El sensor VS1 actual solo se usó en el dominio del tiempo). - ¿El desbalance de clases (Accumulator: 808 vs ~400) está afectando la correlación, o es un problema inherente de separabilidad física de las señales? - ¿La proyección LDA separa mejor que PCA para estos dos componentes? (PCA maximiza varianza total, no separación entre clases).

Sobre el diseño del experimento y modelo - ¿Los datos de test (ciclo fijo repetido) representan bien un escenario industrial real, o el modelo fallaría ante cargas variables/aleatorias? - ¿Qué sensores son estrictamente necesarios? (Si replicamos la investigación de Alenany et al., podríamos reducir el tamaño del modelo un 80%+ utilizando solo 1 o 2 sensores).

Sobre el alcance de Negocio - ¿El objetivo final del modelo es clasificar el estado actual (snapshot) o predecir degradación futura (mantenimiento predictivo a largo plazo)? - ¿Qué error es operativamente más costoso en planta: un falso positivo (alarma y parada innecesaria) o un falso negativo (no detectar falla catastrófica)? Esto definirá qué métrica priorizar (TPR vs TNR). - ¿El dashboard final está orientado a ingenieros de mantenimiento (nivel técnico) o a gerencia (nivel agregado)?


Análisis desarrollado como parte del proyecto de Monitoreo de Sistemas Hidráulicos. Herramienta: R R version 4.5.3 (2026-03-11 ucrt) | Renderizado con Quarto