4  Encadenamiento de Operaciones (Chaining)

En este capítulo dominarás
  • Encadenamiento básico con DT[...][...]
  • Comparación con pipes de dplyr
  • Encadenamiento complejo con múltiples transformaciones
  • Patrones avanzados de flujo de datos
  • Mejores prácticas para código legible y eficiente

4.1 Conceptos Fundamentales del Encadenamiento

El encadenamiento en data.table permite ejecutar múltiples operaciones secuenciales en una sola expresión usando la sintaxis DT[...][...][...]. Cada conjunto de corchetes opera sobre el resultado del anterior.

4.1.1 1. Encadenamiento Básico

# Operaciones separadas (tradicional)
paso1 <- empleados[salario > 45000]  # Filtrar
paso2 <- paso1[, .(empleados = .N, salario_promedio = mean(salario)), by = departamento]  # Agrupar
resultado_separado <- paso2[order(-salario_promedio)]  # Ordenar

# Misma operación encadenada
resultado_encadenado <- empleados[
  salario > 45000
][
  , .(empleados = .N, salario_promedio = round(mean(salario), 0)), by = departamento
][
  order(-salario_promedio)
]

print("Resultado con encadenamiento:")
#> [1] "Resultado con encadenamiento:"
print(resultado_encadenado)
#>    departamento empleados salario_promedio
#>          <char>     <int>            <num>
#> 1:       Ventas         3            74700
#> 2:         RRHH         3            68933
#> 3:     Finanzas         3            63533
#> 4:    Marketing         4            61750
#> 5:           IT         3            61633

4.1.2 2. Ventajas del Encadenamiento

# Ejemplo que muestra las ventajas
analisis_productividad <- empleados[
  activo == TRUE & años_exp >= 3     # Solo empleados activos con experiencia
][
  , productividad_ajustada := pmin(productividad, 5)  # Ajustar outliers
][
  , .(
    empleados = .N,
    productividad_media = round(mean(productividad_ajustada), 2),
    salario_total_grupo = sum(salario_total),
    proyectos_totales = sum(proyectos)
  ), by = .(departamento, nivel)
][
  productividad_media > 1.5          # Solo grupos productivos
][
  order(departamento, -productividad_media)
][
  , ranking := 1:.N                  # Agregar ranking
]

print(analisis_productividad)
#>    departamento   nivel empleados productividad_media salario_total_grupo
#>          <char>  <char>     <int>               <num>               <num>
#> 1:     Finanzas  Junior         1                5.00               79000
#> 2:           IT  Senior         1                5.00               63700
#> 3:           IT Manager         1                4.00               50300
#> 4:    Marketing    Lead         1                2.08               64000
#> 5:    Marketing Manager         1                1.60               78200
#> 6:         RRHH  Junior         1                3.00               82500
#> 7:       Ventas  Junior         1                2.09               89300
#>    proyectos_totales ranking
#>                <int>   <int>
#> 1:                19       1
#> 2:                22       2
#> 3:                24       3
#> 4:                25       4
#> 5:                 8       5
#> 6:                12       6
#> 7:                23       7
💡 ¿Por qué encadenar?
  1. Menos variables temporales - No necesitas almacenar resultados intermedios
  2. Código más compacto - Múltiples operaciones en una expresión
  3. Mejor rendimiento - Menos copias en memoria
  4. Flujo lógico claro - Lee de arriba abajo como una receta

4.2 Comparación con dplyr Pipes

Comparemos las dos aproximaciones principales para operaciones secuenciales:

4.2.1 1. Sintaxis Lado a Lado

# Pipeline complejo con data.table
pipeline_dt <- ventas[
  año == 2024 & valor_neto > 1000    # Filtrar ventas importantes de 2024
][
  , .(
    ventas_totales = sum(valor_neto),
    unidades_totales = sum(cantidad),
    transacciones = .N,
    ticket_promedio = round(mean(valor_neto), 2)
  ), by = .(region, producto)
][
  ventas_totales > 10000             # Solo combinaciones significativas
][
  order(region, -ventas_totales)
][
  , rank_en_region := rank(-ventas_totales), by = region
][
  rank_en_region <= 3                # Top 3 productos por región
]

