library(tidyverse)Semana 07 — Joins, Tidy Data y Relaciones entre Tablas
Introducción al Lenguaje de Programación y Estadística R — ITLA
1 Dataset de Práctica
# Tabla 1: Clientes
clientes <- tibble(
id_cliente = c("C001", "C002", "C003", "C004", "C005", "C006"),
nombre = c("Ana Torres", "Pedro Méndez", "María López",
"Luis García", "Carmen Díaz", "Jorge Reyes"),
ciudad = c("Santo Domingo", "Santiago", "Santo Domingo",
"La Vega", "San Pedro", "Santo Domingo"),
tipo = c("Premium", "Regular", "Premium",
"Regular", "Regular", "Premium")
)
# Tabla 2: Facturas
facturas <- tibble(
id_factura = c("F001", "F002", "F003", "F004", "F005", "F006", "F007", "F008"),
id_cliente = c("C001", "C001", "C002", "C003", "C005", "C007", "C002", "C001"),
mes = c("Ene", "Feb", "Ene", "Feb", "Ene", "Feb", "Mar", "Ene"),
monto = c(15000, 22000, 8500, 31000, 4500, 12000, 9800, 15000)
)
# Tabla 3: Pagos
pagos <- tibble(
id_factura = c("F001", "F002", "F004", "F006", "F007"),
monto_pagado = c(15000, 22000, 31000, 12000, 9800)
)
cat("=== Tablas cargadas ===\n")=== Tablas cargadas ===
cat("clientes: ", nrow(clientes), "filas x", ncol(clientes), "columnas\n")clientes: 6 filas x 4 columnas
cat("facturas: ", nrow(facturas), "filas x", ncol(facturas), "columnas\n")facturas: 8 filas x 4 columnas
cat("pagos: ", nrow(pagos), "filas x", ncol(pagos), "columnas\n")pagos: 5 filas x 2 columnas
2 Parte 1 — Diagnóstico Relacional
2.1 1.1 Facturas con id_cliente que NO existe en clientes
# anti_join(x, y) devuelve las filas de x que NO tienen coincidencia en y
facturas_huerfanas <- facturas |>
anti_join(clientes, by = "id_cliente")
cat("=== Facturas con id_cliente inexistente en clientes ===\n")=== Facturas con id_cliente inexistente en clientes ===
print(facturas_huerfanas)# A tibble: 1 × 4
id_factura id_cliente mes monto
<chr> <chr> <chr> <dbl>
1 F006 C007 Feb 12000
# HALLAZGO: La factura F006 tiene id_cliente = "C007", que no existe
# en la tabla clientes. Esta es una violación de integridad referencial:
# se facturó a un cliente que no está registrado en el sistema.
# Decisión sugerida: investigar si "C007" es un error de tipeo o un
# cliente real que falta agregar a la tabla clientes antes de cualquier análisis.2.2 1.2 Clientes sin ninguna factura
clientes_sin_facturas <- clientes |>
anti_join(facturas, by = "id_cliente")
cat("=== Clientes sin facturas registradas ===\n")=== Clientes sin facturas registradas ===
print(clientes_sin_facturas)# A tibble: 2 × 4
id_cliente nombre ciudad tipo
<chr> <chr> <chr> <chr>
1 C004 Luis García La Vega Regular
2 C006 Jorge Reyes Santo Domingo Premium
cat("Total de clientes sin facturas:", nrow(clientes_sin_facturas), "\n")Total de clientes sin facturas: 2
# HALLAZGO: C004 (Luis García, La Vega) y C006 (Jorge Reyes, Santo Domingo)
# nunca han generado una factura. Son clientes inactivos o nuevos.2.3 1.3 Facturas sin pago registrado
facturas_sin_pago <- facturas |>
anti_join(pagos, by = "id_factura")
cat("=== Facturas sin pago registrado ===\n")=== Facturas sin pago registrado ===
print(facturas_sin_pago)# A tibble: 3 × 4
id_factura id_cliente mes monto
<chr> <chr> <chr> <dbl>
1 F003 C002 Ene 8500
2 F005 C005 Ene 4500
3 F008 C001 Ene 15000
cat("IDs:", paste(facturas_sin_pago$id_factura, collapse = ", "), "\n")IDs: F003, F005, F008
# HALLAZGO: Las facturas F003, F005 y F008 no tienen pago registrado.
# Representan cuentas por cobrar pendientes que requieren seguimiento.2.4 1.4 Duplicados en la tabla facturas
cat("=== Búsqueda de duplicados en facturas ===\n")=== Búsqueda de duplicados en facturas ===
# Verificamos combinaciones id_factura + id_cliente + mes + monto duplicadas
duplicados <- facturas |>
count(id_cliente, mes, monto) |>
filter(n > 1)
print(duplicados)# A tibble: 1 × 4
id_cliente mes monto n
<chr> <chr> <dbl> <int>
1 C001 Ene 15000 2
# Ver las filas duplicadas completas
cat("\nFilas duplicadas en detalle:\n")
Filas duplicadas en detalle:
facturas |>
group_by(id_cliente, mes, monto) |>
filter(n() > 1) |>
arrange(id_cliente, mes) |>
print()# A tibble: 2 × 4
# Groups: id_cliente, mes, monto [1]
id_factura id_cliente mes monto
<chr> <chr> <chr> <dbl>
1 F001 C001 Ene 15000
2 F008 C001 Ene 15000
# HALLAZGO: C001 tiene dos registros idénticos en mes "Ene" con monto 15000
# (F001 y F008). Son el mismo importe, mismo cliente, mismo mes, distinto
# id_factura → probable doble registro por error operativo.
# DECISIÓN: Conservar F001 (primera en aparecer) y eliminar F008,
# ya que F001 ya tiene pago registrado en la tabla pagos y F008 no.3 Parte 2 — Aplicando los 4 Joins
3.1 2.1 left_join — Clientes con facturas
cat("Filas en clientes (antes):", nrow(clientes), "\n")Filas en clientes (antes): 6
clientes_con_facturas <- clientes |>
left_join(facturas, by = "id_cliente")
cat("Filas en clientes_con_facturas (después):", nrow(clientes_con_facturas), "\n")Filas en clientes_con_facturas (después): 9
print(clientes_con_facturas)# A tibble: 9 × 7
id_cliente nombre ciudad tipo id_factura mes monto
<chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 C001 Ana Torres Santo Domingo Premium F001 Ene 15000
2 C001 Ana Torres Santo Domingo Premium F002 Feb 22000
3 C001 Ana Torres Santo Domingo Premium F008 Ene 15000
4 C002 Pedro Méndez Santiago Regular F003 Ene 8500
5 C002 Pedro Méndez Santiago Regular F007 Mar 9800
6 C003 María López Santo Domingo Premium F004 Feb 31000
7 C004 Luis García La Vega Regular <NA> <NA> NA
8 C005 Carmen Díaz San Pedro Regular F005 Ene 4500
9 C006 Jorge Reyes Santo Domingo Premium <NA> <NA> NA
# EXPLICACIÓN: El resultado tiene MÁS filas que la tabla clientes (6)
# porque el left_join replica la fila del cliente por cada factura que tiene.
# C001 tiene 3 facturas → aparece 3 veces.
# C002 tiene 2 facturas → aparece 2 veces.
# C004 y C006 no tienen facturas → aparecen 1 vez con NA en columnas de facturas.
# Total = 3 + 2 + 1 + 1 + 1 + 1 = 9 filas.3.2 2.2 inner_join — Solo clientes CON facturas
clientes_inner <- clientes |>
inner_join(facturas, by = "id_cliente")
cat("Filas con inner_join:", nrow(clientes_inner), "\n")Filas con inner_join: 7
print(clientes_inner)# A tibble: 7 × 7
id_cliente nombre ciudad tipo id_factura mes monto
<chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 C001 Ana Torres Santo Domingo Premium F001 Ene 15000
2 C001 Ana Torres Santo Domingo Premium F002 Feb 22000
3 C001 Ana Torres Santo Domingo Premium F008 Ene 15000
4 C002 Pedro Méndez Santiago Regular F003 Ene 8500
5 C002 Pedro Méndez Santiago Regular F007 Mar 9800
6 C003 María López Santo Domingo Premium F004 Feb 31000
7 C005 Carmen Díaz San Pedro Regular F005 Ene 4500
# DIFERENCIA CON LEFT_JOIN:
# Desaparecen C004 (Luis García) y C006 (Jorge Reyes) porque no tienen
# ninguna factura. El inner_join solo conserva las coincidencias en AMBAS tablas.
# También desaparece la factura F006 (id_cliente = C007) porque C007
# no existe en clientes.3.3 2.3 full_join — Todos los registros
clientes_full <- clientes |>
full_join(facturas, by = "id_cliente")
cat("Filas con full_join:", nrow(clientes_full), "\n")Filas con full_join: 10
print(clientes_full)# A tibble: 10 × 7
id_cliente nombre ciudad tipo id_factura mes monto
<chr> <chr> <chr> <chr> <chr> <chr> <dbl>
1 C001 Ana Torres Santo Domingo Premium F001 Ene 15000
2 C001 Ana Torres Santo Domingo Premium F002 Feb 22000
3 C001 Ana Torres Santo Domingo Premium F008 Ene 15000
4 C002 Pedro Méndez Santiago Regular F003 Ene 8500
5 C002 Pedro Méndez Santiago Regular F007 Mar 9800
6 C003 María López Santo Domingo Premium F004 Feb 31000
7 C004 Luis García La Vega Regular <NA> <NA> NA
8 C005 Carmen Díaz San Pedro Regular F005 Ene 4500
9 C006 Jorge Reyes Santo Domingo Premium <NA> <NA> NA
10 C007 <NA> <NA> <NA> F006 Feb 12000
# FILA EXTRA: Aparece una fila con NA en nombre, ciudad y tipo,
# correspondiente a la factura F006 con id_cliente = "C007".
# Esto ocurre porque full_join incluye TODAS las filas de ambas tablas,
# incluso cuando no hay coincidencia. C007 está en facturas pero no en
# clientes → sus columnas de clientes quedan como NA.3.4 2.4 Joins encadenados — Tabla maestra
tabla_maestra <- clientes |>
left_join(facturas, by = "id_cliente") |>
left_join(pagos, by = "id_factura")
cat("=== Tabla Maestra ===\n")=== Tabla Maestra ===
cat("Filas:", nrow(tabla_maestra), "\n")Filas: 9
print(tabla_maestra)# A tibble: 9 × 8
id_cliente nombre ciudad tipo id_factura mes monto monto_pagado
<chr> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl>
1 C001 Ana Torres Santo Domin… Prem… F001 Ene 15000 15000
2 C001 Ana Torres Santo Domin… Prem… F002 Feb 22000 22000
3 C001 Ana Torres Santo Domin… Prem… F008 Ene 15000 NA
4 C002 Pedro Méndez Santiago Regu… F003 Ene 8500 NA
5 C002 Pedro Méndez Santiago Regu… F007 Mar 9800 9800
6 C003 María López Santo Domin… Prem… F004 Feb 31000 31000
7 C004 Luis García La Vega Regu… <NA> <NA> NA NA
8 C005 Carmen Díaz San Pedro Regu… F005 Ene 4500 NA
9 C006 Jorge Reyes Santo Domin… Prem… <NA> <NA> NA NA
# RESPUESTAS:
# Filas: 9 (igual que el left_join de clientes + facturas, ya que pagos
# solo agrega columnas, no multiplica filas).
# monto_pagado es NA cuando la factura no tiene pago registrado:
# F003, F005 y F008 no están en la tabla pagos → monto_pagado = NA.
# Además C004 y C006 no tienen facturas → id_factura y monto también son NA.4 Parte 3 — Joins de Filtro: Auditoría
4.1 3.1 semi_join — Clientes que SÍ tienen facturas
clientes_con_facturas_semi <- clientes |>
semi_join(facturas, by = "id_cliente")
cat("=== Clientes con al menos una factura ===\n")=== Clientes con al menos una factura ===
print(clientes_con_facturas_semi)# A tibble: 4 × 4
id_cliente nombre ciudad tipo
<chr> <chr> <chr> <chr>
1 C001 Ana Torres Santo Domingo Premium
2 C002 Pedro Méndez Santiago Regular
3 C003 María López Santo Domingo Premium
4 C005 Carmen Díaz San Pedro Regular
cat("Total:", nrow(clientes_con_facturas_semi), "\n")Total: 4
# semi_join devuelve filas de clientes (sin repetir) que tienen
# al menos una coincidencia en facturas. A diferencia del left_join,
# NO agrega columnas de facturas ni repite filas.4.2 3.2 anti_join — Clientes que NUNCA facturaron
clientes_nunca_facturaron <- clientes |>
anti_join(facturas, by = "id_cliente") |>
select(nombre, ciudad)
cat("=== Clientes sin ninguna factura ===\n")=== Clientes sin ninguna factura ===
print(clientes_nunca_facturaron)# A tibble: 2 × 2
nombre ciudad
<chr> <chr>
1 Luis García La Vega
2 Jorge Reyes Santo Domingo
4.3 3.3 anti_join inverso — Facturas de clientes no registrados
facturas_sin_cliente <- facturas |>
anti_join(clientes, by = "id_cliente") |>
select(id_factura, id_cliente, monto)
cat("=== Facturas de clientes no registrados ===\n")=== Facturas de clientes no registrados ===
print(facturas_sin_cliente)# A tibble: 1 × 3
id_factura id_cliente monto
<chr> <chr> <dbl>
1 F006 C007 12000
# La factura F006 (C007, RD$12,000) pertenece a un cliente
# que no existe en el sistema → problema de integridad referencial.5 Parte 4 — Manejo de Duplicados
5.1 4.1 Eliminar duplicados exactos
# distinct() es la forma correcta en dplyr de eliminar filas duplicadas.
# duplicated(.) no funciona dentro de un pipe de dplyr porque el "."
# no se resuelve correctamente en ese contexto.
facturas_sin_dup <- facturas |>
distinct(id_cliente, mes, monto, .keep_all = TRUE)
cat("Filas originales: ", nrow(facturas), "\n")Filas originales: 8
cat("Filas sin duplicados: ", nrow(facturas_sin_dup), "\n")Filas sin duplicados: 7
print(facturas_sin_dup)# A tibble: 7 × 4
id_factura id_cliente mes monto
<chr> <chr> <chr> <dbl>
1 F001 C001 Ene 15000
2 F002 C001 Feb 22000
3 F003 C002 Ene 8500
4 F004 C003 Feb 31000
5 F005 C005 Ene 4500
6 F006 C007 Feb 12000
7 F007 C002 Mar 9800
# Quedan 7 filas: se eliminó F008 (duplicado exacto de F001 en
# id_cliente, mes y monto). Se conserva F001 porque fue la primera
# en registrarse y ya tiene pago registrado.5.2 4.2 Quedarse con la factura de mayor monto por cliente
facturas_max <- facturas |>
group_by(id_cliente) |>
slice_max(order_by = monto, n = 1, with_ties = FALSE) |>
ungroup()
cat("=== Factura de mayor monto por cliente ===\n")=== Factura de mayor monto por cliente ===
print(facturas_max)# A tibble: 5 × 4
id_factura id_cliente mes monto
<chr> <chr> <chr> <dbl>
1 F002 C001 Feb 22000
2 F007 C002 Mar 9800
3 F004 C003 Feb 31000
4 F005 C005 Ene 4500
5 F006 C007 Feb 12000
5.3 4.3 Reconstruir tabla maestra con facturas_sin_dup
tabla_maestra_limpia <- clientes |>
left_join(facturas_sin_dup, by = "id_cliente") |>
left_join(pagos, by = "id_factura")
cat("Filas tabla_maestra original: ", nrow(tabla_maestra), "\n")Filas tabla_maestra original: 9
cat("Filas tabla_maestra limpia: ", nrow(tabla_maestra_limpia), "\n")Filas tabla_maestra limpia: 8
print(tabla_maestra_limpia)# A tibble: 8 × 8
id_cliente nombre ciudad tipo id_factura mes monto monto_pagado
<chr> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl>
1 C001 Ana Torres Santo Domin… Prem… F001 Ene 15000 15000
2 C001 Ana Torres Santo Domin… Prem… F002 Feb 22000 22000
3 C002 Pedro Méndez Santiago Regu… F003 Ene 8500 NA
4 C002 Pedro Méndez Santiago Regu… F007 Mar 9800 9800
5 C003 María López Santo Domin… Prem… F004 Feb 31000 31000
6 C004 Luis García La Vega Regu… <NA> <NA> NA NA
7 C005 Carmen Díaz San Pedro Regu… F005 Ene 4500 NA
8 C006 Jorge Reyes Santo Domin… Prem… <NA> <NA> NA NA
# Sí cambió: pasó de 9 a 8 filas porque se eliminó el duplicado F008
# de C001. Ahora C001 aparece solo 2 veces (F001 y F002) en lugar de 3.6 Parte 5 — Tidy Data con pivot
6.1 5.1 pivot_wider — Ventas por mes (formato ancho)
ventas_por_mes <- tabla_maestra_limpia |>
filter(!is.na(mes)) |>
group_by(nombre, mes) |>
summarise(total = sum(monto, na.rm = TRUE), .groups = "drop") |>
pivot_wider(
names_from = mes,
values_from = total,
values_fill = 0 # donde no hay ventas → 0
)
cat("=== Ventas por cliente y mes (formato ancho) ===\n")=== Ventas por cliente y mes (formato ancho) ===
print(ventas_por_mes)# A tibble: 4 × 4
nombre Ene Feb Mar
<chr> <dbl> <dbl> <dbl>
1 Ana Torres 15000 22000 0
2 Carmen Díaz 4500 0 0
3 María López 0 31000 0
4 Pedro Méndez 8500 0 9800
# Cada columna es un mes. Los 0 indican que ese cliente
# no tuvo ventas en ese período.6.2 5.2 pivot_longer — Volver a formato largo
ventas_largo <- ventas_por_mes |>
pivot_longer(
cols = -nombre, # todo excepto 'nombre'
names_to = "mes",
values_to = "total"
)
cat("=== Ventas en formato largo ===\n")=== Ventas en formato largo ===
print(ventas_largo)# A tibble: 12 × 3
nombre mes total
<chr> <chr> <dbl>
1 Ana Torres Ene 15000
2 Ana Torres Feb 22000
3 Ana Torres Mar 0
4 Carmen Díaz Ene 4500
5 Carmen Díaz Feb 0
6 Carmen Díaz Mar 0
7 María López Ene 0
8 María López Feb 31000
9 María López Mar 0
10 Pedro Méndez Ene 8500
11 Pedro Méndez Feb 0
12 Pedro Méndez Mar 9800
6.3 5.3 Total vendido por mes (de mayor a menor)
cat("=== Total vendido por mes ===\n")=== Total vendido por mes ===
ventas_largo |>
group_by(mes) |>
summarise(
total_mes = sum(total),
n_clientes_activos = sum(total > 0)
) |>
arrange(desc(total_mes)) |>
print()# A tibble: 3 × 3
mes total_mes n_clientes_activos
<chr> <dbl> <int>
1 Feb 53000 2
2 Ene 28000 3
3 Mar 9800 1
# ANÁLISIS: El mes con mayor facturación es el que tiene la suma
# más alta entre todos los clientes. Los meses con pocos clientes
# activos reflejan baja actividad comercial en ese período.Sesión de R:
R version: 4.6.0
tidyverse: 2.0.0
Fecha: 2026-06-05
Tarea Semana 07 — Introducción al Lenguaje de Programación y Estadística R — ITLA T2 2026