3  Símbolos Especiales en data.table

En este capítulo dominarás
  • .SD: El Subset of Data para operaciones por grupo
  • .SDcols: Control granular de columnas con .SD
  • .I: Índices de fila para operaciones avanzadas
  • .GRP: Identificadores de grupo únicos
  • .N: Contador universal (repaso avanzado)
  • Casos de uso complejos y patrones profesionales

3.1 El Símbolo .SD: Subset of Data

.SD es quizás el símbolo más poderoso de data.table. Contiene todas las columnas del grupo actual (excepto las variables de agrupación) como un data.table en sí mismo.

3.1.1 1. Conceptos Fundamentales de .SD

# Veamos qué contiene .SD
empleados_avanzado[, {
  cat("Grupo:", unique(departamento), "\n")
  cat("Columnas en .SD:", names(.SD), "\n")
  cat("Filas en .SD:", nrow(.SD), "\n\n")
  # Devolver algo para que funcione el data.table
  .N
}, by = departamento]
#> Grupo: Ventas 
#> Columnas en .SD: id nombre nivel salario_base bonus años_exp certificaciones proyectos_completados rating_performance fecha_ingreso activo salario_total experiencia_categoria productividad valor_empleado 
#> Filas en .SD: 4 
#> 
#> Grupo: IT 
#> Columnas en .SD: id nombre nivel salario_base bonus años_exp certificaciones proyectos_completados rating_performance fecha_ingreso activo salario_total experiencia_categoria productividad valor_empleado 
#> Filas en .SD: 4 
#> 
#> Grupo: Marketing 
#> Columnas en .SD: id nombre nivel salario_base bonus años_exp certificaciones proyectos_completados rating_performance fecha_ingreso activo salario_total experiencia_categoria productividad valor_empleado 
#> Filas en .SD: 4 
#> 
#> Grupo: RRHH 
#> Columnas en .SD: id nombre nivel salario_base bonus años_exp certificaciones proyectos_completados rating_performance fecha_ingreso activo salario_total experiencia_categoria productividad valor_empleado 
#> Filas en .SD: 4 
#> 
#> Grupo: Finanzas 
#> Columnas en .SD: id nombre nivel salario_base bonus años_exp certificaciones proyectos_completados rating_performance fecha_ingreso activo salario_total experiencia_categoria productividad valor_empleado 
#> Filas en .SD: 4
#>    departamento    V1
#>          <char> <int>
#> 1:       Ventas     4
#> 2:           IT     4
#> 3:    Marketing     4
#> 4:         RRHH     4
#> 5:     Finanzas     4
# Ejemplo práctico: estadísticas de salario por departamento
stats_salarios <- empleados_avanzado[, {
  list(
    empleados = .N,
    salario_promedio = round(mean(.SD$salario_total), 0),
    salario_mediana = round(median(.SD$salario_total), 0),
    salario_max = max(.SD$salario_total),
    salario_min = min(.SD$salario_total),
    rango_salario = max(.SD$salario_total) - min(.SD$salario_total)
  )
}, by = departamento]

print(stats_salarios)
#>    departamento empleados salario_promedio salario_mediana salario_max
#>          <char>     <int>            <num>           <num>       <num>
#> 1:       Ventas         4            52175           52750       70200
#> 2:           IT         4            52200           53350       63300
#> 3:    Marketing         4            55900           57200       60200
#> 4:         RRHH         4            51550           54100       60600
#> 5:     Finanzas         4            57875           56600       71800
#>    salario_min rango_salario
#>          <num>         <num>
#> 1:       33000         37200
#> 2:       38800         24500
#> 3:       49000         11200
#> 4:       37400         23200
#> 5:       46500         25300

3.1.2 2. Aplicar Funciones con lapply(.SD, ...)

El verdadero poder de .SD surge cuando lo combinas con lapply() para aplicar funciones a múltiples columnas:

# Aplicar mean() a todas las columnas numéricas por departamento
medias_por_dept <- empleados_avanzado[, 
  lapply(.SD, mean, na.rm = TRUE), 
  by = departamento,
  .SDcols = is.numeric  # Solo columnas numéricas
]

