# ✅ 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 NA10 Buenas Prácticas y Código Idiomático
10.1 Los Mandamientos de data.table
10.1.1 ✅ QUÉ 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.
# 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ápido2. 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 x3. 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.0020227014. 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 FALSE5. 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.210.1.2 ❌ QUÉ 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: FALSE2. 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 Kb4. 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 lento10.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 310.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: 010.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-1210.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.310.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 : 010.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.8310.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 correctamente10.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.296296310.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.6010.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.0010.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álidos10.6 Ejercicio Final: Refactoring de Código
# 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, 2024Mejoras aplicadas en el refactoring:
- Eliminación total de bucles: Una sola operación
byvectorizada - Sin rbind repetitivo: El resultado se construye eficientemente
- Validación de entrada: Verificación de columnas requeridas
- Operaciones vectorizadas:
mean(),sum(),.Nson nativamente rápidas - Sintaxis data.table pura: Sin conversiones a/desde data.frame
- Filtros inteligentes: Eliminación de grupos pequeños
- Cálculos derivados: Usando
:=para eficiencia - Documentación: Función bien documentada
- Manejo de NAs: Parámetro
na.rm = TRUEdonde corresponde - 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
- El operador
:=es fundamental para código eficiente en data.table - Los bucles explícitos destruyen todas las optimizaciones - usa vectorización
- setkey() es crucial para datasets grandes y operaciones repetitivas
- La gestión de memoria puede marcar la diferencia entre código viable e inviable
- Validación y testing previenen errores costosos en análisis de datos
- El estilo consistente hace el código mantenible y colaborativo
- 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.