10  Buenas Prácticas y Código Idiomático

En este capítulo dominarás
  • Los Mandamientos de data.table: Do’s y Don’ts definitivos
  • Patrones de código idiomático para diferentes escenarios
  • Debugging y troubleshooting de código complejo
  • Estilos de código y convenciones de la comunidad
  • Testing y validación de código data.table

10.1 Los Mandamientos de data.table

10.1.1QUÉ HACER (Do’s)

1. Usa := para Modificaciones Eficientes

El operador := es el corazón de la eficiencia en data.table. Modifica por referencia sin copiar toda la tabla.

# ✅ CORRECTO: Modificación por referencia
employees_dt[, annual_bonus := salary * 0.1]

# ✅ CORRECTO: Múltiples columnas a la vez
employees_dt[, `:=`(
  salary_tier = fifelse(salary > 100000, "High", 
                fifelse(salary > 70000, "Medium", "Low")),
  tenure_years = as.numeric(Sys.Date() - hire_date) / 365.25
)]

# ✅ CORRECTO: Modificación condicional
employees_dt[department == "Engineering", tech_bonus := salary * 0.05]

# Mostrar resultado
head(employees_dt[, .(employee_id, department, salary, salary_tier, annual_bonus, tech_bonus)])
#>    employee_id  department salary salary_tier annual_bonus tech_bonus
#>          <int>      <char>  <num>      <char>        <num>      <num>
#> 1:           1 Engineering 128722        High      12872.2     6436.1
#> 2:           2     Finance 128148        High      12814.8         NA
#> 3:           3     Finance  50533         Low       5053.3         NA
#> 4:           4          HR  41787         Low       4178.7         NA
#> 5:           5       Sales  79771      Medium       7977.1         NA
#> 6:           6     Finance 138060        High      13806.0         NA
# Comparar con método ineficiente
employees_copy <- copy(employees_dt[1:1000])

# ❌ INCORRECTO: Crear copias innecesarias
timing_inefficient <- system.time({
  employees_copy <- employees_copy[, .(employee_id, department, salary, 
                                      new_column = salary * 1.1)]
})

# ✅ CORRECTO: Modificación por referencia
employees_ref <- copy(employees_dt[1:1000])
timing_efficient <- system.time({
  employees_ref[, new_column := salary * 1.1]
})

cat("Método ineficiente:", round(timing_inefficient[3], 4), "segundos\n")
#> Método ineficiente: 0.001 segundos
cat("Método eficiente:", round(timing_efficient[3], 4), "segundos\n")
#> Método eficiente: 0.001 segundos
cat("Mejora:", round(timing_inefficient[3] / timing_efficient[3], 1), "x más rápido\n")
#> Mejora: 1 x más rápido

2. Utiliza setkey() para Joins y Filtros Repetitivos

Cuando vas a hacer múltiples operaciones sobre las mismas columnas, setkey() paga con creces la inversión inicial.

# Crear copias para comparar
trans_no_key <- copy(transactions_dt[1:10000])
trans_with_key <- copy(transactions_dt[1:10000])

# Establecer key
setkey(trans_with_key, customer_id, transaction_date)

# ✅ CORRECTO: Consultas rápidas con key
customers_target <- c(1, 50, 100, 500, 1000)

# Sin key
time_no_key <- system.time({
  result1 <- trans_no_key[customer_id %in% customers_target & 
                         transaction_date >= as.Date("2024-01-01")]
})

# Con key  
time_with_key <- system.time({
  # Usar sintaxis de key para máxima eficiencia
  result2 <- trans_with_key[.(customers_target, 
                             seq(as.Date("2024-01-01"), as.Date("2024-12-31"), by = "day"))]
})

cat("Sin key:", round(time_no_key[3], 4), "segundos\n")
#> Sin key: 0 segundos
cat("Con key:", round(time_with_key[3], 4), "segundos\n")
#> Con key: 0.001 segundos
cat("Speedup:", round(time_no_key[3] / time_with_key[3], 1), "x\n")
#> Speedup: 0 x

3. Aprovecha .SD para Operaciones Múltiples

.SD (Subset of Data) te permite aplicar funciones a múltiples columnas de manera elegante.

# ✅ CORRECTO: Usar .SD para múltiples columnas
numeric_summary <- employees_dt[, lapply(.SD, function(x) {
  list(mean = mean(x, na.rm = TRUE),
       median = median(x, na.rm = TRUE),
       q95 = quantile(x, 0.95, na.rm = TRUE))
}), .SDcols = is.numeric, by = department]

print(head(numeric_summary))
#>     department employee_id   salary performance_score manager_id annual_bonus
#>         <char>      <list>   <list>            <list>     <list>       <list>
#> 1: Engineering    25366.05 95310.06                 3   499.7326     9531.006
#> 2: Engineering       25715    95316                 3      499.5       9531.6
#> 3: Engineering       47551 144724.9                 4     950.95     14472.49
#> 4:     Finance    24716.19 94993.49           3.01034   503.8386     9499.349
#> 5:     Finance       24506    95204                 3        507       9520.4
#> 6:     Finance     47432.4 144319.8                 5        951     14431.98
#>    tenure_years tech_bonus
#>          <list>     <list>
#> 1:     5.616922   4765.503
#> 2:     5.620808     4765.8
#> 3:     10.12266   7236.245
#> 4:     5.664987        NaN
#> 5:     5.670089         NA
#> 6:     10.18754         NA

# ✅ CORRECTO: .SD con transformaciones complejas
employees_standardized <- employees_dt[, c(.SD[, .(employee_id, department)], 
                                          lapply(.SD, function(x) scale(x)[,1])), 
                                      .SDcols = is.numeric]

print(head(employees_standardized[, .(employee_id, department, salary, performance_score)]))
#>    employee_id  department    salary performance_score
#>          <int>      <char>     <num>             <num>
#> 1:           1 Engineering  1.061269      -0.002022701
#> 2:           2     Finance  1.043194      -0.002022701
#> 3:           3     Finance -1.400920      -0.002022701
#> 4:           4          HR -1.676334      -0.002022701
#> 5:           5       Sales -0.480209      -0.002022701
#> 6:           6     Finance  1.355325      -0.002022701