print(medias_por_dept)
#>    departamento    id salario_base bonus años_exp certificaciones
#>          <char> <num>        <num> <num>    <num>           <num>
#> 1:       Ventas   2.5        47475  4700    11.75            2.75
#> 2:           IT   6.5        45625  6575    11.25            1.00
#> 3:    Marketing  10.5        49075  6825     3.00            4.75
#> 4:         RRHH  14.5        44575  6975     9.25            3.25
#> 5:     Finanzas  18.5        51050  6825     4.75            1.75
#>    proyectos_completados rating_performance salario_total productividad
#>                    <num>              <num>         <num>         <num>
#> 1:                 11.25              3.850         52175        1.1900
#> 2:                 35.00              4.150         52200        3.1975
#> 3:                 43.75              4.425         55900       24.6075
#> 4:                 32.50              4.625         51550       11.7425
#> 5:                 38.00              3.675         57875       11.3625
#>    valor_empleado
#>             <num>
#> 1:       2443.035
#> 2:       2415.500
#> 3:        727.840
#> 4:       2051.960
#> 5:       1157.883
# Múltiples estadísticas por grupo usando funciones personalizadas
estadisticas_avanzadas <- empleados_avanzado[, 
  lapply(.SD, function(x) {
    if(is.numeric(x)) {
      list(
        media = round(mean(x, na.rm = TRUE), 2),
        mediana = round(median(x, na.rm = TRUE), 2),
        desv_std = round(sd(x, na.rm = TRUE), 2),
        cv = round(sd(x, na.rm = TRUE) / mean(x, na.rm = TRUE), 3)
      )
    } else {
      list(valores_unicos = length(unique(x)))
    }
  }),
  by = departamento,
  .SDcols = c("salario_total", "años_exp", "rating_performance")
]

print(estadisticas_avanzadas)
#>     departamento salario_total años_exp rating_performance
#>           <char>        <list>   <list>             <list>
#>  1:       Ventas         52175    11.75               3.85
#>  2:       Ventas         52750       12                3.7
#>  3:       Ventas      17803.44     2.99                0.7
#>  4:       Ventas         0.341    0.254              0.181
#>  5:           IT         52200    11.25               4.15
#> ---                                                       
#> 16:         RRHH         0.194     0.62              0.065
#> 17:     Finanzas         57875     4.75               3.67
#> 18:     Finanzas         56600      4.5               3.55
#> 19:     Finanzas      13198.07      2.5               0.45
#> 20:     Finanzas         0.228    0.526              0.122

3.1.3 3. Transformaciones Complejas con .SD

# Normalizar columnas dentro de cada departamento (Z-score)
empleados_normalizados <- empleados_avanzado[,
  c(.SD[, 1:2], lapply(.SD[, -(1:2)], function(x) {
    if(is.numeric(x) && length(unique(x)) > 1) {
      round((x - mean(x)) / sd(x), 3)
    } else {
      x
    }
  })),
  by = departamento
][order(departamento, id)]

# Mostrar solo algunas columnas para claridad
print(empleados_normalizados[, .(departamento, nombre, salario_total, años_exp, rating_performance)])
#>     departamento     nombre salario_total años_exp rating_performance
#>           <char>     <char>         <num>    <num>              <num>
#>  1:     Finanzas Empleado_Q        -0.862   -0.300             -0.833
#>  2:     Finanzas Empleado_R         0.654    0.100              0.056
#>  3:     Finanzas Empleado_S         1.055    1.300              1.389
#>  4:     Finanzas Empleado_T        -0.847   -1.100             -0.611
#>  5:           IT Empleado_E         0.742   -1.306             -0.691
#> ---                                                                  
#> 16:         RRHH Empleado_P        -1.416    0.479             -1.088
#> 17:       Ventas Empleado_A         1.012   -1.256              0.072
#> 18:       Ventas Empleado_B        -0.605   -0.251             -0.503
#> 19:       Ventas Empleado_C        -1.077    0.419             -0.935
#> 20:       Ventas Empleado_D         0.670    1.088              1.366

3.2 Control de Columnas con .SDcols

.SDcols te permite especificar exactamente qué columnas debe incluir .SD.

3.2.1 1. Formas de Especificar .SDcols

# Por nombres de columna
por_nombres <- empleados_avanzado[,
  lapply(.SD, mean),
  by = departamento,
  .SDcols = c("salario_total", "años_exp", "rating_performance")
]

# Por patrones
por_patrones <- empleados_avanzado[,
  lapply(.SD, max),
  by = departamento,
  .SDcols = patterns("salario|rating")
]

# Por tipo de datos
por_tipo <- empleados_avanzado[,
  lapply(.SD, function(x) length(unique(x))),
  by = departamento,
  .SDcols = is.character
]

print("Por nombres:")
#> [1] "Por nombres:"
print(por_nombres)
#>    departamento salario_total años_exp rating_performance
#>          <char>         <num>    <num>              <num>
#> 1:       Ventas         52175    11.75              3.850
#> 2:           IT         52200    11.25              4.150
#> 3:    Marketing         55900     3.00              4.425
#> 4:         RRHH         51550     9.25              4.625
#> 5:     Finanzas         57875     4.75              3.675
print("\nPor patrones:")
#> [1] "\nPor patrones:"
print(por_patrones)
#>    departamento salario_base rating_performance salario_total
#>          <char>        <num>              <num>         <num>
#> 1:       Ventas        65000                4.8         70200
#> 2:           IT        58600                4.8         63300
#> 3:    Marketing        55400                5.0         60200
#> 4:         RRHH        53300                5.0         60600
#> 5:     Finanzas        68500                4.3         71800
print("\nPor tipo (character):")
#> [1] "\nPor tipo (character):"
print(por_tipo)
#>    departamento nombre departamento nivel
#>          <char>  <int>        <int> <int>
#> 1:       Ventas      4            1     4
#> 2:           IT      4            1     4
#> 3:    Marketing      4            1     4
#> 4:         RRHH      4            1     4
#> 5:     Finanzas      4            1     4

