5  Uniones de Datos (Joins)

En este capítulo dominarás
  • Joins básicos con merge() y sintaxis SQL-style
  • Joins optimizados con setkey() y sintaxis X[Y]
  • Sintaxis on para joins flexibles sin keys
  • Update joins con el operador :=
  • Joins múltiples y encadenamiento de uniones
  • Comparación de performance entre métodos

5.1 Fundamentos de Joins en data.table

data.table ofrece múltiples métodos para realizar joins, cada uno optimizado para diferentes casos de uso. La elección del método correcto puede marcar la diferencia entre segundos y minutos en datasets grandes.

5.1.1 1. Tipos de Joins: Conceptos Básicos

# Mostrar las tablas base
cat("=== EMPLEADOS ===\n")
#> === EMPLEADOS ===
print(empleados[1:5, .(emp_id, nombre, departamento_id, salario)])
#>    emp_id     nombre departamento_id salario
#>     <int>     <char>           <int>   <num>
#> 1:      1 Empleado_A               2   39500
#> 2:      2 Empleado_B               5   62200
#> 3:      3 Empleado_C               5   69000
#> 4:      4 Empleado_D               4   66900
#> 5:      5 Empleado_E               1   46700

cat("\n=== DEPARTAMENTOS ===\n") 
#> 
#> === DEPARTAMENTOS ===
print(departamentos[, .(dept_id, nombre_dept, ubicacion)])
#>    dept_id nombre_dept ubicacion
#>      <int>      <char>    <char>
#> 1:       1  Ingeniería    Madrid
#> 2:       2      Ventas Barcelona
#> 3:       3   Marketing   Sevilla
#> 4:       4        RRHH  Valencia
#> 5:       5    Finanzas    Bilbao

cat("\n=== EVALUACIONES (parcial) ===\n")
#> 
#> === EVALUACIONES (parcial) ===
print(evaluaciones[1:5, .(empleado_id, puntuacion, fecha_evaluacion)])
#>    empleado_id puntuacion fecha_evaluacion
#>          <int>      <num>           <Date>
#> 1:           2        3.8       2023-12-22
#> 2:          15        4.3       2023-12-01
#> 3:          14        4.1       2023-12-22
#> 4:           7        4.8       2024-02-09
#> 5:           4        4.1       2023-12-15

5.2 Joins con merge(): El Método Tradicional

La función merge() es familiar para usuarios de R base y ofrece una sintaxis clara para joins simples.

5.2.1 1. Inner Join Básico

# Inner join: solo registros que existen en ambas tablas
empleados_con_dept <- merge(empleados, departamentos, 
                           by.x = "departamento_id", by.y = "dept_id")

cat("Empleados con información de departamento (Inner Join):\n")
#> Empleados con información de departamento (Inner Join):
print(empleados_con_dept[1:6, .(nombre, nombre_dept, salario, ubicacion)])
#>        nombre nombre_dept salario ubicacion
#>        <char>      <char>   <num>    <char>
#> 1: Empleado_E  Ingeniería   46700    Madrid
#> 2: Empleado_G  Ingeniería   67600    Madrid
#> 3: Empleado_A      Ventas   39500 Barcelona
#> 4: Empleado_H      Ventas   35000 Barcelona
#> 5: Empleado_I      Ventas   35800 Barcelona
#> 6: Empleado_N      Ventas   62000 Barcelona

5.2.2 2. Todos los Tipos de Joins con merge()

# Comparar diferentes tipos de joins
inner_join <- merge(empleados, evaluaciones, 
                   by.x = "emp_id", by.y = "empleado_id")
cat("Inner join (empleados con evaluación):", nrow(inner_join), "filas\n")
#> Inner join (empleados con evaluación): 12 filas

left_join <- merge(empleados, evaluaciones, 
                  by.x = "emp_id", by.y = "empleado_id", all.x = TRUE)
cat("Left join (todos los empleados):", nrow(left_join), "filas\n")
#> Left join (todos los empleados): 19 filas

right_join <- merge(empleados, evaluaciones, 
                   by.x = "emp_id", by.y = "empleado_id", all.y = TRUE)
cat("Right join (todas las evaluaciones):", nrow(right_join), "filas\n")
#> Right join (todas las evaluaciones): 12 filas

full_join <- merge(empleados, evaluaciones, 
                  by.x = "emp_id", by.y = "empleado_id", all = TRUE)
cat("Full join (empleados + evaluaciones):", nrow(full_join), "filas\n")
#> Full join (empleados + evaluaciones): 19 filas

# Mostrar ejemplo de left join
print(left_join[1:8, .(nombre, nivel, puntuacion, fecha_evaluacion)])
#>        nombre  nivel puntuacion fecha_evaluacion
#>        <char> <char>      <num>           <Date>
#> 1: Empleado_A Senior         NA             <NA>
#> 2: Empleado_B Junior        3.8       2023-12-22
#> 3: Empleado_B Junior        4.6       2023-12-08
#> 4: Empleado_C Senior        3.4       2023-12-15
#> 5: Empleado_D Junior        4.1       2023-12-15
#> 6: Empleado_E   Lead         NA             <NA>
#> 7: Empleado_F   Lead         NA             <NA>
#> 8: Empleado_G   Lead        4.8       2024-02-09