print(head(pipeline_dt, 12))
#>     region   producto ventas_totales unidades_totales transacciones
#>     <char>     <char>          <num>            <int>         <int>
#>  1: Centro Accesorios      1050344.8             1151           199
#>  2: Centro Smartphone      1023300.8             1023           171
#>  3: Centro     Tablet       874227.4              916           162
#>  4:   Este Smartphone       989853.6             1048           182
#>  5:   Este     Laptop       941461.4              984           164
#> ---                                                                
#>  8:  Norte Accesorios       922333.3             1026           168
#>  9:  Norte     Tablet       909612.5              953           172
#> 10:  Oeste     Laptop      1161534.8             1270           203
#> 11:  Oeste Smartphone       993922.4             1104           178
#> 12:  Oeste Accesorios       950027.6             1048           183
#>     ticket_promedio rank_en_region
#>               <num>          <num>
#>  1:         5278.11              1
#>  2:         5984.22              2
#>  3:         5396.47              3
#>  4:         5438.76              1
#>  5:         5740.62              2
#> ---                               
#>  8:         5490.08              2
#>  9:         5288.44              3
#> 10:         5721.85              1
#> 11:         5583.83              2
#> 12:         5191.41              3
# Mismo pipeline con dplyr
pipeline_dplyr <- ventas %>%
  filter(año == 2024, valor_neto > 1000) %>%
  group_by(region, producto) %>%
  summarise(
    ventas_totales = sum(valor_neto),
    unidades_totales = sum(cantidad),
    transacciones = n(),
    ticket_promedio = round(mean(valor_neto), 2),
    .groups = 'drop'
  ) %>%
  filter(ventas_totales > 10000) %>%
  arrange(region, desc(ventas_totales)) %>%
  group_by(region) %>%
  mutate(rank_en_region = rank(desc(ventas_totales))) %>%
  filter(rank_en_region <= 3) %>%
  ungroup()

print(head(as.data.table(pipeline_dplyr), 12))
#>     region   producto ventas_totales unidades_totales transacciones
#>     <char>     <char>          <num>            <int>         <int>
#>  1: Centro Accesorios      1050344.8             1151           199
#>  2: Centro Smartphone      1023300.8             1023           171
#>  3: Centro     Tablet       874227.4              916           162
#>  4:   Este Smartphone       989853.6             1048           182
#>  5:   Este     Laptop       941461.4              984           164
#> ---                                                                
#>  8:  Norte Accesorios       922333.3             1026           168
#>  9:  Norte     Tablet       909612.5              953           172
#> 10:  Oeste     Laptop      1161534.8             1270           203
#> 11:  Oeste Smartphone       993922.4             1104           178
#> 12:  Oeste Accesorios       950027.6             1048           183
#>     ticket_promedio rank_en_region
#>               <num>          <num>
#>  1:         5278.11              1
#>  2:         5984.22              2
#>  3:         5396.47              3
#>  4:         5438.76              1
#>  5:         5740.62              2
#> ---                               
#>  8:         5490.08              2
#>  9:         5288.44              3
#> 10:         5721.85              1
#> 11:         5583.83              2
#> 12:         5191.41              3

4.2.2 2. Benchmark de Performance

# Crear dataset más grande para benchmark significativo
set.seed(123)
ventas_grandes <- data.table(
  id = 1:100000,
  categoria = sample(LETTERS[1:5], 100000, replace = TRUE),
  valor = runif(100000, 10, 1000),
  fecha = sample(seq(as.Date("2023-01-01"), as.Date("2024-12-31"), by = "day"), 100000, replace = TRUE)
)
ventas_grandes[, año := year(fecha)]