3.2.2 2. Casos de Uso Avanzados de .SDcols

# Análisis de correlaciones por departamento
correlaciones_dept <- empleados_avanzado[activo == TRUE,  # Solo empleados activos
{
  # Seleccionar solo columnas numéricas con variabilidad
  cols_numericas <- sapply(.SD, function(x) is.numeric(x) && var(x, na.rm = TRUE) > 0)
  nombres_cols_validas <- names(cols_numericas)[cols_numericas]
  
  if(length(nombres_cols_validas) >= 2) {
    cor_matrix <- cor(.SD[, nombres_cols_validas, with = FALSE])
    # Extraer correlación específica: salario vs rating (si ambas existen)
    if("salario_total" %in% nombres_cols_validas && "rating_performance" %in% nombres_cols_validas) {
      correlacion_sal_rating <- round(cor_matrix["salario_total", "rating_performance"], 3)
    } else {
      correlacion_sal_rating <- NA
    }
    list(
      correlacion_salario_rating = correlacion_sal_rating,
      num_variables = length(nombres_cols_validas)
    )
  } else {
    list(correlacion_salario_rating = NA, num_variables = length(nombres_cols_validas))
  }
},
by = departamento,
.SDcols = is.numeric
]

print(correlaciones_dept)
#>    departamento correlacion_salario_rating num_variables
#>          <char>                      <num>         <int>
#> 1:       Ventas                      0.586            10
#> 2:           IT                     -0.399            10
#> 3:    Marketing                      0.033            10
#> 4:         RRHH                      0.845            10
#> 5:     Finanzas                      0.912            10
# Selección dinámica de columnas basada en criterios
columnas_con_variacion <- empleados_avanzado[, 
  sapply(.SD, function(x) if(is.numeric(x)) var(x, na.rm = TRUE) > 1000 else FALSE),
  .SDcols = is.numeric
]

print("Columnas con alta variación:")
#> [1] "Columnas con alta variación:"
print(names(columnas_con_variacion)[columnas_con_variacion])
#> [1] "salario_base"   "bonus"          "salario_total"  "valor_empleado"

# Usar esas columnas para análisis
analisis_alta_variacion <- empleados_avanzado[,
  lapply(.SD, function(x) c(min = min(x), max = max(x), rango = max(x) - min(x))),
  .SDcols = names(columnas_con_variacion)[columnas_con_variacion]
]

print(analisis_alta_variacion)
#>    salario_base bonus salario_total valor_empleado
#>           <num> <num>         <num>          <num>
#> 1:        30600  2400         33000          301.0
#> 2:        68500 12300         71800         4615.2
#> 3:        37900  9900         38800         4314.2

3.3 El Símbolo .I: Índices de Fila

.I contiene los índices (números de fila) de las observaciones del grupo actual en el data.table original.

3.3.1 1. Usos Básicos de .I

# Encontrar el índice del empleado con mayor salario por departamento
indices_top_salario <- empleados_avanzado[,
  .I[which.max(salario_total)],
  by = departamento
]

print("Índices de empleados con mayor salario:")
#> [1] "Índices de empleados con mayor salario:"
print(indices_top_salario)
#>    departamento    V1
#>          <char> <int>
#> 1:       Ventas     1
#> 2:           IT     7
#> 3:    Marketing     9
#> 4:         RRHH    14
#> 5:     Finanzas    19

# Usar esos índices para extraer las filas completas
top_empleados_por_dept <- empleados_avanzado[indices_top_salario$V1]
print("\nEmpleados con mayor salario por departamento:")
#> [1] "\nEmpleados con mayor salario por departamento:"
print(top_empleados_por_dept[, .(departamento, nombre, salario_total)])
#>    departamento     nombre salario_total
#>          <char>     <char>         <num>
#> 1:       Ventas Empleado_A         70200
#> 2:           IT Empleado_G         63300
#> 3:    Marketing Empleado_I         60200
#> 4:         RRHH Empleado_N         60600
#> 5:     Finanzas Empleado_S         71800

3.3.2 2. Casos de Uso Avanzados con .I

# Top N empleados por departamento
top_2_por_dept <- empleados_avanzado[,
  .I[order(-salario_total)][1:min(2, .N)],  # Top 2 o todos si hay menos de 2
  by = departamento
]