5.2.3 3. Joins por Múltiples Columnas

# Crear tabla de ejemplo con múltiples keys
historico_salarios <- data.table(
  empleado_id = rep(c(1, 2, 3), each = 2),
  departamento_id = rep(c(1, 2, 1), each = 2),
  año = rep(c(2023, 2024), times = 3),
  salario_historico = c(40000, 42000, 45000, 47000, 38000, 40000)
)

# Join por múltiples columnas
join_multiple <- merge(empleados[1:3, .(emp_id, nombre, departamento_id)], 
                      historico_salarios, 
                      by.x = c("emp_id", "departamento_id"), 
                      by.y = c("empleado_id", "departamento_id"))

print(join_multiple)
#> Empty data.table (0 rows and 5 cols): emp_id,departamento_id,nombre,año,salario_historico

5.3 Joins Optimizados con setkey()

Para datasets grandes y joins repetitivos, establecer keys proporciona un rendimiento excepcional.

5.3.1 1. Estableciendo Keys y Sintaxis X[Y]

# Hacer copias para no modificar originales
emp_key <- copy(empleados)
dept_key <- copy(departamentos)

# Establecer keys (ordena físicamente las tablas)
setkey(emp_key, departamento_id)
setkey(dept_key, dept_id)

# Verificar que las keys están establecidas
cat("Key de empleados:", key(emp_key), "\n")
#> Key de empleados: departamento_id
cat("Key de departamentos:", key(dept_key), "\n")
#> Key de departamentos: dept_id

# Join ultra-rápido con sintaxis X[Y]
resultado_key_join <- dept_key[emp_key]

print(resultado_key_join[1:6, .(nombre, nombre_dept, salario, presupuesto)])
#>        nombre nombre_dept salario presupuesto
#>        <char>      <char>   <num>       <num>
#> 1: Empleado_E  Ingeniería   46700       8e+05
#> 2: Empleado_G  Ingeniería   67600       8e+05
#> 3: Empleado_A      Ventas   39500       6e+05
#> 4: Empleado_H      Ventas   35000       6e+05
#> 5: Empleado_I      Ventas   35800       6e+05
#> 6: Empleado_N      Ventas   62000       6e+05

5.3.2 2. Ventajas de las Keys

# Las tablas con key están ordenadas físicamente
cat("Empleados ordenados por departamento_id:\n")
#> Empleados ordenados por departamento_id:
print(emp_key[, .(emp_id, nombre, departamento_id)])
#> Key: <departamento_id>
#>     emp_id     nombre departamento_id
#>      <int>     <char>           <int>
#>  1:      5 Empleado_E               1
#>  2:      7 Empleado_G               1
#>  3:      1 Empleado_A               2
#>  4:      8 Empleado_H               2
#>  5:      9 Empleado_I               2
#> ---                                  
#> 11:      2 Empleado_B               5
#> 12:      3 Empleado_C               5
#> 13:      6 Empleado_F               5
#> 14:     10 Empleado_J               5
#> 15:     12 Empleado_L               5

cat("\nDepartamentos ordenados por dept_id:\n") 
#> 
#> Departamentos ordenados por dept_id:
print(dept_key[, .(dept_id, nombre_dept)])
#> Key: <dept_id>
#>    dept_id nombre_dept
#>      <int>      <char>
#> 1:       1  Ingeniería
#> 2:       2      Ventas
#> 3:       3   Marketing
#> 4:       4        RRHH
#> 5:       5    Finanzas

# Acceso ultra-rápido por key
departamento_2 <- dept_key[2]  # Busca dept_id == 2
empleados_dept_2 <- emp_key[2]  # Busca departamento_id == 2

cat("\nEmpleados del departamento 2:\n")
#> 
#> Empleados del departamento 2:
print(empleados_dept_2[, .(nombre, nivel, salario)])
#>        nombre  nivel salario
#>        <char> <char>   <num>
#> 1: Empleado_G   Lead   67600

5.3.3 3. Múltiples Keys

# Establecer múltiples columnas como key
setkey(asignaciones, empleado_id, proyecto_id)

# Buscar por key compuesta
asignacion_especifica <- asignaciones[.(1, 2)]  # empleado 1, proyecto 2
print(asignacion_especifica)
#> Key: <empleado_id, proyecto_id>
#>    asignacion_id empleado_id proyecto_id horas_asignadas fecha_asignacion
#>            <int>       <int>       <int>           <int>           <Date>
#> 1:            NA           1           2              NA             <NA>
#>    rol_proyecto
#>          <char>
#> 1:         <NA>

# Búsquedas parciales
asignaciones_emp_1 <- asignaciones[.(1)]  # Solo empleado 1
cat("Asignaciones del empleado 1:", nrow(asignaciones_emp_1), "proyectos\n")
#> Asignaciones del empleado 1: 3 proyectos