# Benchmark
benchmark_pipes <- microbenchmark(
  "data.table_chain" = ventas_grandes[año == 2024][, .(suma = sum(valor)), by = categoria][order(-suma)],
  "dplyr_pipes" = ventas_grandes %>% filter(año == 2024) %>% group_by(categoria) %>% summarise(suma = sum(valor), .groups = 'drop') %>% arrange(desc(suma)),
  times = 20
)

print(benchmark_pipes)
#> Unit: milliseconds
#>              expr      min       lq     mean   median       uq       max neval
#>  data.table_chain 3.392227 3.770979 4.534649 4.212111 4.887797  9.395095    20
#>       dplyr_pipes 6.779315 6.970010 8.489753 7.592757 9.959217 13.315547    20

4.3 Patrones Avanzados de Encadenamiento

4.3.1 1. Encadenamiento con Modificación por Referencia

# Combinar := con encadenamiento para análisis iterativo
empleados_analisis <- copy(empleados)[
  , salario_z := scale(salario_total)[,1], by = departamento  # Z-score por depto
][
  , categoria_performance := cut(salario_z, breaks = c(-Inf, -1, 1, Inf), 
                                labels = c("Bajo", "Medio", "Alto"))
][
  , .(
    empleados = .N,
    salario_promedio = round(mean(salario_total), 0),
    performance_dist = paste(table(categoria_performance), collapse = "/")
  ), by = .(departamento, categoria_performance)
][
  order(departamento, categoria_performance)
]

print(empleados_analisis)
#>     departamento categoria_performance empleados salario_promedio
#>           <char>                <fctr>     <int>            <num>
#>  1:     Finanzas                  Bajo         1            39000
#>  2:     Finanzas                 Medio         3            71167
#>  3:           IT                  Bajo         1            50300
#>  4:           IT                 Medio         2            63500
#>  5:           IT                  Alto         1            72700
#>  6:    Marketing                  Bajo         1            64000
#>  7:    Marketing                 Medio         3            75167
#>  8:         RRHH                 Medio         3            62933
#>  9:         RRHH                  Alto         1            89500
#> 10:       Ventas                  Bajo         1            59100
#> 11:       Ventas                 Medio         3            84667
#>     performance_dist
#>               <char>
#>  1:            1/0/0
#>  2:            0/1/0
#>  3:            1/0/0
#>  4:            0/1/0
#>  5:            0/0/1
#>  6:            1/0/0
#>  7:            0/1/0
#>  8:            0/1/0
#>  9:            0/0/1
#> 10:            1/0/0
#> 11:            0/1/0

4.3.2 2. Encadenamiento con Validaciones

# Pipeline con validaciones integradas
pipeline_validado <- ventas[
  !is.na(valor_neto) & valor_neto > 0     # Validar datos básicos
][
  , .N, by = año                           # Verificar distribución temporal
][
  , {
    cat("Distribución por año:\n")
    print(.SD)
    if(min(N) < 100) warning("Pocos datos en algunos años")
    .SD
  }
][
  # Continuar con el análisis principal
  , .(años_con_datos = .N, transacciones_totales = sum(N))
]
#> Distribución por año:
#>      año     N
#>    <int> <int>
#> 1:  2023  4962
#> 2:  2024  5038

print(pipeline_validado)
#>    años_con_datos transacciones_totales
#>             <int>                 <int>
#> 1:              2                 10000

4.3.3 3. Encadenamiento con Análisis Exploratorio