empleados_top2 <- empleados_avanzado[top_2_por_dept$V1]
print("Top 2 empleados por departamento:")
#> [1] "Top 2 empleados por departamento:"
print(empleados_top2[, .(departamento, nombre, salario_total)][order(departamento, -salario_total)])
#>     departamento     nombre salario_total
#>           <char>     <char>         <num>
#>  1:     Finanzas Empleado_S         71800
#>  2:     Finanzas Empleado_R         66500
#>  3:           IT Empleado_G         63300
#>  4:           IT Empleado_E         61000
#>  5:    Marketing Empleado_I         60200
#>  6:    Marketing Empleado_L         59600
#>  7:         RRHH Empleado_N         60600
#>  8:         RRHH Empleado_O         55600
#>  9:       Ventas Empleado_A         70200
#> 10:       Ventas Empleado_D         64100
# Muestreo estratificado usando .I
set.seed(123)
muestra_estratificada <- empleados_avanzado[,
  .I[sample(.N, size = min(2, .N))],  # 2 empleados por departamento
  by = departamento
]

empleados_muestra <- empleados_avanzado[muestra_estratificada$V1]
print("Muestra estratificada:")
#> [1] "Muestra estratificada:"
print(empleados_muestra[, .(departamento, nombre, años_exp)])
#>     departamento     nombre años_exp
#>           <char>     <char>    <int>
#>  1:       Ventas Empleado_C       13
#>  2:       Ventas Empleado_D       15
#>  3:           IT Empleado_G       12
#>  4:           IT Empleado_F       11
#>  5:    Marketing Empleado_K        2
#>  6:    Marketing Empleado_J        7
#>  7:         RRHH Empleado_N        1
#>  8:         RRHH Empleado_P       12
#>  9:     Finanzas Empleado_S        8
#> 10:     Finanzas Empleado_Q        4

3.4 El Símbolo .GRP: Identificador de Grupo

.GRP es un contador que asigna un número único e incremental a cada grupo.

3.4.1 1. Usos de .GRP

# Asignar ID único a cada departamento
empleados_con_grupo <- empleados_avanzado[,
  .(nombre, departamento, grupo_id = .GRP, empleados_en_grupo = .N),
  by = departamento
]

print(empleados_con_grupo)
#>     departamento     nombre departamento grupo_id empleados_en_grupo
#>           <char>     <char>       <char>    <int>              <int>
#>  1:       Ventas Empleado_A       Ventas        1                  4
#>  2:       Ventas Empleado_B       Ventas        1                  4
#>  3:       Ventas Empleado_C       Ventas        1                  4
#>  4:       Ventas Empleado_D       Ventas        1                  4
#>  5:           IT Empleado_E           IT        2                  4
#> ---                                                                 
#> 16:         RRHH Empleado_P         RRHH        4                  4
#> 17:     Finanzas Empleado_Q     Finanzas        5                  4
#> 18:     Finanzas Empleado_R     Finanzas        5                  4
#> 19:     Finanzas Empleado_S     Finanzas        5                  4
#> 20:     Finanzas Empleado_T     Finanzas        5                  4
# Análisis de transacciones por grupo de cliente
analisis_grupos <- transacciones[,
  .(
    grupo_cliente = .GRP,
    num_transacciones = .N,
    monto_promedio = round(mean(monto_final), 2),
    primera_compra = min(fecha),
    ultima_compra = max(fecha)
  ),
  by = tipo_cliente
]

print(analisis_grupos)
#>    tipo_cliente grupo_cliente num_transacciones monto_promedio primera_compra
#>          <char>         <int>             <int>          <num>         <Date>
#> 1:        Basic             1               502         223.58     2023-01-01
#> 2:      Regular             2               297         207.99     2023-01-02
#> 3:      Premium             3               201         219.04     2023-01-02
#>    ultima_compra
#>           <Date>
#> 1:    2023-12-31
#> 2:    2023-12-31
#> 3:    2023-12-30

3.5 Combinando Símbolos: Casos de Uso Profesionales

3.5.1 1. Análisis de Cohortes

# Análisis de cohortes de empleados por año de ingreso
library(lubridate)

analisis_cohortes <- empleados_avanzado[,
  .(
    cohorte_id = .GRP,
    año_ingreso = year(min(fecha_ingreso)),
    empleados_iniciales = .N,
    salario_inicial_promedio = round(mean(salario_base), 0),
    retencion_actual = round(mean(activo) * 100, 1),
    performance_promedio = round(mean(rating_performance), 2),
    # Usando .SD para métricas adicionales
    experiencia_rango = paste0(min(.SD$años_exp), "-", max(.SD$años_exp), " años")
  ),
  by = .(año_cohorte = year(fecha_ingreso)),
  .SDcols = "años_exp"
][order(año_cohorte)]