5.4 Sintaxis on: Joins Flexibles sin Keys

La sintaxis on permite joins puntuales sin necesidad de establecer keys.

5.4.1 1. Join Básico con on

# Join sin modificar las tablas originales
resultado_on <- empleados[departamentos, on = .(departamento_id = dept_id)]

print(resultado_on[1:6, .(nombre, nombre_dept, salario, ubicacion)])
#>        nombre nombre_dept salario ubicacion
#>        <char>      <char>   <num>    <char>
#> 1: Empleado_E  Ingeniería   46700    Madrid
#> 2: Empleado_G  Ingeniería   67600    Madrid
#> 3: Empleado_A      Ventas   39500 Barcelona
#> 4: Empleado_H      Ventas   35000 Barcelona
#> 5: Empleado_I      Ventas   35800 Barcelona
#> 6: Empleado_N      Ventas   62000 Barcelona

5.4.2 2. Join con Renombrado de Columnas

# Join con selección y renombrado
empleados_completo <- empleados[
  departamentos, 
  on = .(departamento_id = dept_id)
][
  , .(
    empleado = nombre,
    departamento = nombre_dept, 
    salario,
    presupuesto_dept = presupuesto,
    ubicacion,
    ratio_salario_presupuesto = round(salario / presupuesto * 100, 3)
  )
]

print(empleados_completo[order(-ratio_salario_presupuesto)][1:6])
#>      empleado departamento salario presupuesto_dept ubicacion
#>        <char>       <char>   <num>            <num>    <char>
#> 1: Empleado_M         RRHH   73900            3e+05  Valencia
#> 2: Empleado_D         RRHH   66900            3e+05  Valencia
#> 3: Empleado_C     Finanzas   69000            5e+05    Bilbao
#> 4: Empleado_B     Finanzas   62200            5e+05    Bilbao
#> 5: Empleado_F     Finanzas   59600            5e+05    Bilbao
#> 6: Empleado_K    Marketing   46600            4e+05   Sevilla
#>    ratio_salario_presupuesto
#>                        <num>
#> 1:                    24.633
#> 2:                    22.300
#> 3:                    13.800
#> 4:                    12.440
#> 5:                    11.920
#> 6:                    11.650

5.4.3 3. Joins con Condiciones Adicionales

# Join con filtro simultáneo
empleados_evaluados_reciente <- empleados[
  evaluaciones[fecha_evaluacion >= "2024-01-01"], 
  on = .(emp_id = empleado_id)
][
  !is.na(puntuacion)  # Solo empleados con evaluación
][
  , .(nombre, nivel, puntuacion, fecha_evaluacion)
][
  order(-puntuacion)
]

print(empleados_evaluados_reciente)
#>        nombre  nivel puntuacion fecha_evaluacion
#>        <char> <char>      <num>           <Date>
#> 1: Empleado_G   Lead        4.8       2024-02-09
#> 2: Empleado_M Senior        4.8       2024-02-09
#> 3: Empleado_O   Lead        4.6       2024-01-19
#> 4: Empleado_O   Lead        3.7       2024-02-16

5.5 Update Joins: La Característica Estrella

Los update joins permiten modificar una tabla basándose en valores de otra tabla de forma eficiente.

5.5.1 1. Update Join Básico

# Crear tabla de bonos
bonos_dept <- data.table(
  dept_id = 1:5,
  bono_porcentaje = c(0.15, 0.20, 0.12, 0.18, 0.16),
  bono_fijo = c(5000, 7000, 4000, 6000, 5500)
)

# Hacer copia para update join
emp_con_bonus <- copy(empleados)

# Update join: agregar columnas basadas en departamento
emp_con_bonus[bonos_dept, on = .(departamento_id = dept_id), 
              `:=`(
                bono_porcentaje = i.bono_porcentaje,
                bono_calculado = salario * i.bono_porcentaje + i.bono_fijo
              )]

print(emp_con_bonus[1:6, .(nombre, departamento_id, salario, bono_porcentaje, bono_calculado)])
#>        nombre departamento_id salario bono_porcentaje bono_calculado
#>        <char>           <int>   <num>           <num>          <num>
#> 1: Empleado_A               2   39500            0.20          14900
#> 2: Empleado_B               5   62200            0.16          15452
#> 3: Empleado_C               5   69000            0.16          16540
#> 4: Empleado_D               4   66900            0.18          18042
#> 5: Empleado_E               1   46700            0.15          12005
#> 6: Empleado_F               5   59600            0.16          15036

5.5.2 2. Update Join Condicional

# Update join solo para ciertos empleados
emp_con_bonus[bonos_dept, on = .(departamento_id = dept_id), 
              bono_extra := ifelse(i.bono_porcentaje > 0.15 & salario > 50000, 
                                  2000, 0)]