# Pipeline de análisis exploratorio
eda_pipeline <- ventas[
  sample(.N, 5000)                    # Muestra para EDA rápido
][
  , .(
    valores_unicos = uniqueN(cliente_id),
    valor_promedio = round(mean(valor_neto), 2),
    valor_mediana = round(median(valor_neto), 2),
    outliers_superiores = sum(valor_neto > quantile(valor_neto, 0.95))
  ), by = .(region, categoria)
][
  , coef_variacion := round((valor_promedio - valor_mediana) / valor_promedio, 3)
][
  order(-valores_unicos)
][
  , {
    cat("Top regiones-categorías por diversidad de clientes:\n")
    print(.SD[1:5])
    .SD
  }
]
#> Top regiones-categorías por diversidad de clientes:
#>    region categoria valores_unicos valor_promedio valor_mediana
#>    <char>    <char>          <int>          <num>         <num>
#> 1:  Oeste Servicios            259        4661.32       3620.13
#> 2:  Norte  Hardware            255        4513.11       3287.83
#> 3:  Oeste  Hardware            249        4866.92       3741.86
#> 4:   Este  Software            247        4632.75       3713.56
#> 5: Centro  Software            246        4679.73       3562.80
#>    outliers_superiores coef_variacion
#>                  <int>          <num>
#> 1:                  19          0.223
#> 2:                  18          0.271
#> 3:                  18          0.231
#> 4:                  18          0.198
#> 5:                  17          0.239

4.3.4 4. Encadenamiento con Funciones Personalizadas

# Definir función auxiliar
calcular_metricas_avanzadas <- function(valores) {
  list(
    media = round(mean(valores), 2),
    percentil_95 = round(quantile(valores, 0.95), 2),
    coef_asimetria = round((mean(valores) - median(valores)) / sd(valores), 3),
    outliers_count = sum(valores > (mean(valores) + 2 * sd(valores)))
  )
}

# Pipeline con función personalizada
metricas_avanzadas <- ventas[
  año == 2024 & !is.na(valor_neto)
][
  , calcular_metricas_avanzadas(valor_neto), by = .(region, trimestre)
][
  outliers_count > 5                 # Solo regiones/trimestres con anomalías
][
  order(region, trimestre)
]

print(metricas_avanzadas)
#>     region trimestre   media percentil_95 coef_asimetria outliers_count
#>     <char>     <int>   <num>        <num>          <num>          <int>
#>  1: Centro         1 4512.27     12113.24          0.276             14
#>  2: Centro         2 4379.47     12860.78          0.303             13
#>  3: Centro         3 4586.36     12705.74          0.346             16
#>  4: Centro         4 5031.66     13807.56          0.272             18
#>  5:   Este         1 4647.60     12434.10          0.265             14
#> ---                                                                    
#> 16:  Oeste         4 4629.18     13104.52          0.269             17
#> 17:    Sur         1 5657.17     14089.79          0.278             13
#> 18:    Sur         2 4854.70     12187.45          0.212             11
#> 19:    Sur         3 5142.59     13569.57          0.337              9
#> 20:    Sur         4 4893.29     12974.33          0.307             13

4.4 Técnicas de Debugging en Encadenamientos

4.4.1 1. Inspección Intermedia

# Técnica: usar {} para inspeccionar pasos intermedios
debug_pipeline <- empleados[
  salario_total > 50000
][
  , {
    cat("Después del filtro:", .N, "filas\n")
    .SD
  }
][
  , .(empleados = .N, salario_avg = mean(salario_total)), by = departamento
][
  , {
    cat("Después de agrupar:", nrow(.SD), "grupos\n")
    print(.SD)
    .SD
  }
][
  order(-salario_avg)
]
#> Después del filtro: 19 filas
#> Después de agrupar: 5 grupos
#>    departamento empleados salario_avg
#>          <char>     <int>       <num>
#> 1:       Ventas         4    78275.00
#> 2:           IT         4    62500.00
#> 3:    Marketing         4    72375.00
#> 4:         RRHH         4    69575.00
#> 5:     Finanzas         3    71166.67

4.4.2 2. Validaciones en Cadena

# Pipeline robusto con validaciones
pipeline_robusto <- ventas[
  año %in% 2023:2024                      # Años válidos
][
  , if(.N == 0) stop("No hay datos después del filtro de años") else .SD
][
  !is.na(valor_neto) & valor_neto > 0     # Valores válidos
][
  , if(.N < 1000) warning("Pocos datos para análisis confiable") else .SD
][
  , .(ventas = sum(valor_neto), transacciones = .N), by = .(año, region)
][
  ventas > 0                              # Verificar resultados lógicos
]