print(analisis_cohortes)
#>    año_cohorte cohorte_id año_ingreso empleados_iniciales
#>          <num>      <int>       <num>               <int>
#> 1:        2015          1        2015                   1
#> 2:        2016          7        2016                   4
#> 3:        2017          8        2017                   2
#> 4:        2018          3        2018                   3
#> 5:        2019          6        2019                   1
#> 6:        2021          2        2021                   3
#> 7:        2022          5        2022                   3
#> 8:        2023          4        2023                   3
#>    salario_inicial_promedio retencion_actual performance_promedio
#>                       <num>            <num>                <num>
#> 1:                    65000            100.0                 3.90
#> 2:                    50875            100.0                 4.30
#> 3:                    50550            100.0                 4.10
#> 4:                    36267             66.7                 4.00
#> 5:                    58600            100.0                 4.30
#> 6:                    43533            100.0                 4.40
#> 7:                    51567            100.0                 3.60
#> 8:                    42967            100.0                 4.43
#>    experiencia_rango
#>               <char>
#> 1:          8-8 años
#> 2:         1-14 años
#> 3:          2-2 años
#> 4:         7-13 años
#> 5:        12-12 años
#> 6:         1-11 años
#> 7:         4-10 años
#> 8:        11-15 años

3.5.2 2. Detección de Outliers por Grupo

# Detectar outliers en salario dentro de cada departamento
outliers_salario <- empleados_avanzado[,
{
  Q1 <- quantile(salario_total, 0.25)
  Q3 <- quantile(salario_total, 0.75)
  IQR <- Q3 - Q1
  limite_inferior <- Q1 - 1.5 * IQR
  limite_superior <- Q3 + 1.5 * IQR
  
  outliers_indices <- .I[salario_total < limite_inferior | salario_total > limite_superior]
  
  list(
    departamento = unique(departamento),
    num_outliers = length(outliers_indices),
    outliers_ids = if(length(outliers_indices) > 0) list(outliers_indices) else list(integer(0)),
    limite_inf = round(limite_inferior, 0),
    limite_sup = round(limite_superior, 0)
  )
},
by = departamento
]

print(outliers_salario)
#>    departamento departamento num_outliers outliers_ids limite_inf limite_sup
#>          <char>       <char>        <int>       <list>      <num>      <num>
#> 1:       Ventas       Ventas            0                    -188     105112
#> 2:           IT           IT            0                   17575      87975
#> 3:    Marketing    Marketing            0                   43750      69350
#> 4:         RRHH         RRHH            0                   36725      68925
#> 5:     Finanzas     Finanzas            0                   14888      99588

# Extraer los outliers reales
outliers_reales <- unique(unlist(outliers_salario$outliers_ids))
if(length(outliers_reales) > 0) {
  empleados_outliers <- empleados_avanzado[outliers_reales]
  print("\nEmpleados con salarios outliers:")
  print(empleados_outliers[, .(nombre, departamento, salario_total)])
}

3.5.3 3. Ventana Móvil con .SD

# Análisis de ventana móvil en transacciones
# Primero ordenamos por fecha
transacciones_ordenadas <- transacciones[order(fecha)]

# Crear grupos por mes para simular ventana temporal
ventana_movil <- transacciones_ordenadas[,
{
  # Para cada mes, calcular métricas usando .SD
  current_data <- .SD
  list(
    mes = unique(mes),
    transacciones_mes = .N,
    monto_total = sum(monto_final),
    ticket_promedio = round(mean(monto_final), 2),
    # Diversidad de productos
    productos_unicos = uniqueN(producto_id),
    # Análisis de canales usando .SD
    canal_dominante = names(sort(table(.SD$canal), decreasing = TRUE))[1],
    concentracion_clientes = round(1 - (uniqueN(cliente_id) / .N), 3)  # Índice de concentración
  )
},
by = .(año = year(fecha), mes),
.SDcols = c("monto_final", "producto_id", "canal", "cliente_id")
][order(año, mes)]

print(ventana_movil)
#>       año   mes   mes transacciones_mes monto_total ticket_promedio
#>     <num> <int> <int>             <int>       <num>           <num>
#>  1:  2023     1     1                86    17489.98          203.37
#>  2:  2023     2     2                86    18022.41          209.56
#>  3:  2023     3     3                73    14743.75          201.97
#>  4:  2023     4     4                87    18146.44          208.58
#>  5:  2023     5     5                86    17393.11          202.25
#> ---                                                                
#>  8:  2023     8     8                87    18408.98          211.60
#>  9:  2023     9     9                72    17707.42          245.94
#> 10:  2023    10    10                88    19787.56          224.86
#> 11:  2023    11    11                84    18717.37          222.83
#> 12:  2023    12    12                90    20800.00          231.11
#>     productos_unicos canal_dominante concentracion_clientes
#>                <int>          <char>                  <num>
#>  1:               10          Online                  0.337
#>  2:               10          Online                  0.326
#>  3:               10          Online                  0.315
#>  4:               10          Online                  0.253
#>  5:               10          Online                  0.395
#> ---                                                        
#>  8:               10          Online                  0.322
#>  9:               10          Online                  0.375
#> 10:               10          Online                  0.273
#> 11:               10          Online                  0.274
#> 12:               10          Online                  0.267