4. Usa Vectorización en Lugar de Bucles

Las operaciones vectorizadas son siempre más rápidas y más legibles.

# ✅ CORRECTO: Operaciones vectorizadas
transactions_dt[, transaction_quarter := paste0("Q", ceiling(month(transaction_date)/3), 
                                               "_", year(transaction_date))]

# ✅ CORRECTO: Condicionales vectorizadas con fifelse
transactions_dt[, amount_category := fifelse(
  amount > 100, "High",
  fifelse(amount > 50, "Medium", "Low")
)]

# ✅ CORRECTO: Uso de %between% para rangos
employees_dt[, mid_career := salary %between% c(60000, 120000)]

# Mostrar resultados
print(head(transactions_dt[, .(transaction_id, amount, amount_category, transaction_quarter)]))
#>    transaction_id amount amount_category transaction_quarter
#>             <int>  <num>          <char>              <char>
#> 1:              1  19.01             Low             Q3_2024
#> 2:              2  28.39             Low             Q4_2024
#> 3:              3  41.68             Low             Q4_2023
#> 4:              4  10.01             Low             Q3_2023
#> 5:              5  29.42             Low             Q1_2023
#> 6:              6  38.53             Low             Q3_2023
print(head(employees_dt[, .(employee_id, salary, mid_career)]))
#>    employee_id salary mid_career
#>          <int>  <num>     <lgcl>
#> 1:           1 128722      FALSE
#> 2:           2 128148      FALSE
#> 3:           3  50533      FALSE
#> 4:           4  41787      FALSE
#> 5:           5  79771       TRUE
#> 6:           6 138060      FALSE

5. Utiliza Encadenamiento para Operaciones Complejas

El encadenamiento DT[...][...] es más eficiente que variables intermedias.

# ✅ CORRECTO: Encadenamiento eficiente
high_performers <- employees_dt[
  performance_score >= 4 & tenure_years >= 2
][
  , .(avg_salary = mean(salary), 
      count = .N,
      avg_tenure = mean(tenure_years)), 
  by = department
][
  order(-avg_salary)
]

print(high_performers)
#>     department avg_salary count avg_tenure
#>         <char>      <num> <int>      <num>
#> 1:       Sales   95508.85  1735   6.272262
#> 2:   Marketing   95241.65  1680   6.295960
#> 3:     Finance   95124.27  1766   6.336704
#> 4: Engineering   94705.74  1697   6.234244
#> 5:          HR   94161.02  1647   6.259084

# ✅ CORRECTO: Encadenamiento con modificaciones
top_departments <- transactions_dt[
  transaction_date >= as.Date("2024-01-01")
][
  , total_revenue := sum(amount), by = store_location
][
  total_revenue > 10000
][
  order(-total_revenue)
]

print(head(top_departments[, .(store_location, total_revenue)]))
#>    store_location total_revenue
#>            <char>         <num>
#> 1:        Store_P      132929.2
#> 2:        Store_P      132929.2
#> 3:        Store_P      132929.2
#> 4:        Store_P      132929.2
#> 5:        Store_P      132929.2
#> 6:        Store_P      132929.2

10.1.2QUÉ NO HACER (Don’ts)

1. No Uses Bucles for con data.table

Los bucles explícitos destruyen todas las optimizaciones de data.table.

# Crear dataset pequeño para la demostración
sample_trans <- transactions_dt[sample(.N, 1000)]

# ❌ INCORRECTO: Bucle ineficiente
calculate_inefficient <- function(dt) {
  result <- copy(dt)
  for(i in 1:nrow(result)) {
    result[i, profit_margin := amount[i] * 0.2]
  }
  return(result)
}

# ✅ CORRECTO: Operación vectorizada
calculate_efficient <- function(dt) {
  result <- copy(dt)
  result[, profit_margin := amount * 0.2]
  return(result)
}

# Comparar tiempos
time_inefficient <- system.time(result_bad <- calculate_inefficient(sample_trans))
time_efficient <- system.time(result_good <- calculate_efficient(sample_trans))

cat("Método con bucle:", round(time_inefficient[3], 4), "segundos\n")
#> Método con bucle: 0.3 segundos
cat("Método vectorizado:", round(time_efficient[3], 4), "segundos\n")
#> Método vectorizado: 0 segundos
cat("Mejora:", round(time_inefficient[3] / time_efficient[3], 1), "x más rápido\n")
#> Mejora: Inf x más rápido

# Verificar que los resultados son idénticos
cat("Resultados idénticos:", identical(result_bad$profit_margin, result_good$profit_margin), "\n")
#> Resultados idénticos: FALSE

2. No Mezcles dplyr con data.table sin Cuidado

Mixing paradigmas puede causar copias inesperadas y pérdida de performance.

# ❌ PROBLEMÁTICO: Puede forzar copias y perder optimizaciones
library(dplyr)
employees_dt %>% 
  mutate(new_salary = salary * 1.1) %>% 
  filter(department == "Engineering") %>%
  arrange(desc(salary))

# ✅ CORRECTO: Sintaxis data.table pura
employees_dt[, new_salary := salary * 1.1][
  department == "Engineering"
][order(-salary)]

# ✅ ALTERNATIVA: dtplyr para sintaxis dplyr + performance data.table
library(dtplyr)
employees_dt %>% 
  lazy_dt() %>%
  mutate(new_salary = salary * 1.1) %>% 
  filter(department == "Engineering") %>%
  arrange(desc(salary)) %>%
  as.data.table()

3. No Ignores la Gestión de Memoria

Crear copias innecesarias puede agotar la memoria rápidamente.

# Demostrar el problema con copias
demo_dt <- employees_dt[1:1000]

# ❌ INCORRECTO: Crear múltiples copias
measure_memory_waste <- function() {
  copy1 <- copy(demo_dt)
  copy2 <- copy(demo_dt)
  copy3 <- copy(demo_dt)
  
  # Modificaciones que podrían haberse hecho por referencia
  copy1[, bonus1 := salary * 0.1]
  copy2[, bonus2 := salary * 0.15]
  copy3[, bonus3 := salary * 0.2]
  
  return(list(copy1, copy2, copy3))
}