print(pipeline_robusto)
#>       año region  ventas transacciones
#>     <int> <char>   <num>         <int>
#>  1:  2023    Sur 4770068           958
#>  2:  2024 Centro 4669677          1005
#>  3:  2024  Norte 4571534           985
#>  4:  2024   Este 4589704           979
#>  5:  2023 Centro 4841287           986
#>  6:  2023   Este 4799598           993
#>  7:  2024    Sur 5136588           996
#>  8:  2024  Oeste 5059483          1073
#>  9:  2023  Norte 4917000          1016
#> 10:  2023  Oeste 4672004          1009

4.5 Ejercicios Prácticos

🏋️ Ejercicio 6: Pipeline de Análisis de Ventas

Usando el dataset ventas, crea un pipeline encadenado que:

  1. Filtre ventas del último trimestre de 2024
  2. Calcule métricas por vendedor y región
  3. Identifique vendedores top (> percentil 80 en ventas)
  4. Cree ranking por región
  5. Genere un reporte final con formato
# Pipeline complejo de análisis de ventas
reporte_ventas <- ventas[
  año == 2024 & trimestre == 4           # 1. Q4 2024
][
  , .(                                    # 2. Métricas por vendedor-región
    ventas_totales = sum(valor_neto),
    unidades_vendidas = sum(cantidad),
    transacciones = .N,
    ticket_promedio = round(mean(valor_neto), 2),
    mejor_producto = names(sort(table(producto), decreasing = TRUE))[1]
  ), by = .(vendedor_id, region)
][
  ventas_totales > quantile(ventas_totales, 0.8)  # 3. Top performers (percentil 80)
][
  order(region, -ventas_totales)
][
  , ranking_regional := 1:.N, by = region         # 4. Ranking por región
][
  , .(                                    # 5. Reporte final formateado
    Region = region,
    Vendedor = paste0("ID_", vendedor_id),
    Ranking = ranking_regional,
    Ventas = paste0("$", format(round(ventas_totales, 0), big.mark = ",")),
    Unidades = format(unidades_vendidas, big.mark = ","),
    Transacciones = transacciones,
    Ticket_Avg = paste0("$", ticket_promedio),
    Top_Producto = mejor_producto,
    Performance = ifelse(ranking_regional == 1, "★★★", 
                        ifelse(ranking_regional <= 3, "★★", "★"))
  )
][
  order(Region, Ranking)
]

print(reporte_ventas)
#>     Region Vendedor Ranking  Ventas Unidades Transacciones Ticket_Avg
#>     <char>   <char>   <int>  <char>   <char>         <int>     <char>
#>  1: Centro    ID_21       1 $57,943       62             8   $7242.93
#>  2: Centro    ID_23       2 $55,866       56            10   $5586.62
#>  3: Centro    ID_22       3 $50,392       63             8   $6299.04
#>  4: Centro     ID_9       4 $47,863       44             6   $7977.08
#>  5: Centro    ID_24       5 $47,785       37             8   $5973.07
#> ---                                                                  
#> 46:    Sur    ID_24       3 $42,631       34             7    $6090.2
#> 47:    Sur     ID_5       4 $41,461       42            11   $3769.21
#> 48:    Sur    ID_43       5 $41,168       38             7   $5881.16
#> 49:    Sur    ID_48       6 $39,735       39             7   $5676.37
#> 50:    Sur    ID_28       7 $39,045       29             5   $7809.03
#>     Top_Producto Performance
#>           <char>      <char>
#>  1:       Laptop         ★★★
#>  2:       Tablet          ★★
#>  3:      Desktop          ★★
#>  4:   Smartphone           ★
#>  5:      Desktop           ★
#> ---                         
#> 46:   Accesorios          ★★
#> 47:       Tablet           ★
#> 48:   Accesorios           ★
#> 49:   Accesorios           ★
#> 50:       Laptop           ★