3.6 Ejercicios Prácticos

🏋️ Ejercicio 5: Análisis Multidimensional con Símbolos Especiales

Usando el dataset transacciones, crea un análisis que utilice todos los símbolos especiales:

  1. Con .SD: Calcula estadísticas de monto por categoría y canal
  2. Con .SDcols: Analiza solo columnas que contengan “monto” o “descuento”
  3. Con .I: Encuentra las 3 mejores transacciones por categoría
  4. Con .GRP: Asigna IDs únicos a combinaciones categoría-canal
  5. Análisis combinado: Crea un reporte complejo que use múltiples símbolos
# 1. Estadísticas con .SD por categoría y canal
estadisticas_SD <- transacciones[,
  lapply(.SD, function(x) {
    if(is.numeric(x)) {
      list(
        promedio = round(mean(x, na.rm = TRUE), 2),
        mediana = round(median(x, na.rm = TRUE), 2),
        desv_std = round(sd(x, na.rm = TRUE), 2)
      )
    } else {
      list(valores_unicos = length(unique(x)))
    }
  }),
  by = .(categoria, canal),
  .SDcols = c("monto", "monto_final", "descuento")
]

print("1. Estadísticas por categoría y canal:")
#> [1] "1. Estadísticas por categoría y canal:"
print(head(estadisticas_SD, 8))
#>      categoria  canal  monto monto_final descuento
#>         <char> <char> <list>      <list>    <list>
#> 1:      Sports Online 262.85      224.49      0.15
#> 2:      Sports Online 251.83      219.81      0.15
#> 3:      Sports Online    133      116.98      0.09
#> 4: Electronics Mobile  236.1      198.43      0.15
#> 5: Electronics Mobile 191.67      152.54      0.14
#> 6: Electronics Mobile 157.27      130.66      0.09
#> 7:       Books Mobile 248.05      210.83      0.16
#> 8:       Books Mobile 244.29       208.4      0.17

# 2. Análisis con .SDcols específicas
analisis_montos <- transacciones[,
  lapply(.SD, function(x) c(
    min = min(x),
    max = max(x),
    rango = max(x) - min(x),
    coef_var = sd(x) / mean(x)
  )),
  by = categoria,
  .SDcols = patterns("monto|descuento")
]

print("\n2. Análisis de montos y descuentos:")
#> [1] "\n2. Análisis de montos y descuentos:"
print(analisis_montos)
#>       categoria       monto descuento monto_final
#>          <char>       <num>     <num>       <num>
#>  1:      Sports  13.6900000 0.0000000  11.4996000
#>  2:      Sports 498.1800000 0.3000000 465.4980000
#>  3:      Sports 484.4900000 0.3000000 453.9984000
#>  4:      Sports   0.5180114 0.5658237   0.5377436
#>  5: Electronics  12.2600000 0.0000000  10.4796000
#> ---                                              
#> 12:       Books   0.5671143 0.6141504   0.5912270
#> 13:    Clothing  10.1700000 0.0000000   9.2547000
#> 14:    Clothing 497.7800000 0.3000000 489.6243000
#> 15:    Clothing 487.6100000 0.3000000 480.3696000
#> 16:    Clothing   0.5622603 0.5388315   0.5733788

# 3. Top 3 transacciones por categoría usando .I
indices_top3 <- transacciones[,
  .I[order(-monto_final)][1:min(3, .N)],
  by = categoria
]

top3_transacciones <- transacciones[indices_top3$V1]
print("\n3. Top 3 transacciones por categoría:")
#> [1] "\n3. Top 3 transacciones por categoría:"
print(top3_transacciones[, .(categoria, transaction_id, monto_final, producto_id)][order(categoria, -monto_final)])
#>       categoria transaction_id monto_final producto_id
#>          <char>          <int>       <num>      <char>
#>  1:       Books            235    480.2000           D
#>  2:       Books            826    479.8431           C
#>  3:       Books            545    475.1424           F
#>  4:    Clothing            304    489.6243           E
#>  5:    Clothing            718    486.0300           I
#> ---                                                   
#>  8: Electronics            225    477.9190           E
#>  9: Electronics            636    476.8132           C
#> 10:      Sports            148    465.4980           E
#> 11:      Sports            776    465.4642           H
#> 12:      Sports             73    462.3894           C

# 4. IDs únicos con .GRP
grupos_categoria_canal <- transacciones[,
  .(
    grupo_id = .GRP,
    transacciones = .N,
    monto_promedio = round(mean(monto_final), 2)
  ),
  by = .(categoria, canal)
][order(grupo_id)]