# ✅ CORRECTO: Trabajar con referencias
measure_memory_efficient <- function() {
  working_dt <- demo_dt  # Solo una referencia
  
  # Todas las modificaciones por referencia
  working_dt[, `:=`(
    bonus1 = salary * 0.1,
    bonus2 = salary * 0.15, 
    bonus3 = salary * 0.2
  )]
  
  return(working_dt)
}

# Nota: En este ejemplo, usamos copias pequeñas para demostrar el concepto
# sin consumir mucha memoria en el tutorial
memory_waste <- object.size(measure_memory_waste())
memory_efficient <- object.size(measure_memory_efficient())

cat("Enfoque con múltiples copias:", format(memory_waste, units = "KB"), "\n")
#> Enfoque con múltiples copias: 247.1 Kb
cat("Enfoque eficiente:", format(memory_efficient, units = "KB"), "\n")
#> Enfoque eficiente: 98.3 Kb

4. No Uses rbind() Repetitivo

Construir tablas fila por fila es extremadamente ineficiente.

# ❌ INCORRECTO: rbind repetitivo (simulado para evitar demora)
build_table_bad <- function(n) {
  result <- data.table()
  # Simulamos solo algunas iteraciones para el ejemplo
  for(i in 1:min(n, 50)) {  # Limitamos a 50 para el ejemplo
    new_row <- data.table(
      id = i, 
      value = rnorm(1),
      category = sample(LETTERS[1:3], 1)
    )
    result <- rbind(result, new_row)
  }
  return(result)
}

# ✅ CORRECTO: Crear toda la tabla de una vez
build_table_good <- function(n) {
  data.table(
    id = 1:n,
    value = rnorm(n),
    category = sample(LETTERS[1:3], n, replace = TRUE)
  )
}

# Comparar tiempos
n_rows <- 50  # Pequeño para el ejemplo
time_bad <- system.time(table_bad <- build_table_bad(n_rows))
time_good <- system.time(table_good <- build_table_good(n_rows))

cat("Método rbind repetitivo:", round(time_bad[3], 4), "segundos\n")
#> Método rbind repetitivo: 0.013 segundos
cat("Método eficiente:", round(time_good[3], 4), "segundos\n")
#> Método eficiente: 0 segundos
cat("Diferencia:", round(time_bad[3] / time_good[3], 1), "x más lento\n")
#> Diferencia: Inf x más lento

# Para tablas grandes, la diferencia sería dramática
cat("\nNota: Para 10,000 filas, el método rbind puede ser 100-1000x más lento\n")
#> 
#> Nota: Para 10,000 filas, el método rbind puede ser 100-1000x más lento

10.2 Patrones de Código Idiomático

10.2.1 1. Análisis Exploratorio de Datos

# Patrón: Resumen rápido de todas las variables numéricas
eda_summary <- employees_dt[, lapply(.SD, function(x) {
  if(is.numeric(x)) {
    list(
      count = sum(!is.na(x)),
      mean = round(mean(x, na.rm = TRUE), 2),
      median = round(median(x, na.rm = TRUE), 2),
      min = min(x, na.rm = TRUE),
      max = max(x, na.rm = TRUE),
      missing = sum(is.na(x))
    )
  }
}), .SDcols = is.numeric]

print(eda_summary)
#>    employee_id   salary performance_score manager_id annual_bonus tenure_years
#>         <list>   <list>            <list>     <list>       <list>       <list>
#> 1:       50000    50000             50000      49022        50000        50000
#> 2:     25000.5 95020.46                 3     500.86      9502.05         5.65
#> 3:     25000.5  94991.5                 3        501      9499.15         5.65
#> 4:           1    40013                 1          1       4001.3    0.6379192
#> 5:       50000   149999                 5       1000      14999.9     10.63655
#> 6:           0        0                 0        978            0            0
#>    tech_bonus
#>        <list>
#> 1:      10095
#> 2:     4765.5
#> 3:     4765.8
#> 4:    2000.65
#> 5:     7499.9
#> 6:      39905

# Patrón: Distribución de variables categóricas
categorical_summary <- employees_dt[, .(
  count = .N,
  avg_salary = round(mean(salary), 0),
  median_performance = median(as.numeric(performance_score))
), by = .(department, remote_work)][order(department, -avg_salary)]

print(categorical_summary)
#>      department remote_work count avg_salary median_performance
#>          <char>      <lgcl> <int>      <num>              <num>
#>  1: Engineering       FALSE  7016      95520                  3
#>  2: Engineering        TRUE  3079      94832                  3
#>  3:     Finance        TRUE  2952      95463                  3
#>  4:     Finance       FALSE  6913      94793                  3
#>  5:          HR        TRUE  2993      94883                  3
#>  6:          HR       FALSE  6990      94690                  3
#>  7:   Marketing       FALSE  7034      95350                  3
#>  8:   Marketing        TRUE  3016      93386                  3
#>  9:       Sales        TRUE  2971      95904                  3
#> 10:       Sales       FALSE  7036      95028                  3

10.2.2 2. Limpieza y Validación de Datos