# Mostrar tabla del reporte formateada para PDF
knitr::kable(
  reporte_ventas,
  caption = "Reporte de Top Performers Q4 2024",
  digits = 2,
  format.args = list(big.mark = ",")
)
Reporte de Top Performers Q4 2024
Region Vendedor Ranking Ventas Unidades Transacciones Ticket_Avg Top_Producto Performance
Centro ID_21 1 $57,943 62 8 $7242.93 Laptop ★★★
Centro ID_23 2 $55,866 56 10 $5586.62 Tablet ★★
Centro ID_22 3 $50,392 63 8 $6299.04 Desktop ★★
Centro ID_9 4 $47,863 44 6 $7977.08 Smartphone
Centro ID_24 5 $47,785 37 8 $5973.07 Desktop
Centro ID_36 6 $45,849 44 7 $6549.82 Accesorios
Centro ID_12 7 $45,062 79 11 $4096.56 Smartphone
Centro ID_27 8 $43,315 48 9 $4812.83 Laptop
Centro ID_28 9 $43,007 43 8 $5375.83 Accesorios
Centro ID_48 10 $40,534 44 8 $5066.81 Tablet
Centro ID_38 11 $39,617 53 9 $4401.87 Tablet
Centro ID_41 12 $38,036 64 9 $4226.2 Tablet
Este ID_4 1 $60,526 80 12 $5043.81 Smartphone ★★★
Este ID_1 2 $59,086 49 10 $5908.63 Desktop ★★
Este ID_6 3 $44,048 34 7 $6292.61 Desktop ★★
Este ID_45 4 $41,654 53 7 $5950.57 Smartphone
Este ID_27 5 $41,578 39 7 $5939.73 Desktop
Este ID_37 6 $41,310 49 9 $4590.03 Laptop
Este ID_50 7 $40,439 33 8 $5054.91 Desktop
Este ID_11 8 $38,579 41 7 $5511.27 Desktop
Este ID_3 9 $38,573 45 8 $4821.65 Laptop
Este ID_44 10 $37,725 46 7 $5389.32 Laptop
Norte ID_5 1 $61,248 55 9 $6805.35 Accesorios ★★★
Norte ID_10 2 $58,480 60 10 $5848.04 Smartphone ★★
Norte ID_40 3 $52,650 53 8 $6581.22 Laptop ★★
Norte ID_46 4 $43,763 44 7 $6251.83 Laptop
Norte ID_6 5 $43,651 50 12 $3637.6 Laptop
Norte ID_31 6 $41,523 37 6 $6920.42 Laptop
Oeste ID_36 1 $52,220 48 6 $8703.3 Smartphone ★★★
Oeste ID_24 2 $49,463 58 9 $5495.85 Laptop ★★
Oeste ID_1 3 $49,336 62 9 $5481.81 Accesorios ★★
Oeste ID_43 4 $48,605 59 11 $4418.62 Smartphone
Oeste ID_48 5 $47,777 52 9 $5308.6 Desktop
Oeste ID_40 6 $45,381 47 9 $5042.38 Accesorios
Oeste ID_47 7 $45,205 58 9 $5022.72 Laptop
Oeste ID_23 8 $44,807 55 12 $3733.94 Laptop
Oeste ID_32 9 $44,092 49 8 $5511.46 Laptop
Oeste ID_4 10 $43,620 52 7 $6231.43 Laptop
Oeste ID_18 11 $43,598 36 5 $8719.6 Desktop
Oeste ID_12 12 $43,212 35 7 $6173.21 Laptop
Oeste ID_42 13 $42,199 45 7 $6028.39 Accesorios
Oeste ID_3 14 $41,547 35 7 $5935.25 Laptop
Oeste ID_13 15 $39,928 32 6 $6654.71 Smartphone
Sur ID_3 1 $52,542 43 7 $7506.06 Accesorios ★★★
Sur ID_7 2 $45,035 39 9 $5003.94 Tablet ★★
Sur ID_24 3 $42,631 34 7 $6090.2 Accesorios ★★
Sur ID_5 4 $41,461 42 11 $3769.21 Tablet
Sur ID_43 5 $41,168 38 7 $5881.16 Accesorios
Sur ID_48 6 $39,735 39 7 $5676.37 Accesorios
Sur ID_28 7 $39,045 29 5 $7809.03 Laptop
🏋️ Ejercicio 7: Análisis de Cohortes con Encadenamiento