print("\n4. IDs de grupos categoría-canal:")
#> [1] "\n4. IDs de grupos categoría-canal:"
print(grupos_categoria_canal)
#>       categoria  canal grupo_id transacciones monto_promedio
#>          <char> <char>    <int>         <int>          <num>
#>  1:      Sports Online        1           125         224.49
#>  2: Electronics Mobile        2            49         198.43
#>  3:       Books Mobile        3            47         210.83
#>  4: Electronics Online        4           125         228.96
#>  5: Electronics  Store        5            77         196.52
#> ---                                                         
#>  8:       Books Online        8           129         222.99
#>  9:    Clothing  Store        9            85         200.10
#> 10:      Sports Mobile       10            48         237.55
#> 11:      Sports  Store       11            76         222.88
#> 12:    Clothing Mobile       12            41         225.89

# 5. Análisis combinado ultra-avanzado
analisis_completo <- transacciones[,
{
  # Usar todos los símbolos en un análisis complejo
  grupo_id <- .GRP
  num_trans <- .N
  
  # Estadísticas básicas con .SD
  stats_basicas <- lapply(.SD[, .(monto_final, descuento)], function(x) {
    c(media = mean(x), mediana = median(x))
  })
  
  # Top performer con .I
  top_transaction_idx <- .I[which.max(monto_final)]
  
  # Análisis de clientes
  clientes_unicos <- uniqueN(cliente_id)
  cliente_top <- cliente_id[which.max(monto_final)]
  
  list(
    grupo_id = grupo_id,
    categoria = unique(categoria),
    canal = unique(canal),
    num_transacciones = num_trans,
    monto_promedio = round(stats_basicas$monto_final["media"], 2),
    monto_mediana = round(stats_basicas$monto_final["mediana"], 2),
    descuento_promedio = round(stats_basicas$descuento["media"], 3),
    clientes_unicos = clientes_unicos,
    concentracion = round(1 - (clientes_unicos / num_trans), 3),
    top_transaction_id = transacciones[top_transaction_idx, transaction_id],
    top_cliente_id = cliente_top,
    diversidad_productos = uniqueN(producto_id)
  )
},
by = .(categoria, canal),
.SDcols = c("monto_final", "descuento", "cliente_id", "producto_id")
][order(-monto_promedio)]

print("\n5. Análisis completo combinando todos los símbolos:")
#> [1] "\n5. Análisis completo combinando todos los símbolos:"
print(analisis_completo)
#>       categoria  canal grupo_id   categoria  canal num_transacciones
#>          <char> <char>    <int>      <char> <char>             <int>
#>  1:      Sports Mobile       10      Sports Mobile                48
#>  2: Electronics Online        4 Electronics Online               125
#>  3:    Clothing Mobile       12    Clothing Mobile                41
#>  4:      Sports Online        1      Sports Online               125
#>  5:       Books  Store        7       Books  Store                77
#> ---                                                                 
#>  8:    Clothing Online        6    Clothing Online               121
#>  9:       Books Mobile        3       Books Mobile                47
#> 10:    Clothing  Store        9    Clothing  Store                85
#> 11: Electronics Mobile        2 Electronics Mobile                49
#> 12: Electronics  Store        5 Electronics  Store                77
#>     monto_promedio monto_mediana descuento_promedio clientes_unicos
#>              <num>         <num>              <num>           <int>
#>  1:         237.55        272.55              0.166              39
#>  2:         228.96        235.17              0.155              77
#>  3:         225.89        244.68              0.149              34
#>  4:         224.49        219.81              0.150              76
#>  5:         223.28        214.69              0.133              54
#> ---                                                                
#>  8:         215.08        216.92              0.160              66
#>  9:         210.83        208.40              0.164              35
#> 10:         200.10        176.29              0.155              57
#> 11:         198.43        152.54              0.153              33
#> 12:         196.52        185.82              0.139              58
#>     concentracion top_transaction_id top_cliente_id diversidad_productos
#>             <num>              <int>          <int>                <int>
#>  1:         0.188                776             88                   10
#>  2:         0.384                 74             74                   10
#>  3:         0.171                780             76                   10
#>  4:         0.392                148             53                   10
#>  5:         0.299                242             57                   10
#> ---                                                                     
#>  8:         0.455                304             41                   10
#>  9:         0.255                545             88                   10
#> 10:         0.329                718             50                   10
#> 11:         0.327                636             98                   10
#> 12:         0.247                788             52                   10