print(emp_con_bonus[bono_extra > 0, .(nombre, departamento_id, salario, bono_extra)])
#>        nombre departamento_id salario bono_extra
#>        <char>           <int>   <num>      <num>
#> 1: Empleado_B               5   62200       2000
#> 2: Empleado_C               5   69000       2000
#> 3: Empleado_D               4   66900       2000
#> 4: Empleado_F               5   59600       2000
#> 5: Empleado_J               5   52600       2000
#> 6: Empleado_M               4   73900       2000
#> 7: Empleado_N               2   62000       2000
#> 8: Empleado_O               2   62200       2000

5.5.3 3. Update Join con Agregaciones

# Calcular estadísticas por departamento
stats_dept <- empleados[, .(
  empleados_count = .N,
  salario_promedio_dept = round(mean(salario), 0),
  salario_max_dept = max(salario)
), by = departamento_id]

# Update join con estadísticas
emp_con_bonus[stats_dept, on = .(departamento_id), 
              `:=`(
                empleados_en_dept = i.empleados_count,
                percentil_en_dept = round(salario / i.salario_max_dept * 100, 1),
                vs_promedio_dept = salario - i.salario_promedio_dept
              )]

print(emp_con_bonus[1:8, .(nombre, departamento_id, salario, empleados_en_dept, 
                           percentil_en_dept, vs_promedio_dept)])
#>        nombre departamento_id salario empleados_en_dept percentil_en_dept
#>        <char>           <int>   <num>             <int>             <num>
#> 1: Empleado_A               2   39500                 5              63.5
#> 2: Empleado_B               5   62200                 5              90.1
#> 3: Empleado_C               5   69000                 5             100.0
#> 4: Empleado_D               4   66900                 2              90.5
#> 5: Empleado_E               1   46700                 2              69.1
#> 6: Empleado_F               5   59600                 5              86.4
#> 7: Empleado_G               1   67600                 2             100.0
#> 8: Empleado_H               2   35000                 5              56.3
#>    vs_promedio_dept
#>               <num>
#> 1:            -7400
#> 2:             5120
#> 3:            11920
#> 4:            -3500
#> 5:           -10450
#> 6:             2520
#> 7:            10450
#> 8:           -11900

5.6 Joins Múltiples y Encadenamiento

Combinando múltiples tablas en operaciones complejas.

5.6.1 1. Joins Encadenados

# Pipeline complejo: empleados -> departamentos -> proyectos -> evaluaciones
analisis_completo <- empleados[
  departamentos, on = .(departamento_id = dept_id)        # Join 1: empleados + departamentos
][
  asignaciones, on = .(emp_id = empleado_id), allow.cartesian = TRUE  # Join 2: + asignaciones
][
  proyectos, on = .(proyecto_id), allow.cartesian = TRUE   # Join 3: + proyectos  
][
  evaluaciones, on = .(emp_id = empleado_id)               # Join 4: + evaluaciones
][
  !is.na(puntuacion)                                       # Solo empleados evaluados
][
  , .(
    empleado = nombre,
    departamento = nombre_dept,
    proyecto = nombre_proyecto,
    horas_asignadas,
    rol_proyecto,
    puntuacion,
    salario,
    presupuesto_proyecto
  )
][
  order(empleado, proyecto)
]

print(head(analisis_completo, 10))
#>       empleado departamento   proyecto horas_asignadas rol_proyecto puntuacion
#>         <char>       <char>     <char>           <int>       <char>      <num>
#>  1: Empleado_B     Finanzas Proyecto_G              39        Líder        3.8
#>  2: Empleado_B     Finanzas Proyecto_G              39        Líder        4.6
#>  3: Empleado_B     Finanzas Proyecto_H              37       Tester        3.8
#>  4: Empleado_B     Finanzas Proyecto_H              37       Tester        4.6
#>  5: Empleado_B     Finanzas Proyecto_J              14       Tester        3.8
#>  6: Empleado_B     Finanzas Proyecto_J              14       Tester        4.6
#>  7: Empleado_B     Finanzas Proyecto_L              21     Analista        3.8
#>  8: Empleado_B     Finanzas Proyecto_L              21     Analista        4.6
#>  9: Empleado_C     Finanzas Proyecto_L              16        Líder        3.4
#> 10: Empleado_D         RRHH Proyecto_A              36     Analista        4.1
#>     salario presupuesto_proyecto
#>       <num>                <num>
#>  1:   62200               276000
#>  2:   62200               276000
#>  3:   62200               260000
#>  4:   62200               260000
#>  5:   62200               172000
#>  6:   62200               172000
#>  7:   62200               286000
#>  8:   62200               286000
#>  9:   69000               286000
#> 10:   66900               251000

5.6.2 2. Agregaciones Complejas con Múltiples Joins

# Análisis de productividad por departamento
productividad_dept <- empleados[
  asignaciones, on = .(emp_id = empleado_id), allow.cartesian = TRUE
][
  proyectos, on = .(proyecto_id)
][
  departamentos, on = .(departamento_id = dept_id)
][
  , .(
    empleados_únicos = uniqueN(emp_id),
    proyectos_únicos = uniqueN(proyecto_id),
    horas_totales = sum(horas_asignadas),
    presupuesto_total = sum(presupuesto_proyecto),
    proyectos_completados = sum(estado == "Completado")
  ), by = .(departamento_id, nombre_dept)
][
  , `:=`(
    horas_por_empleado = round(horas_totales / empleados_únicos, 1),
    proyectos_por_empleado = round(proyectos_únicos / empleados_únicos, 2),
    tasa_completación = round(proyectos_completados / proyectos_únicos * 100, 1)
  )
][
  order(-tasa_completación)
]