Crea un análisis de cohortes de empleados:

  1. Agrupa empleados por año de ingreso
  2. Calcula retención, salarios promedios, y promociones
  3. Identifica cohortes exitosas vs. problemáticas
  4. Genera insights automáticos
# Análisis de cohortes con encadenamiento
library(lubridate)

analisis_cohortes <- empleados[
  !is.na(fecha_ingreso)                    # 1. Datos válidos
][
  , año_ingreso := year(fecha_ingreso)     # Extraer año de cohorte
][
  , años_en_empresa := round(as.numeric(Sys.Date() - fecha_ingreso) / 365, 1)
][
  , .(                                     # 2. Métricas por cohorte
    empleados_iniciales = .N,
    empleados_activos = sum(activo),
    retencion_pct = round(mean(activo) * 100, 1),
    salario_inicial_avg = round(mean(salario), 0),
    salario_actual_avg = round(mean(salario_total), 0),
    crecimiento_salarial = round(mean(salario_total) / mean(salario) - 1, 2),
    años_promedio = round(mean(años_en_empresa), 1),
    productividad_avg = round(mean(productividad), 2),
    managers_promocionados = sum(nivel == "Manager")
  ), by = año_ingreso
][
  empleados_iniciales >= 2                 # Solo cohortes con suficientes datos
][
  , `:=`(                                 # 3. Clasificar cohortes
    categoria_retencion = cut(retencion_pct, 
                             breaks = c(0, 70, 90, 100), 
                             labels = c("Problemática", "Regular", "Exitosa")),
    categoria_crecimiento = ifelse(crecimiento_salarial > 0.15, "Alto", 
                                 ifelse(crecimiento_salarial > 0.05, "Medio", "Bajo"))
  )
][
  order(año_ingreso)
][
  , {                                     # 4. Generar insights automáticos
    cat("=== INSIGHTS DE COHORTES ===\n\n")
    
    cohorte_mejor <- .SD[which.max(retencion_pct * crecimiento_salarial)]
    cohorte_peor <- .SD[which.min(retencion_pct * (1 + crecimiento_salarial))]
    
    cat("🏆 MEJOR COHORTE:", cohorte_mejor$año_ingreso, "\n")
    cat("   • Retención:", cohorte_mejor$retencion_pct, "%\n")
    cat("   • Crecimiento salarial:", cohorte_mejor$crecimiento_salarial * 100, "%\n\n")
    
    cat("⚠️  COHORTE PROBLEMÁTICA:", cohorte_peor$año_ingreso, "\n")
    cat("   • Retención:", cohorte_peor$retencion_pct, "%\n")
    cat("   • Crecimiento salarial:", cohorte_peor$crecimiento_salarial * 100, "%\n\n")
    
    cat("📊 RESUMEN GENERAL:\n")
    cat("   • Retención promedio:", round(mean(retencion_pct), 1), "%\n")
    cat("   • Cohortes exitosas:", sum(categoria_retencion == "Exitosa"), "de", .N, "\n")
    cat("   • Managers promocionados:", sum(managers_promocionados), "total\n\n")
    
    .SD
  }
]
#> === INSIGHTS DE COHORTES ===
#> 
#> 🏆 MEJOR COHORTE: 2022 
#>    • Retención: 100 %
#>    • Crecimiento salarial: 21 %
#> 
#> ⚠️  COHORTE PROBLEMÁTICA: 2023 
#>    • Retención: 66.7 %
#>    • Crecimiento salarial: 15 %
#> 
#> 📊 RESUMEN GENERAL:
#>    • Retención promedio: 83.3 %
#>    • Cohortes exitosas: 2 de 5 
#>    • Managers promocionados: 4 total