# Mostrar tabla final formateada para PDF
knitr::kable(
  analisis_completo,
  caption = "Análisis Completo por Categoría y Canal",
  digits = 2,
  format.args = list(big.mark = ",")
)
Análisis Completo por Categoría y Canal
categoria canal grupo_id categoria canal num_transacciones monto_promedio monto_mediana descuento_promedio clientes_unicos concentracion top_transaction_id top_cliente_id diversidad_productos
Sports Mobile 10 Sports Mobile 48 237.55 272.55 0.17 39 0.19 776 88 10
Electronics Online 4 Electronics Online 125 228.96 235.17 0.16 77 0.38 74 74 10
Clothing Mobile 12 Clothing Mobile 41 225.89 244.68 0.15 34 0.17 780 76 10
Sports Online 1 Sports Online 125 224.49 219.81 0.15 76 0.39 148 53 10
Books Store 7 Books Store 77 223.28 214.69 0.13 54 0.30 242 57 10
Books Online 8 Books Online 129 222.99 205.71 0.14 69 0.47 235 99 10
Sports Store 11 Sports Store 76 222.88 227.40 0.16 52 0.32 73 54 10
Clothing Online 6 Clothing Online 121 215.08 216.92 0.16 66 0.46 304 41 10
Books Mobile 3 Books Mobile 47 210.83 208.40 0.16 35 0.26 545 88 10
Clothing Store 9 Clothing Store 85 200.10 176.29 0.16 57 0.33 718 50 10
Electronics Mobile 2 Electronics Mobile 49 198.43 152.54 0.15 33 0.33 636 98 10
Electronics Store 5 Electronics Store 77 196.52 185.82 0.14 58 0.25 788 52 10

3.7 Patrones Avanzados y Mejores Prácticas

3.7.1 1. Funciones Personalizadas con .SD

# Crear función personalizada para análisis estadístico
analisis_estadistico <- function(dt, by_vars, numeric_cols) {
  dt[,
    lapply(.SD, function(x) {
      if(is.numeric(x) && length(unique(x)) > 1) {
        list(
          n = length(x),
          media = round(mean(x, na.rm = TRUE), 2),
          mediana = round(median(x, na.rm = TRUE), 2),
          q25 = round(quantile(x, 0.25, na.rm = TRUE), 2),
          q75 = round(quantile(x, 0.75, na.rm = TRUE), 2),
          desv_std = round(sd(x, na.rm = TRUE), 2),
          cv = round(sd(x, na.rm = TRUE) / mean(x, na.rm = TRUE), 3),
          asimetria = round((mean(x) - median(x)) / sd(x), 3)
        )
      } else {
        list(valores_unicos = length(unique(x)))
      }
    }),
    by = by_vars,
    .SDcols = numeric_cols
  ]
}

# Aplicar la función
resultado_personalizado <- analisis_estadistico(
  empleados_avanzado,
  by_vars = "departamento",
  numeric_cols = c("salario_total", "años_exp", "rating_performance")
)

print(resultado_personalizado)
#>     departamento salario_total años_exp rating_performance
#>           <char>        <list>   <list>             <list>
#>  1:       Ventas             4        4                  4
#>  2:       Ventas         52175    11.75               3.85
#>  3:       Ventas         52750       12                3.7
#>  4:       Ventas         39300    10.25               3.42
#>  5:       Ventas         65625     13.5               4.12
#> ---                                                       
#> 36:     Finanzas         46650      3.5               3.38
#> 37:     Finanzas         67825     5.75               3.85
#> 38:     Finanzas      13198.07      2.5               0.45
#> 39:     Finanzas         0.228    0.526              0.122
#> 40:     Finanzas         0.097      0.1              0.278

3.7.2 2. Optimización de Performance

# ✅ HACER: Usar .SDcols para limitar columnas
# Más eficiente
empleados[, lapply(.SD, mean), by = dept, .SDcols = c("sal", "exp")]

# ❌ NO HACER: Procesar todas las columnas innecesariamente
# Menos eficiente
empleados[, lapply(.SD, mean), by = dept]

# ✅ HACER: Combinar operaciones en una sola expresión
# Más eficiente
empleados[, {
  list(
    media_sal = mean(salario),
    max_exp = max(años_exp),
    grupo_id = .GRP
  )
}, by = dept]

# ❌ NO HACER: Múltiples pasadas por los datos
# Menos eficiente
media_sal <- empleados[, mean(salario), by = dept]
max_exp <- empleados[, max(años_exp), by = dept]

🎯 Puntos Clave de Este Capítulo
  1. .SD es un mini-data.table con los datos del grupo actual - úsalo con lapply() para operaciones múltiples
  2. .SDcols controla qué columnas incluye .SD - esencial para performance y precisión
  3. .I te da los índices reales - perfecto para top-N, muestreo y filtrado avanzado
  4. .GRP asigna IDs únicos a grupos - útil para tracking y análisis de cohortes
  5. Combinar símbolos permite análisis complejos en una sola expresión
  6. Performance: Limita .SDcols y combina operaciones para máxima eficiencia

Con el dominio de estos símbolos especiales, tienes las herramientas para realizar análisis de datos sofisticados y eficientes. En el próximo módulo exploraremos técnicas de manipulación intermedia como encadenamiento y joins.