print(productividad_dept)
#>    departamento_id nombre_dept empleados_únicos proyectos_únicos horas_totales
#>              <int>      <char>            <int>            <int>         <int>
#> 1:               2      Ventas                5                6           257
#> 2:               5    Finanzas                5                6           186
#> 3:               1  Ingeniería                2                4            82
#> 4:               4        RRHH                2                2            71
#> 5:               3   Marketing                1                1            NA
#>    presupuesto_total proyectos_completados horas_por_empleado
#>                <num>                 <int>              <num>
#> 1:           1748000                     4               51.4
#> 2:           1902000                     2               37.2
#> 3:            894000                     1               41.0
#> 4:            323000                     0               35.5
#> 5:                NA                    NA                 NA
#>    proyectos_por_empleado tasa_completación
#>                     <num>             <num>
#> 1:                    1.2              66.7
#> 2:                    1.2              33.3
#> 3:                    2.0              25.0
#> 4:                    1.0               0.0
#> 5:                    1.0                NA

5.7 Casos Especiales: Joins con Diferentes Estructuras

5.7.1 1. Self-Joins: Jerarquías

# Self-join para obtener información de managers
empleados_con_manager <- empleados[
  empleados[, .(manager_emp_id = emp_id, manager_nombre = nombre, manager_nivel = nivel)], 
  on = .(manager_id = manager_emp_id)
][
  , .(empleado = nombre, nivel, salario, 
      manager = manager_nombre, manager_nivel)
][
  !is.na(manager)  # Solo empleados con manager
][
  order(manager, empleado)
]

print(empleados_con_manager)
#>       empleado  nivel salario    manager manager_nivel
#>         <char> <char>   <num>     <char>        <char>
#>  1: Empleado_B Junior   62200 Empleado_A        Senior
#>  2: Empleado_C Senior   69000 Empleado_A        Senior
#>  3:       <NA>   <NA>      NA Empleado_B        Junior
#>  4:       <NA>   <NA>      NA Empleado_C        Senior
#>  5: Empleado_E   Lead   46700 Empleado_D        Junior
#> ---                                                   
#> 14: Empleado_L Senior   42000 Empleado_K        Junior
#> 15:       <NA>   <NA>      NA Empleado_L        Senior
#> 16: Empleado_N Junior   62000 Empleado_M        Senior
#> 17: Empleado_O   Lead   62200 Empleado_N        Junior
#> 18:       <NA>   <NA>      NA Empleado_O          Lead

5.7.2 2. Many-to-Many Joins

# Análisis de empleados en múltiples proyectos
empleados_multiproyecto <- asignaciones[, .N, by = empleado_id][N > 1]

detalle_multiproyecto <- empleados[empleados_multiproyecto, on = .(emp_id = empleado_id)][
  asignaciones, on = .(emp_id = empleado_id), allow.cartesian = TRUE
][
  proyectos, on = .(proyecto_id)
][
  , .(
    empleado = nombre,
    nivel,
    proyecto = nombre_proyecto,
    horas_asignadas,
    rol_proyecto,
    estado_proyecto = estado
  )
][
  order(empleado, proyecto)
]

print(head(detalle_multiproyecto, 12))
#>       empleado  nivel   proyecto horas_asignadas  rol_proyecto estado_proyecto
#>         <char> <char>     <char>           <int>        <char>          <char>
#>  1: Empleado_A Senior Proyecto_C              34        Tester      Completado
#>  2: Empleado_A Senior Proyecto_E              34        Tester         Pausado
#>  3: Empleado_A Senior Proyecto_J              19      Analista   Planificación
#>  4: Empleado_B Junior Proyecto_G              39         Líder      Completado
#>  5: Empleado_B Junior Proyecto_H              37        Tester        En Curso
#> ---                                                                           
#>  8: Empleado_E   Lead Proyecto_G              17         Líder      Completado
#>  9: Empleado_E   Lead Proyecto_H              38         Líder        En Curso
#> 10: Empleado_E   Lead Proyecto_L              10      Analista        En Curso
#> 11: Empleado_I Junior Proyecto_C              10 Desarrollador      Completado
#> 12: Empleado_I Junior Proyecto_D              40        Tester        En Curso

# Resumen por empleado
resumen_multiproyecto <- detalle_multiproyecto[, .(
  proyectos_total = .N,
  horas_totales = sum(horas_asignadas),
  roles_únicos = uniqueN(rol_proyecto),
  proyectos_completados = sum(estado_proyecto == "Completado")
), by = .(empleado, nivel)][
  order(-horas_totales)
]