print(analisis_cohortes[, .(año_ingreso, empleados_iniciales, retencion_pct, 
                           crecimiento_salarial, categoria_retencion, categoria_crecimiento)])
#>    año_ingreso empleados_iniciales retencion_pct crecimiento_salarial
#>          <num>               <int>         <num>                <num>
#> 1:        2019                   6          83.3                 0.10
#> 2:        2020                   3         100.0                 0.09
#> 3:        2021                   3          66.7                 0.18
#> 4:        2022                   4         100.0                 0.21
#> 5:        2023                   3          66.7                 0.15
#>    categoria_retencion categoria_crecimiento
#>                 <fctr>                <char>
#> 1:             Regular                 Medio
#> 2:             Exitosa                 Medio
#> 3:        Problemática                  Alto
#> 4:             Exitosa                  Alto
#> 5:        Problemática                 Medio

4.6 Mejores Prácticas para Encadenamiento

4.6.1 1. Legibilidad vs. Performance

# ✅ HACER: Encadenamiento legible con líneas separadas
resultado <- datos[
  filtro_simple & condicion_clara
][
  , .(metrica1 = func1(col1), metrica2 = func2(col2)), by = grupo
][
  order(-metrica1)
][
  1:10
]

# ❌ EVITAR: Una línea muy larga
resultado <- datos[filtro & condicion][, .(m1 = f1(c1), m2 = f2(c2)), by = g][order(-m1)][1:10]

# ✅ HACER: Comentarios para pasos complejos
resultado <- datos[
  filtro_complejo                    # Paso 1: filtrar casos válidos
][
  , calculo_complejo(), by = grupo   # Paso 2: agregaciones por grupo
][
  post_procesamiento()               # Paso 3: ajustes finales
]

4.6.2 2. Cuándo NO usar encadenamiento

# ❌ Evitar: Lógica muy compleja en una cadena
# Mejor usar pasos separados para debugging
paso1 <- datos[filtro_complejo_con_multiples_condiciones]
paso2 <- paso1[, calculo_muy_complejo_con_multiples_funciones(), by = multiples_grupos]
paso3 <- paso2[post_procesamiento_complejo_con_validaciones()]

# ❌ Evitar: Cadenas que modifican el objeto original sin copy()
# Peligroso si necesitas preservar datos originales
datos_originales[, nueva_col := calculo()][filtro][, otra_col := otro_calculo()]

# ✅ Hacer: Usar copy() cuando modifiques
datos_procesados <- copy(datos_originales)[
  , nueva_col := calculo()
][
  filtro
][
  , otra_col := otro_calculo()
]

4.6.3 3. Optimización de Performance

# ✅ HACER: Filtrar temprano para reducir datos
datos[
  filtro_restrictivo               # Reduce datos primero
][
  calculo_costoso(), by = grupo    # Luego opera en menos datos
]

# ❌ EVITAR: Cálculos costosos antes de filtrar
datos[
  calculo_costoso(), by = grupo    # Opera en todos los datos
][
  filtro_restrictivo               # Filtra después
]

# ✅ HACER: Usar .SDcols para limitar columnas en operaciones costosas
datos[
  filtro
][
  , lapply(.SD, operacion_costosa), by = grupo, .SDcols = columnas_necesarias
]

🎯 Puntos Clave de Este Capítulo
  1. El encadenamiento hace el código más conciso y eficiente al eliminar variables temporales
  2. Cada [...] opera sobre el resultado del anterior, creando un flujo lógico claro
  3. Performance: data.table chaining es más rápido que dplyr pipes para la mayoría de operaciones
  4. Legibilidad: Usar líneas separadas y comentarios para encadenamientos complejos
  5. Debugging: Insertar {} para inspeccionar resultados intermedios
  6. Filtrar temprano para optimizar performance en cadenas largas

El encadenamiento es una técnica fundamental que, una vez dominada, hace que tu código data.table sea más elegante y eficiente. En el próximo capítulo exploraremos cómo combinar múltiples tablas usando joins.