# Patrón: Identificar y manejar outliers
identify_outliers <- function(dt, column) {
  Q1 <- quantile(dt[[column]], 0.25, na.rm = TRUE)
  Q3 <- quantile(dt[[column]], 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  lower_bound <- Q1 - 1.5 * IQR
  upper_bound <- Q3 + 1.5 * IQR
  
  dt[, paste0(column, "_outlier") := get(column) < lower_bound | get(column) > upper_bound]
  
  return(dt[get(paste0(column, "_outlier")) == TRUE])
}

# Identificar outliers en salarios
salary_outliers <- identify_outliers(copy(employees_dt), "salary")
cat("Outliers en salarios encontrados:", nrow(salary_outliers), "\n")
#> Outliers en salarios encontrados: 0
print(head(salary_outliers[, .(employee_id, department, salary)]))
#> Empty data.table (0 rows and 3 cols): employee_id,department,salary

# Patrón: Validación de integridad de datos
data_quality_check <- employees_dt[, .(
  total_records = .N,
  missing_salary = sum(is.na(salary)),
  invalid_performance = sum(performance_score < 1 | performance_score > 5, na.rm = TRUE),
  future_hire_dates = sum(hire_date > Sys.Date(), na.rm = TRUE),
  negative_salaries = sum(salary < 0, na.rm = TRUE)
)]

print(data_quality_check)
#>    total_records missing_salary invalid_performance future_hire_dates
#>            <int>          <int>               <int>             <int>
#> 1:         50000              0                   0                 0
#>    negative_salaries
#>                <int>
#> 1:                 0

10.2.3 3. Análisis de Series Temporales

# Patrón: Agregaciones temporales con rolling windows
# Crear datos temporales
daily_sales <- transactions_dt[, .(
  daily_revenue = sum(amount),
  transaction_count = .N
), by = transaction_date][order(transaction_date)]

# Rolling average de 7 días
daily_sales[, `:=`(
  revenue_7day_avg = frollmean(daily_revenue, 7, align = "right"),
  revenue_7day_sum = frollsum(daily_revenue, 7, align = "right"),
  growth_rate = (daily_revenue / data.table::shift(daily_revenue, 1) - 1) * 100
)]

print(head(daily_sales[!is.na(revenue_7day_avg)], 10))
#>     transaction_date daily_revenue transaction_count revenue_7day_avg
#>               <Date>         <num>             <int>            <num>
#>  1:       2023-01-07       7600.28               161         6830.914
#>  2:       2023-01-08       6939.91               144         6860.439
#>  3:       2023-01-09       8018.96               140         6919.359
#>  4:       2023-01-10       6010.28               130         6963.241
#>  5:       2023-01-11       8311.52               152         7188.080
#>  6:       2023-01-12       7070.97               148         7351.023
#>  7:       2023-01-13       6851.72               125         7257.663
#>  8:       2023-01-14       7485.59               137         7241.279
#>  9:       2023-01-15       7499.29               140         7321.190
#> 10:       2023-01-16       6335.47               120         7080.691
#>     revenue_7day_sum growth_rate
#>                <num>       <num>
#>  1:         47816.40   1.2663153
#>  2:         48023.07  -8.6887588
#>  3:         48435.51  15.5484725
#>  4:         48742.69 -25.0491336
#>  5:         50316.56  38.2883992
#>  6:         51457.16 -14.9256694
#>  7:         50803.64  -3.1007061
#>  8:         50688.95   9.2512537
#>  9:         51248.33   0.1830183
#> 10:         49564.84 -15.5190691

# Patrón: Análisis de tendencias por período
monthly_trends <- transactions_dt[, .(
  total_revenue = sum(amount),
  avg_transaction = round(mean(amount), 2),
  transaction_count = .N
), by = .(year = year(transaction_date), month = month(transaction_date))][
  order(year, month)
][, `:=`(
  revenue_growth = (total_revenue / data.table::shift(total_revenue, 1) - 1) * 100,
  period = paste0(year, "-", sprintf("%02d", month))
)]

print(head(monthly_trends, 12))
#>      year month total_revenue avg_transaction transaction_count revenue_growth
#>     <int> <int>         <num>           <num>             <int>          <num>
#>  1:  2023     1      219815.3           51.12              4300             NA
#>  2:  2023     2      199300.1           51.70              3855      -9.332936
#>  3:  2023     3      218209.1           50.59              4313       9.487719
#>  4:  2023     4      200160.0           49.56              4039      -8.271484
#>  5:  2023     5      214969.5           50.27              4276       7.398852
#> ---                                                                           
#>  8:  2023     8      214542.1           51.12              4197       4.867881
#>  9:  2023     9      209143.9           51.16              4088      -2.516172
#> 10:  2023    10      214762.0           50.66              4239       2.686223
#> 11:  2023    11      194004.0           47.25              4106      -9.665572
#> 12:  2023    12      213822.5           49.96              4280      10.215517
#>      period
#>      <char>
#>  1: 2023-01
#>  2: 2023-02
#>  3: 2023-03
#>  4: 2023-04
#>  5: 2023-05
#> ---        
#>  8: 2023-08
#>  9: 2023-09
#> 10: 2023-10
#> 11: 2023-11
#> 12: 2023-12

10.2.4 4. Análisis de Cohortes

# Patrón: Análisis de cohorte de empleados por año de contratación
cohort_analysis <- employees_dt[, hire_year := year(hire_date)][, .(
  cohort_size = .N,
  avg_current_salary = round(mean(salary), 0),
  avg_performance = round(mean(performance_score), 2),
  retention_rate = round(.N / employees_dt[year(hire_date) == hire_year, .N] * 100, 1)
), by = hire_year][order(hire_year)]

print(cohort_analysis)
#>     hire_year cohort_size avg_current_salary avg_performance retention_rate
#>         <int>       <int>              <num>           <num>          <num>
#>  1:      2015        5054              94757            2.98           10.1
#>  2:      2016        5018              95581            2.99           10.0
#>  3:      2017        5061              94834            2.99           10.1
#>  4:      2018        4906              94943            3.00            9.8
#>  5:      2019        5010              95127            3.00           10.0
#>  6:      2020        5086              94636            3.00           10.2
#>  7:      2021        4927              95138            3.02            9.9
#>  8:      2022        4929              94619            3.02            9.9
#>  9:      2023        4975              95381            3.00           10.0
#> 10:      2024        5034              95193            3.02           10.1

# Patrón: Segmentación de clientes por comportamiento
customer_segmentation <- transactions_dt[, .(
  total_spent = sum(amount),
  transaction_frequency = .N,
  avg_transaction = round(mean(amount), 2),
  days_active = as.numeric(max(transaction_date) - min(transaction_date)) + 1,
  favorite_category = names(sort(table(product_category), decreasing = TRUE))[1]
), by = customer_id][, `:=`(
  spending_tier = cut(total_spent, 
                     breaks = quantile(total_spent, c(0, 0.33, 0.66, 1)), 
                     labels = c("Low", "Medium", "High"),
                     include.lowest = TRUE),
  frequency_tier = cut(transaction_frequency,
                      breaks = quantile(transaction_frequency, c(0, 0.5, 1)),
                      labels = c("Occasional", "Frequent"),
                      include.lowest = TRUE)
)]

# Resumen de segmentación
segment_summary <- customer_segmentation[, .(
  customers = .N,
  avg_total_spent = round(mean(total_spent), 2),
  avg_frequency = round(mean(transaction_frequency), 1)
), by = .(spending_tier, frequency_tier)]

print(segment_summary)
#>    spending_tier frequency_tier customers avg_total_spent avg_frequency
#>           <fctr>         <fctr>     <int>           <num>         <num>
#> 1:           Low     Occasional      2984          264.30           7.0
#> 2:        Medium     Occasional      1958          460.58           8.6
#> 3:           Low       Frequent       316          321.93          11.6
#> 4:          High       Frequent      2525          770.54          13.5
#> 5:          High     Occasional       875          679.31           9.1
#> 6:        Medium       Frequent      1341          485.92          12.3

10.3 Debugging y Troubleshooting

10.3.1 1. Herramientas de Diagnóstico

# Función para inspeccionar comprehensivamente un data.table
inspect_dt <- function(dt, name = "data.table") {
  cat("=== Inspección de", name, "===\n")
  cat("Dimensiones:", nrow(dt), "x", ncol(dt), "\n")
  cat("Memoria:", format(object.size(dt), units = "MB"), "\n")
  cat("Key:", ifelse(is.null(key(dt)), "Ninguna", paste(key(dt), collapse = ", ")), "\n")
  cat("Índices:", length(indices(dt)), "\n")
  if(length(indices(dt)) > 0) {
    cat("Índices disponibles:\n")
    for(idx in indices(dt)) {
      cat("  -", paste(idx, collapse = ", "), "\n")
    }
  }
  cat("Clases de columnas:\n")
  col_classes <- sapply(dt, function(x) paste(class(x), collapse = ", "))
  for(i in seq_along(col_classes)) {
    cat("  ", names(col_classes)[i], ":", col_classes[i], "\n")
  }
  cat("Valores faltantes por columna:\n")
  missing_counts <- dt[, lapply(.SD, function(x) sum(is.na(x)))]
  for(i in seq_along(missing_counts)) {
    cat("  ", names(missing_counts)[i], ":", missing_counts[[i]], "\n")
  }
  cat("\n")
}

# Inspeccionar nuestros datasets principales
inspect_dt(employees_dt[1:1000], "employees_dt (muestra)")
#> === Inspección de employees_dt (muestra) ===
#> Dimensiones: 1000 x 13 
#> Memoria: 0.1 Mb 
#> Key: Ninguna 
#> Índices: 0 
#> Clases de columnas:
#>    employee_id : integer 
#>    department : character 
#>    salary : numeric 
#>    hire_date : Date 
#>    performance_score : integer 
#>    remote_work : logical 
#>    manager_id : integer 
#>    annual_bonus : numeric 
#>    salary_tier : character 
#>    tenure_years : numeric 
#>    tech_bonus : numeric 
#>    mid_career : logical 
#>    hire_year : integer 
#> Valores faltantes por columna:
#>    employee_id : 0 
#>    department : 0 
#>    salary : 0 
#>    hire_date : 0 
#>    performance_score : 0 
#>    remote_work : 0 
#>    manager_id : 25 
#>    annual_bonus : 0 
#>    salary_tier : 0 
#>    tenure_years : 0 
#>    tech_bonus : 805 
#>    mid_career : 0 
#>    hire_year : 0

10.3.2 2. Debugging de Operaciones Complejas

# Función para debuggear operaciones paso a paso
debug_complex_operation <- function(dt, verbose = TRUE) {
  if(verbose) cat("Paso 1: Filtrado inicial\n")
  step1 <- dt[salary > 70000 & !is.na(performance_score)]
  if(verbose) cat("  Filas después del filtro:", nrow(step1), "\n")
  
  if(verbose) cat("Paso 2: Cálculos por grupo\n")
  step2 <- step1[, .(
    avg_salary = mean(salary),
    avg_performance = mean(performance_score),
    count = .N,
    salary_std = sd(salary)
  ), by = department]
  if(verbose) cat("  Grupos creados:", nrow(step2), "\n")
  
  if(verbose) cat("Paso 3: Filtrado post-agregación\n")
  step3 <- step2[count >= 10]  # Solo departamentos con suficientes empleados
  if(verbose) cat("  Grupos finales:", nrow(step3), "\n")
  
  if(verbose) cat("Paso 4: Ordenamiento final\n")
  result <- step3[order(-avg_salary)]
  
  return(result)
}

# Ejecutar con debugging
result_debug <- debug_complex_operation(employees_dt, verbose = TRUE)
#> Paso 1: Filtrado inicial
#>   Filas después del filtro: 36411 
#> Paso 2: Cálculos por grupo
#>   Grupos creados: 5 
#> Paso 3: Filtrado post-agregación
#>   Grupos finales: 5 
#> Paso 4: Ordenamiento final
print(result_debug)
#>     department avg_salary avg_performance count salary_std
#>         <char>      <num>           <num> <int>      <num>
#> 1: Engineering   110479.1        2.988536  7327   23043.35
#> 2:       Sales   110280.8        3.002877  7300   23149.18
#> 3:     Finance   109922.3        3.013630  7190   22984.42
#> 4:   Marketing   109620.3        2.989902  7328   23162.78
#> 5:          HR   109554.0        3.007982  7266   23207.83

10.3.3 3. Validación y Testing

# Función para validar resultados de operaciones
validate_operation <- function(original_dt, result_dt, operation_name) {
  cat("=== Validación de", operation_name, "===\n")
  
  # Verificar que no se perdieron datos inesperadamente
  if("by" %in% names(attributes(result_dt))) {
    cat("Operación de agregación detectada\n")
  } else {
    rows_ratio <- nrow(result_dt) / nrow(original_dt)
    cat("Ratio de filas resultado/original:", round(rows_ratio, 3), "\n")
    if(rows_ratio > 1) {
      cat("⚠️ ADVERTENCIA: El resultado tiene más filas que el original\n")
    }
  }
  
  # Verificar valores faltantes
  original_na <- original_dt[, lapply(.SD, function(x) sum(is.na(x)))]
  result_na <- result_dt[, lapply(.SD, function(x) sum(is.na(x)))]
  
  cat("NAs en original:", sum(unlist(original_na)), "\n")
  cat("NAs en resultado:", sum(unlist(result_na)), "\n")
  
  # Verificar tipos de datos
  original_types <- sapply(original_dt, class)
  result_types <- sapply(result_dt, class)
  
  common_cols <- intersect(names(original_types), names(result_types))
  type_changes <- sapply(common_cols, function(col) {
    !identical(original_types[[col]], result_types[[col]])
  })
  
  if(any(type_changes)) {
    cat("⚠️ ADVERTENCIA: Cambios de tipo detectados en columnas:", 
        paste(names(type_changes)[type_changes], collapse = ", "), "\n")
  } else {
    cat("✅ Tipos de datos preservados correctamente\n")
  }
  
  cat("\n")
}

# Ejemplo de validación
sample_employees <- employees_dt[1:1000]
filtered_result <- sample_employees[salary > 80000]
validate_operation(sample_employees, filtered_result, "filtrado por salario")
#> === Validación de filtrado por salario ===
#> Ratio de filas resultado/original: 0.629 
#> NAs en original: 830 
#> NAs en resultado: 515 
#> ✅ Tipos de datos preservados correctamente

aggregated_result <- sample_employees[, .(avg_salary = mean(salary)), by = department]
validate_operation(sample_employees, aggregated_result, "agregación por departamento")
#> === Validación de agregación por departamento ===
#> Ratio de filas resultado/original: 0.005 
#> NAs en original: 830 
#> NAs en resultado: 0 
#> ✅ Tipos de datos preservados correctamente

10.4 Estilo de Código y Convenciones

10.4.1 1. Naming Conventions

# ✅ CORRECTO: Nombres descriptivos
customer_lifetime_value <- transactions_dt[, .(
  total_revenue = sum(amount),
  avg_order_value = mean(amount),
  transaction_count = .N
), by = customer_id]

# ✅ CORRECTO: Consistencia en naming
dt_sales_daily <- transactions_dt[, .(daily_revenue = sum(amount)), by = transaction_date]
dt_sales_monthly <- transactions_dt[, .(monthly_revenue = sum(amount)), 
                                   by = .(year = year(transaction_date), 
                                          month = month(transaction_date))]

# ✅ CORRECTO: Prefijos para variables temporales
tmp_high_value_customers <- customer_lifetime_value[total_revenue > 1000]
temp_analysis_result <- tmp_high_value_customers[, .N, by = .(revenue_tier = cut(total_revenue, 3))]

10.4.2 2. Formateo y Organización

# ✅ CORRECTO: Formateo claro para operaciones complejas
complex_analysis <- employees_dt[
  # Filtros principales
  salary > 50000 & 
  !is.na(performance_score) & 
  tenure_years >= 1,
  
  # Cálculos
  .(
    employee_count = .N,
    avg_salary = round(mean(salary), 0),
    median_salary = round(median(salary), 0),
    salary_range = max(salary) - min(salary),
    top_performer_ratio = sum(performance_score >= 4) / .N,
    remote_work_ratio = sum(remote_work, na.rm = TRUE) / .N
  ),
  
  # Agrupación
  by = .(
    department,
    salary_tier = cut(salary, 
                     breaks = c(0, 60000, 90000, Inf), 
                     labels = c("Entry", "Mid", "Senior"))
  )
][
  # Post-procesamiento
  employee_count >= 5  # Solo grupos con suficientes empleados
][
  # Ordenamiento
  order(department, -avg_salary)
]

print(head(complex_analysis))
#>     department salary_tier employee_count avg_salary median_salary salary_range
#>         <char>      <fctr>          <int>      <num>         <num>        <num>
#> 1: Engineering      Senior           5368     120081        120132        59994
#> 2: Engineering         Mid           2598      74729         74684        29969
#> 3: Engineering       Entry            910      54994         55008         9979
#> 4:     Finance      Senior           5223     119707        119357        59994
#> 5:     Finance         Mid           2529      74926         74889        29994
#> 6:     Finance       Entry            891      54939         54765         9961
#>    top_performer_ratio remote_work_ratio
#>                  <num>             <num>
#> 1:           0.1905738         0.2997392
#> 2:           0.1989992         0.2998460
#> 3:           0.2054945         0.3186813
#> 4:           0.2042887         0.3034654
#> 5:           0.2119415         0.3013049
#> 6:           0.1818182         0.2962963

10.4.3 3. Documentación y Comentarios

# Función bien documentada para análisis de retención
analyze_employee_retention <- function(employees_dt, analysis_date = Sys.Date()) {
  #' Analiza patrones de retención de empleados
  #' 
  #' @param employees_dt data.table con datos de empleados
  #' @param analysis_date Fecha de referencia para el análisis
  #' @return data.table con métricas de retención por departamento
  
  # Calcular métricas base
  employees_dt[, `:=`(
    tenure_years = as.numeric(analysis_date - hire_date) / 365.25,
    is_long_tenure = tenure_years >= 3
  )]
  
  # Análisis de retención por departamento
  retention_analysis <- employees_dt[
    !is.na(tenure_years),
    .(
      total_employees = .N,
      avg_tenure = round(mean(tenure_years), 2),
      retention_3_year = sum(is_long_tenure) / .N,
      avg_salary_retained = mean(salary[is_long_tenure]),
      avg_salary_new = mean(salary[!is_long_tenure])
    ),
    by = department
  ][
    order(-retention_3_year)
  ]
  
  return(retention_analysis)
}

# Uso de la función
retention_results <- analyze_employee_retention(employees_dt)
print(retention_results)
#>     department total_employees avg_tenure retention_3_year avg_salary_retained
#>         <char>           <int>      <num>            <num>               <num>
#> 1:     Finance            9865       5.66        0.7691840            94876.98
#> 2:          HR            9983       5.64        0.7668036            94738.28
#> 3:   Marketing           10050       5.69        0.7661692            94675.73
#> 4: Engineering           10095       5.62        0.7630510            95434.67
#> 5:       Sales           10007       5.63        0.7553712            95281.11
#>    avg_salary_new
#>             <num>
#> 1:       95381.75
#> 2:       94779.52
#> 3:       95038.42
#> 4:       94908.80
#> 5:       95308.60

10.5 Testing y Validación

10.5.1 1. Unit Tests para Funciones data.table

# Función simple para testing
calculate_employee_bonus <- function(salary, performance_score, department) {
  base_bonus <- salary * 0.1
  performance_multiplier <- performance_score / 3
  department_bonus <- fifelse(department == "Sales", salary * 0.05, 0)
  
  return(base_bonus * performance_multiplier + department_bonus)
}

# Tests básicos
test_bonus_calculation <- function() {
  # Test 1: Cálculo básico
  test_salary <- 100000
  test_performance <- 3
  test_dept <- "Engineering"
  
  expected_bonus <- (100000 * 0.1) * (3/3) + 0  # 10000
  actual_bonus <- calculate_employee_bonus(test_salary, test_performance, test_dept)
  
  cat("Test 1 - Cálculo básico:", 
      ifelse(abs(actual_bonus - expected_bonus) < 0.01, "✅ PASS", "❌ FAIL"), "\n")
  
  # Test 2: Bonus de ventas
  test_dept_sales <- "Sales"
  expected_bonus_sales <- (100000 * 0.1) * (3/3) + (100000 * 0.05)  # 15000
  actual_bonus_sales <- calculate_employee_bonus(test_salary, test_performance, test_dept_sales)
  
  cat("Test 2 - Bonus de ventas:", 
      ifelse(abs(actual_bonus_sales - expected_bonus_sales) < 0.01, "✅ PASS", "❌ FAIL"), "\n")
  
  # Test 3: Performance alto
  test_performance_high <- 5
  expected_bonus_high <- (100000 * 0.1) * (5/3) + 0  # 16666.67
  actual_bonus_high <- calculate_employee_bonus(test_salary, test_performance_high, test_dept)
  
  cat("Test 3 - Performance alto:", 
      ifelse(abs(actual_bonus_high - expected_bonus_high) < 1, "✅ PASS", "❌ FAIL"), "\n")
}

# Ejecutar tests
test_bonus_calculation()
#> Test 1 - Cálculo básico: ✅ PASS 
#> Test 2 - Bonus de ventas: ✅ PASS 
#> Test 3 - Performance alto: ✅ PASS

# Aplicar a datos reales
employees_dt[, calculated_bonus := calculate_employee_bonus(salary, performance_score, department)]
print(head(employees_dt[, .(employee_id, department, salary, performance_score, calculated_bonus)]))
#>    employee_id  department salary performance_score calculated_bonus
#>          <int>      <char>  <num>             <int>            <num>
#> 1:           1 Engineering 128722                 3         12872.20
#> 2:           2     Finance 128148                 3         12814.80
#> 3:           3     Finance  50533                 3          5053.30
#> 4:           4          HR  41787                 3          4178.70
#> 5:           5       Sales  79771                 3         11965.65
#> 6:           6     Finance 138060                 3         13806.00

10.5.2 2. Validation de Integridad de Datos

# Suite completa de validación
validate_data_integrity <- function(dt, table_name = "data.table") {
  cat("=== Validación de Integridad:", table_name, "===\n")
  
  validation_results <- list()
  
  # 1. Verificar duplicados en ID
  if("employee_id" %in% names(dt)) {
    duplicate_ids <- dt[, .N, by = employee_id][N > 1]
    validation_results$duplicate_ids <- nrow(duplicate_ids)
    cat("IDs duplicados:", nrow(duplicate_ids), 
        ifelse(nrow(duplicate_ids) == 0, "✅", "❌"), "\n")
  }
  
  # 2. Verificar rangos válidos
  if("salary" %in% names(dt)) {
    invalid_salaries <- dt[salary < 0 | salary > 1000000, .N]
    validation_results$invalid_salaries <- invalid_salaries
    cat("Salarios inválidos:", invalid_salaries, 
        ifelse(invalid_salaries == 0, "✅", "❌"), "\n")
  }
  
  if("performance_score" %in% names(dt)) {
    invalid_performance <- dt[performance_score < 1 | performance_score > 5, .N]
    validation_results$invalid_performance <- invalid_performance
    cat("Scores de performance inválidos:", invalid_performance,
        ifelse(invalid_performance == 0, "✅", "❌"), "\n")
  }
  
  # 3. Verificar fechas
  if("hire_date" %in% names(dt)) {
    future_dates <- dt[hire_date > Sys.Date(), .N]
    very_old_dates <- dt[hire_date < as.Date("1950-01-01"), .N]
    validation_results$future_dates <- future_dates
    validation_results$very_old_dates <- very_old_dates
    cat("Fechas futuras:", future_dates, ifelse(future_dates == 0, "✅", "❌"), "\n")
    cat("Fechas muy antiguas:", very_old_dates, ifelse(very_old_dates == 0, "✅", "❌"), "\n")
  }
  
  # 4. Verificar consistencia referencial
  if(all(c("manager_id", "employee_id") %in% names(dt))) {
    orphan_managers <- dt[!is.na(manager_id) & !manager_id %in% employee_id, .N]
    validation_results$orphan_managers <- orphan_managers
    cat("Managers inexistentes:", orphan_managers, 
        ifelse(orphan_managers == 0, "✅", "❌"), "\n")
  }
  
  # Resumen
  total_issues <- sum(unlist(validation_results))
  cat("\nResumen: Total de issues encontrados:", total_issues, 
      ifelse(total_issues == 0, "✅ Datos válidos", "❌ Requiere atención"), "\n\n")
  
  return(validation_results)
}

# Ejecutar validación
integrity_results <- validate_data_integrity(employees_dt, "employees_dt")
#> === Validación de Integridad: employees_dt ===
#> IDs duplicados: 0 ✅ 
#> Salarios inválidos: 0 ✅ 
#> Scores de performance inválidos: 0 ✅ 
#> Fechas futuras: 0 ✅ 
#> Fechas muy antiguas: 0 ✅ 
#> Managers inexistentes: 0 ✅ 
#> 
#> Resumen: Total de issues encontrados: 0 ✅ Datos válidos

10.6 Ejercicio Final: Refactoring de Código

🛠️ Ejercicio 9: Refactoring Completo

Toma el siguiente código mal escrito y refactorízalo aplicando todas las buenas prácticas:

# CÓDIGO MALO para refactorizar
bad_analysis <- function(data) {
  result <- data.frame()
  
  for(dept in unique(data$department)) {
    dept_data <- data[data$department == dept, ]
    
    for(year in 2020:2024) {
      year_data <- dept_data[as.numeric(format(dept_data$hire_date, "%Y")) == year, ]
      
      if(nrow(year_data) > 0) {
        avg_sal <- mean(year_data$salary)
        count <- nrow(year_data)
        high_performers <- nrow(year_data[year_data$performance_score >= 4, ])
        
        new_row <- data.frame(
          department = dept,
          hire_year = year,
          avg_salary = avg_sal,
          employee_count = count,
          high_performer_count = high_performers,
          high_performer_rate = high_performers / count
        )
        
        result <- rbind(result, new_row)
      }
    }
  }
  
  return(result)
}
# CÓDIGO REFACTORIZADO aplicando buenas prácticas
good_analysis <- function(employees_dt) {
  #' Analiza empleados por departamento y año de contratación
  #' @param employees_dt data.table con datos de empleados
  #' @return data.table con análisis agregado
  
  # Validación de entrada
  required_cols <- c("department", "hire_date", "salary", "performance_score")
  if(!all(required_cols %in% names(employees_dt))) {
    stop("Faltan columnas requeridas: ", 
         paste(setdiff(required_cols, names(employees_dt)), collapse = ", "))
  }
  
  # Una sola operación vectorizada que reemplaza todos los bucles
  result <- employees_dt[
    # Filtro para años de interés
    year(hire_date) %between% c(2020, 2024),
    
    # Cálculos agregados
    .(
      avg_salary = round(mean(salary, na.rm = TRUE), 0),
      employee_count = .N,
      high_performer_count = sum(performance_score >= 4, na.rm = TRUE),
      median_salary = median(salary, na.rm = TRUE),
      salary_std = round(sd(salary, na.rm = TRUE), 0)
    ),
    
    # Agrupación
    by = .(department, hire_year = year(hire_date))
  ][
    # Cálculos derivados
    , high_performer_rate := round(high_performer_count / employee_count, 3)
  ][
    # Filtrar grupos pequeños
    employee_count >= 3
  ][
    # Ordenamiento lógico
    order(department, hire_year)
  ]
  
  return(result)
}

# Comparar rendimiento
sample_employees <- employees_dt[sample(.N, 5000)]

# Tiempo del método refactorizado
tiempo_bueno <- system.time({
  resultado_bueno <- good_analysis(sample_employees)
})

cat("Método refactorizado:", round(tiempo_bueno[3], 4), "segundos\n")
#> Método refactorizado: 0.013 segundos
cat("Filas resultado:", nrow(resultado_bueno), "\n")
#> Filas resultado: 25

print(head(resultado_bueno))
#>     department hire_year avg_salary employee_count high_performer_count
#>         <char>     <int>      <num>          <int>                <int>
#> 1: Engineering      2020      95376            102                   21
#> 2: Engineering      2021      92345             87                   24
#> 3: Engineering      2022      98022            104                   23
#> 4: Engineering      2023      87590            110                   17
#> 5: Engineering      2024      91134             95                   13
#> 6:     Finance      2020      95926            110                   16
#>    median_salary salary_std high_performer_rate
#>            <num>      <num>               <num>
#> 1:       94355.5      30045               0.206
#> 2:       86740.0      35390               0.276
#> 3:      102976.5      31567               0.221
#> 4:       86547.0      33963               0.155
#> 5:       90209.0      32451               0.137
#> 6:       97021.0      29789               0.145

# Verificación adicional: completeness
cat("\nVerificación de completeness:\n")
#> 
#> Verificación de completeness:
cat("Departamentos únicos en original:", uniqueN(sample_employees$department), "\n")
#> Departamentos únicos en original: 5
cat("Departamentos únicos en resultado:", uniqueN(resultado_bueno$department), "\n")
#> Departamentos únicos en resultado: 5
cat("Años únicos en resultado:", paste(sort(unique(resultado_bueno$hire_year)), collapse = ", "), "\n")
#> Años únicos en resultado: 2020, 2021, 2022, 2023, 2024

Mejoras aplicadas en el refactoring:

  1. Eliminación total de bucles: Una sola operación by vectorizada
  2. Sin rbind repetitivo: El resultado se construye eficientemente
  3. Validación de entrada: Verificación de columnas requeridas
  4. Operaciones vectorizadas: mean(), sum(), .N son nativamente rápidas
  5. Sintaxis data.table pura: Sin conversiones a/desde data.frame
  6. Filtros inteligentes: Eliminación de grupos pequeños
  7. Cálculos derivados: Usando := para eficiencia
  8. Documentación: Función bien documentada
  9. Manejo de NAs: Parámetro na.rm = TRUE donde corresponde
  10. Ordenamiento lógico: Resultado ordenado para mejor interpretación

10.7 Próximo Capítulo: Integración con el Ecosistema

En el siguiente y último capítulo exploraremos: - Integración con ggplot2 para visualización de datos - Workflows con shiny para aplicaciones interactivas
- Interoperabilidad con tidymodels para machine learning - Conexión con bases de datos y sistemas Big Data - dtplyr: El puente entre data.table y tidyverse


🎯 Puntos Clave de Este Capítulo
  1. El operador := es fundamental para código eficiente en data.table
  2. Los bucles explícitos destruyen todas las optimizaciones - usa vectorización
  3. setkey() es crucial para datasets grandes y operaciones repetitivas
  4. La gestión de memoria puede marcar la diferencia entre código viable e inviable
  5. Validación y testing previenen errores costosos en análisis de datos
  6. El estilo consistente hace el código mantenible y colaborativo
  7. Una operación data.table bien diseñada puede reemplazar cientos de líneas de código tradicional

Has dominado las buenas prácticas de data.table. En el próximo capítulo veremos cómo integrar todo este poder con el ecosistema R para crear soluciones completas de análisis de datos.