print(resumen_multiproyecto)
#>      empleado  nivel proyectos_total horas_totales roles_únicos
#>        <char> <char>           <int>         <int>        <int>
#> 1: Empleado_B Junior               4           111            3
#> 2: Empleado_A Senior               3            87            2
#> 3: Empleado_I Junior               3            72            2
#> 4: Empleado_E   Lead               3            65            2
#> 5:       <NA>   <NA>              12            NA            5
#>    proyectos_completados
#>                    <int>
#> 1:                     1
#> 2:                     1
#> 3:                     1
#> 4:                     1
#> 5:                     3

5.8 Comparación de Performance entre Métodos

# Preparar datasets para benchmark
clientes_sample <- clientes_info[sample(.N, 500)]
trans_sample <- transacciones_grandes[sample(.N, 25000)]

# Copias para diferentes métodos
clientes_key <- copy(clientes_sample)
trans_key <- copy(trans_sample)
setkey(clientes_key, cliente_id)
setkey(trans_key, cliente_id)

# Benchmark de diferentes métodos
benchmark_joins <- microbenchmark(
  "merge()" = merge(trans_sample, clientes_sample, by = "cliente_id"),
  "X[Y, on=]" = trans_sample[clientes_sample, on = .(cliente_id)],
  "setkey + X[Y]" = clientes_key[trans_key],
  times = 10
)

print(benchmark_joins)
#> Unit: milliseconds
#>           expr      min       lq     mean   median       uq      max neval
#>        merge() 3.315414 3.615093 3.624520 3.660953 3.685535 3.741128    10
#>      X[Y, on=] 2.275124 2.290552 2.324806 2.314798 2.345124 2.423731    10
#>  setkey + X[Y] 1.318099 1.620793 1.620885 1.653484 1.679794 1.695613    10

# Mostrar eficiencia relativa
benchmark_summary <- as.data.table(summary(benchmark_joins))
print(benchmark_summary[, .(expr, median, relative_speed = round(median / min(median), 2))])
#>             expr   median relative_speed
#>           <fctr>    <num>          <num>
#> 1:       merge() 3.660953           2.21
#> 2:     X[Y, on=] 2.314798           1.40
#> 3: setkey + X[Y] 1.653484           1.00

5.9 Ejercicios Prácticos

🏋️ Ejercicio 8: Pipeline de Joins Complejo

Usando las tablas disponibles, crea un análisis que:

  1. Una empleados con sus departamentos y evaluaciones
  2. Filtre solo empleados con evaluación > 4.0
  3. Agregue información de proyectos asignados
  4. Calcule métricas de productividad por departamento
  5. Genere un ranking de departamentos por performance
# Pipeline completo de joins y análisis
ranking_departamentos <- empleados[
  evaluaciones[puntuacion > 4.0], on = .(emp_id = empleado_id)  # 1. Join + filtro
][
  departamentos, on = .(departamento_id = dept_id)              # 2. Agregar info departamental
][
  asignaciones, on = .(emp_id = empleado_id), allow.cartesian = TRUE  # 3. Proyectos asignados
][
  proyectos, on = .(proyecto_id)                                # 4. Info de proyectos
][
  , .(                                                          # 5. Métricas por departamento
    empleados_alto_performance = uniqueN(emp_id),
    puntuacion_promedio = round(mean(puntuacion), 2),
    salario_promedio = round(mean(salario), 0),
    horas_totales_asignadas = sum(horas_asignadas),
    proyectos_gestionados = uniqueN(proyecto_id),
    presupuesto_total_proyectos = sum(presupuesto_proyecto),
    proyectos_completados = sum(estado == "Completado"),
    presupuesto_departamental = first(presupuesto)
  ), by = .(departamento_id, nombre_dept, ubicacion)
][
  , `:=`(                                                      # 6. Cálculos de productividad
    horas_por_empleado = round(horas_totales_asignadas / empleados_alto_performance, 1),
    proyectos_por_empleado = round(proyectos_gestionados / empleados_alto_performance, 2),
    tasa_completación = round(proyectos_completados / proyectos_gestionados * 100, 1),
    eficiencia_presupuestal = round((proyectos_completados * presupuesto_total_proyectos) / presupuesto_departamental, 2)
  )
][
  , score_performance := round(                                # 7. Score compuesto
    (puntuacion_promedio * 20) + 
    (tasa_completación * 0.5) + 
    (eficiencia_presupuestal * 10), 1
  )
][
  order(-score_performance)                                    # 8. Ranking final
][
  , ranking := 1:.N
]

print(ranking_departamentos[, .(
  ranking, nombre_dept, ubicacion, empleados_alto_performance,
  puntuacion_promedio, tasa_completación, eficiencia_presupuestal, score_performance
)])
#>    ranking nombre_dept ubicacion empleados_alto_performance puntuacion_promedio
#>      <int>      <char>    <char>                      <int>               <num>
#> 1:       1    Finanzas    Bilbao                          2                4.50
#> 2:       2      Ventas Barcelona                          2                4.33
#> 3:       3  Ingeniería    Madrid                          1                4.80
#> 4:       4        RRHH  Valencia                          2                4.45
#> 5:       5        <NA>      <NA>                          8                  NA
#>    tasa_completación eficiencia_presupuestal score_performance
#>                <num>                   <num>             <num>
#> 1:              75.0                    9.28             220.3
#> 2:              50.0                    1.21             123.7
#> 3:               0.0                    0.00              96.0
#> 4:               0.0                    0.00              89.0
#> 5:              36.4                      NA                NA

