Semana 07 — Joins, Tidy Data y Relaciones entre Tablas

Introducción al Lenguaje de Programación y Estadística R — ITLA

Author

Angelo Pérez

Published

June 5, 2026

library(tidyverse)

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