# Crear visualización del ranking
cat("\n🏆 RANKING DE DEPARTAMENTOS POR PERFORMANCE:\n")
#> 
#> 🏆 RANKING DE DEPARTAMENTOS POR PERFORMANCE:
for(i in 1:nrow(ranking_departamentos)) {
  dept <- ranking_departamentos[i]
  medal <- ifelse(i == 1, "🥇", ifelse(i == 2, "🥈", ifelse(i == 3, "🥉", paste0("#", i))))
  cat(sprintf("%s %s (%s) - Score: %.1f\n", medal, dept$nombre_dept, dept$ubicacion, dept$score_performance))
  cat(sprintf("   • %d empleados alto performance, %.1f%% completación\n", 
              dept$empleados_alto_performance, dept$tasa_completación))
}
#> 🥇 Finanzas (Bilbao) - Score: 220.3
#>    • 2 empleados alto performance, 75.0% completación
#> 🥈 Ventas (Barcelona) - Score: 123.7
#>    • 2 empleados alto performance, 50.0% completación
#> 🥉 Ingeniería (Madrid) - Score: 96.0
#>    • 1 empleados alto performance, 0.0% completación
#> #4 RRHH (Valencia) - Score: 89.0
#>    • 2 empleados alto performance, 0.0% completación
#> #5 NA (NA) - Score: NA
#>    • 8 empleados alto performance, 36.4% completación
🏋️ Ejercicio 9: Update Joins Avanzados
  1. Crea una tabla de ajustes salariales por departamento
  2. Usa update joins para aplicar los ajustes
  3. Calcula el impacto presupuestal por departamento
  4. Identifica empleados que necesitan reclasificación de nivel
# 1. Crear tabla de ajustes salariales
ajustes_salariales <- data.table(
  dept_id = 1:5,
  nombre_dept = c("Ingeniería", "Ventas", "Marketing", "RRHH", "Finanzas"),
  ajuste_porcentaje = c(0.08, 0.12, 0.06, 0.10, 0.07),  # 8%, 12%, 6%, 10%, 7%
  bono_retencion = c(3000, 5000, 2000, 2500, 4000),
  criterio_nivel = c(65000, 55000, 45000, 50000, 60000)  # Umbral para nivel Senior+
)

print("Ajustes salariales por departamento:")
#> [1] "Ajustes salariales por departamento:"
print(ajustes_salariales)
#>    dept_id nombre_dept ajuste_porcentaje bono_retencion criterio_nivel
#>      <int>      <char>             <num>          <num>          <num>
#> 1:       1  Ingeniería              0.08           3000          65000
#> 2:       2      Ventas              0.12           5000          55000
#> 3:       3   Marketing              0.06           2000          45000
#> 4:       4        RRHH              0.10           2500          50000
#> 5:       5    Finanzas              0.07           4000          60000

# 2. Aplicar ajustes usando update joins
empleados_ajuste <- copy(empleados)

# Update join principal
empleados_ajuste[ajustes_salariales, on = .(departamento_id = dept_id),
                 `:=`(
                   salario_anterior = salario,
                   ajuste_pct = i.ajuste_porcentaje,
                   salario_ajustado = salario * (1 + i.ajuste_porcentaje),
                   bono_retencion = i.bono_retencion,
                   umbral_senior = i.criterio_nivel
                 )]

# Update join condicional para bonos extra
empleados_ajuste[nivel %in% c("Lead", "Manager") & salario_ajustado > 60000, 
                 bono_extra := salario_ajustado * 0.05]
empleados_ajuste[is.na(bono_extra), bono_extra := 0]

# 3. Calcular impacto presupuestal
impacto_presupuestal <- empleados_ajuste[
  departamentos, on = .(departamento_id = dept_id)
][
  , .(
    empleados = .N,
    salario_total_anterior = sum(salario_anterior),
    salario_total_nuevo = sum(salario_ajustado),
    bonos_retencion = sum(bono_retencion),
    bonos_extra = sum(bono_extra),
    presupuesto_dept = first(presupuesto)
  ), by = .(departamento_id, nombre_dept)
][
  , `:=`(
    incremento_salarial = salario_total_nuevo - salario_total_anterior,
    costo_total_ajuste = (salario_total_nuevo - salario_total_anterior) + bonos_retencion + bonos_extra,
    impacto_presupuestal_pct = round(((salario_total_nuevo - salario_total_anterior) + bonos_retencion + bonos_extra) / presupuesto_dept * 100, 2)
  )
][
  order(-costo_total_ajuste)
]

print("\n3. Impacto presupuestal por departamento:")
#> [1] "\n3. Impacto presupuestal por departamento:"
print(impacto_presupuestal[, .(nombre_dept, empleados, incremento_salarial, 
                               costo_total_ajuste, impacto_presupuestal_pct)])
#>    nombre_dept empleados incremento_salarial costo_total_ajuste
#>         <char>     <int>               <num>              <num>
#> 1:      Ventas         5               28140            56623.2
#> 2:    Finanzas         5               19978            43166.6
#> 3:        RRHH         2               14080            19080.0
#> 4:  Ingeniería         2                9144            18794.4
#> 5:   Marketing         1                2796             4796.0
#>    impacto_presupuestal_pct
#>                       <num>
#> 1:                     9.44
#> 2:                     8.63
#> 3:                     6.36
#> 4:                     2.35
#> 5:                     1.20

# 4. Identificar empleados para reclasificación
reclasificacion <- empleados_ajuste[, .(
  nombre,
  departamento_id,
  nivel_actual = nivel,
  salario_ajustado,
  umbral_senior,
  necesita_reclasificacion = salario_ajustado >= umbral_senior & nivel == "Junior"
)][necesita_reclasificacion == TRUE]

if(nrow(reclasificacion) > 0) {
  cat("\n4. Empleados que necesitan reclasificación:\n")
  print(reclasificacion[, .(nombre, nivel_actual, salario_ajustado, umbral_senior)])
} else {
  cat("\n4. No hay empleados que requieran reclasificación inmediata.\n")
}
#> 
#> 4. Empleados que necesitan reclasificación:
#>        nombre nivel_actual salario_ajustado umbral_senior
#>        <char>       <char>            <num>         <num>
#> 1: Empleado_B       Junior            66554         60000
#> 2: Empleado_D       Junior            73590         50000
#> 3: Empleado_K       Junior            49396         45000
#> 4: Empleado_N       Junior            69440         55000

# Resumen ejecutivo
cat("\n📊 RESUMEN EJECUTIVO DEL AJUSTE SALARIAL:\n")
#> 
#> 📊 RESUMEN EJECUTIVO DEL AJUSTE SALARIAL:
cat(sprintf("• Total empleados afectados: %d\n", nrow(empleados_ajuste)))
#> • Total empleados afectados: 15
cat(sprintf("• Incremento salarial total: $%s\n", 
            format(sum(impacto_presupuestal$incremento_salarial), big.mark = ",")))
#> • Incremento salarial total: $74,138
cat(sprintf("• Costo total del ajuste: $%s\n", 
            format(sum(impacto_presupuestal$costo_total_ajuste), big.mark = ",")))
#> • Costo total del ajuste: $142,460.2
cat(sprintf("• Departamento más impactado: %s (%.2f%% del presupuesto)\n",
            impacto_presupuestal[1, nombre_dept], impacto_presupuestal[1, impacto_presupuestal_pct]))
#> • Departamento más impactado: Ventas (9.44% del presupuesto)

5.10 Mejores Prácticas para Joins

5.10.1 1. Elección del Método Adecuado

# ✅ Para joins únicos: usar "on ="
resultado <- tabla1[tabla2, on = .(key)]

# ✅ Para joins repetitivos en datos grandes: usar setkey
setkey(tabla1, key)
setkey(tabla2, key)
resultado <- tabla2[tabla1]

# ✅ Para análisis exploratorio: usar merge()
resultado <- merge(tabla1, tabla2, by = "key")

# ✅ Para modificar tabla existente: usar update join
tabla1[tabla2, on = .(key), nueva_col := i.columna]

5.10.2 2. Gestión de Memoria en Joins

# ✅ HACER: Filtrar antes de join
tabla_pequeña <- tabla_grande[filtro_importante]
resultado <- tabla_pequeña[otra_tabla, on = .(key)]

# ❌ EVITAR: Join primero, filtrar después
resultado <- tabla_grande[otra_tabla, on = .(key)][filtro_importante]

# ✅ HACER: Usar .SDcols para limitar columnas en joins complejos
resultado <- tabla1[tabla2, on = .(key), .SDcols = columnas_necesarias]

5.10.3 3. Troubleshooting de Joins

# Diagnosticar problemas de joins
cat("Claves duplicadas en tabla1:", anyDuplicated(tabla1, by = "key"), "\n")
cat("Claves faltantes:", sum(is.na(tabla1$key)), "\n")
cat("Rango de keys:", range(tabla1$key, na.rm = TRUE), "\n")

# Verificar resultado de join
cat("Filas antes del join:", nrow(tabla1), "\n")
cat("Filas después del join:", nrow(resultado), "\n")
cat("Columnas agregadas:", ncol(resultado) - ncol(tabla1), "\n")

🎯 Puntos Clave de Este Capítulo
  1. merge() es intuitivo para joins simples, setkey() es óptimo para datos grandes
  2. Sintaxis on ofrece flexibilidad sin modificar las tablas originales
  3. Update joins con := permiten modificar tablas de forma eficiente
  4. La elección del método depende del tamaño de datos y frecuencia de uso
  5. Filtrar antes de join mejora significativamente el rendimiento
  6. Always verify el resultado de joins complejos para evitar cartesian products