Cómo crear una tabla dinámica en R (como en Excel)
Una tabla dinámica (PivotTable en la versión en inglés de Excel) resume un conjunto de datos colocando una variable en las filas, otra en las columnas y una suma, un recuento o un promedio en las celdas. R no tiene un botón de “Insertar tabla dinámica”, pero tampoco lo necesita: con unas pocas líneas de código usando table, xtabs, aggregate, tapply o dplyr y tidyr obtienes el mismo resultado, y puedes volver a ejecutar el código cada vez que cambien los datos. En este tutorial aprenderás a qué equivale en R cada área de la tabla dinámica de Excel, cómo añadir totales generales, subtotales y porcentajes, cómo filtrar y ordenar la tabla y cómo exportarla de nuevo a Excel.
Respuesta rápida
La forma más flexible de crear una tabla dinámica en R es resumir por grupos y después convertir una de las variables de agrupación en columnas. El siguiente código calcula los ingresos totales de cada región (filas) y producto (columnas), rellenando las celdas vacías con 0:
library(dplyr)
library(tidyr)
ventas %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop") %>%
pivot_wider(names_from = producto, values_from = ingresos, values_fill = 0)
# A tibble: 3 × 4
region Móvil Portátil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 10000 8000 1200
2 Oeste 12500 4000 1800
3 Sur 0 8000 2100
Si solo necesitas recuentos o sumas de una variable, en R base es todavía más corto: xtabs(ingresos ~ region + producto, data = ventas) devuelve la misma tabla. Ambas opciones se explican a continuación paso a paso.
Datos de ejemplo
A lo largo del tutorial usaremos un pequeño data frame con las ventas de una tienda. Cada fila es una venta, igual que estaría en una hoja de Excel antes de insertar la tabla dinámica: la región, el producto, el mes, las unidades vendidas y los ingresos.
ventas <- data.frame(
region = c("Este", "Este", "Este", "Este", "Oeste", "Oeste",
"Oeste", "Oeste", "Sur", "Sur", "Sur", "Este"),
producto = c("Portátil", "Móvil", "Portátil", "Móvil", "Portátil", "Móvil",
"Tablet", "Móvil", "Portátil", "Tablet", "Portátil", "Tablet"),
mes = c("Ene", "Ene", "Feb", "Feb", "Ene", "Ene",
"Feb", "Mar", "Ene", "Feb", "Mar", "Mar"),
unidades = c(5, 12, 3, 8, 4, 15, 6, 10, 2, 7, 6, 4),
ingresos = c(5000, 6000, 3000, 4000, 4000, 7500, 1800, 5000, 2000, 2100, 6000, 1200)
)
ventas
region producto mes unidades ingresos
1 Este Portátil Ene 5 5000
2 Este Móvil Ene 12 6000
3 Este Portátil Feb 3 3000
4 Este Móvil Feb 8 4000
5 Oeste Portátil Ene 4 4000
6 Oeste Móvil Ene 15 7500
7 Oeste Tablet Feb 6 1800
8 Oeste Móvil Mar 10 5000
9 Sur Portátil Ene 2 2000
10 Sur Tablet Feb 7 2100
11 Sur Portátil Mar 6 6000
12 Este Tablet Mar 4 1200
Fíjate en que la región Sur nunca vendió un móvil. Esa combinación vacía es la que en Excel verás como una celda en blanco y en R como NA o 0, según la función que uses.
Si tus datos están en un archivo de Excel, léelos primero con read_excel y el resto del tutorial funciona igual.
De las áreas de la tabla dinámica de Excel a R
En Excel arrastras los campos a cuatro áreas: Filas (Rows), Columnas (Columns), Valores (Values) y Filtros (Filters). Cada una tiene un equivalente directo en R:
| Área de la tabla dinámica | Qué hace | R base | dplyr + tidyr |
|---|---|---|---|
| Filas | Grupos que se muestran como filas | Lado izquierdo de ~ o primer factor |
group_by(region) |
| Columnas | Grupos que se muestran como columnas | Segundo factor | pivot_wider(names_from = producto) |
| Valores (Suma, Cuenta, Promedio) | Resumen que aparece en las celdas | FUN = sum, table, xtabs(y ~ ...) |
summarise(ingresos = sum(ingresos)) |
| Filtros | Limita las filas que se usan | Argumento subset o ventas[cond, ] |
filter(mes == "Ene") |
| Totales generales | Totales de filas y columnas | addmargins |
bind_rows con una fila de totales |
| Mostrar valores como (% de…) | Porcentajes | prop.table |
mutate(pct = x / sum(x)) |
Ten esta tabla en mente: cualquier tabla dinámica en R no es más que una combinación de estas piezas.
Contar filas: table y xtabs
La tabla dinámica más sencilla es la que cuenta cuántas filas hay en cada combinación, como cuando pones “Cuenta de” en el área de Valores. En R esto es una tabla de contingencia y puedes crearla con la función table:
table(ventas$region, ventas$producto)
Móvil Portátil Tablet
Este 2 2 1
Oeste 2 1 1
Sur 0 2 1
Lee la salida como “la región Este tiene 2 ventas de móviles, 2 de portátiles y 1 de tablets”. Son número de filas (pedidos), no unidades vendidas.
La función xtabs hace lo mismo con una fórmula y el argumento data, lo que resulta más limpio y además conserva los nombres de las variables como encabezados:
xtabs(~ region + producto, data = ventas)
producto
region Móvil Portátil Tablet
Este 2 2 1
Oeste 2 1 1
Sur 0 2 1
La variable que va antes del + se coloca en las filas y la que va después en las columnas. Aprende a trabajar con estos objetos en el artículo sobre tablas de contingencia en R.
Sumar y promediar: xtabs, aggregate y tapply
Sumar con xtabs
Si pones una variable numérica en el lado izquierdo de la fórmula, xtabs la suma en lugar de contar filas. Es la tabla dinámica de “Suma de ingresos”:
xtabs(ingresos ~ region + producto, data = ventas)
producto
region Móvil Portátil Tablet
Este 10000 8000 1200
Oeste 12500 4000 1800
Sur 0 8000 2100
xtabs solo sabe sumar. Para medias, medianas o cualquier otra función necesitas aggregate, tapply o dplyr.
aggregate
La función aggregate aplica cualquier función por grupos, pero devuelve una tabla larga, con una fila por combinación, en lugar de una tabla cruzada:
aggregate(ingresos ~ region + producto, data = ventas, FUN = sum)
region producto ingresos
1 Este Móvil 10000
2 Oeste Móvil 12500
3 Este Portátil 8000
4 Oeste Portátil 4000
5 Sur Portátil 8000
6 Este Tablet 1200
7 Oeste Tablet 1800
8 Sur Tablet 2100
Este formato largo equivale a la tabla dinámica de Excel con los dos campos en el área de Filas (“Mostrar en formato tabular”). Observa que las combinaciones sin datos, como Sur y Móvil, directamente no aparecen. Con una sola variable de agrupación, aggregate es la forma más rápida de obtener la media de cada grupo:
aggregate(ingresos ~ region, data = ventas, FUN = mean)
region ingresos
1 Este 3840.000
2 Oeste 4575.000
3 Sur 3366.667
tapply
La función tapply devuelve directamente una tabla cruzada cuando le pasas una lista con dos variables de agrupación. Aquí calculamos las unidades medias por venta de cada región y producto:
tapply(ventas$unidades, list(ventas$region, ventas$producto), mean)
Móvil Portátil Tablet
Este 10.0 4 4
Oeste 12.5 4 6
Sur NA 4 7
La combinación vacía aparece ahora como NA, porque no hay ningún valor que promediar. Cuando sumas con tapply, puedes sustituir esos NA por ceros con el argumento default:
tapply(ventas$ingresos, list(ventas$region, ventas$producto), sum, default = 0)
Móvil Portátil Tablet
Este 10000 8000 1200
Oeste 12500 4000 1800
Sur 0 8000 2100
Con dplyr: group_by, summarise y pivot_wider
El enfoque del tidyverse tiene dos pasos y es el que mejor escala cuando la tabla dinámica se complica:
- Agrupa con group_by por los campos de Filas y Columnas y resume con summarise el campo de Valores. Obtienes una tabla larga.
- Convierte el campo de Columnas en columnas con pivot_wider.
El primer paso devuelve la misma tabla larga que aggregate:
library(dplyr)
library(tidyr)
larga <- ventas %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop")
larga
# A tibble: 8 × 3
region producto ingresos
<chr> <chr> <dbl>
1 Este Móvil 10000
2 Este Portátil 8000
3 Este Tablet 1200
4 Oeste Móvil 12500
5 Oeste Portátil 4000
6 Oeste Tablet 1800
7 Sur Portátil 8000
8 Sur Tablet 2100
Y el segundo paso convierte los productos en columnas:
larga %>%
pivot_wider(names_from = producto, values_from = ingresos)
# A tibble: 3 × 4
region Móvil Portátil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 10000 8000 1200
2 Oeste 12500 4000 1800
3 Sur NA 8000 2100
El resultado es un data frame (un tibble), no un objeto de tipo tabla, así que puedes seguir trabajando con él usando mutate, filter o arrange y exportarlo tal cual.
Para mostrar una media o un recuento en lugar de una suma, cambia la función dentro de summarise: mean(unidades), n(), median(ingresos), max(ingresos), etc. Por ejemplo, las unidades medias por venta según región y mes:
ventas %>%
group_by(region, mes) %>%
summarise(media_unidades = mean(unidades), .groups = "drop") %>%
pivot_wider(names_from = mes, values_from = media_unidades)
# A tibble: 3 × 4
region Ene Feb Mar
<chr> <dbl> <dbl> <dbl>
1 Este 8.5 5.5 4
2 Oeste 9.5 6 10
3 Sur 2 7 6
Rellenar las celdas vacías con 0
En la tabla anterior de ingresos la celda Sur-Móvil era NA. Cuando el valor es una suma o un recuento, una celda vacía significa realmente cero, así que indica values_fill = 0 en pivot_wider, que equivale a la opción de Excel “Para celdas vacías, mostrar: 0”:
dinamica <- ventas %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop") %>%
pivot_wider(names_from = producto, values_from = ingresos,
values_fill = 0)
dinamica
# A tibble: 3 × 4
region Móvil Portátil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 10000 8000 1200
2 Oeste 12500 4000 1800
3 Sur 0 8000 2100
No rellenes con 0 una tabla dinámica de medias. La media de un producto que nunca se vendió no es 0, simplemente no existe. Un 0 ahí rebajaría cualquier promedio que calcules después a partir de la tabla. En ese caso, deja el NA.
Varios campos de valores
En Excel puedes arrastrar varios campos al área de Valores, por ejemplo “Suma de unidades” y “Suma de ingresos”. En dplyr basta con añadir más columnas dentro de summarise. Si la tabla no tiene campo de Columnas, ya has terminado:
ventas %>%
group_by(region) %>%
summarise(pedidos = n(),
unidades = sum(unidades),
ingresos = sum(ingresos),
precio_medio = sum(ingresos) / sum(unidades))
# A tibble: 3 × 5
region pedidos unidades ingresos precio_medio
<chr> <int> <dbl> <dbl> <dbl>
1 Este 5 32 19200 600
2 Oeste 4 35 18300 523.
3 Sur 3 15 10100 673.
Si además necesitas un campo de Columnas, pasa todas las columnas de valores a values_from. pivot_wider construirá los nombres uniendo el valor y la categoría:
ventas %>%
group_by(region, producto) %>%
summarise(unidades = sum(unidades), ingresos = sum(ingresos), .groups = "drop") %>%
pivot_wider(names_from = producto,
values_from = c(unidades, ingresos),
values_fill = 0)
# A tibble: 3 × 7
region unidades_Móvil unidades_Portátil unidades_Tablet ingresos_Móvil
<chr> <dbl> <dbl> <dbl> <dbl>
1 Este 20 8 4 10000
2 Oeste 25 4 6 12500
3 Sur 0 8 7 0
# ℹ 2 more variables: ingresos_Portátil <dbl>, ingresos_Tablet <dbl>
Las últimas columnas no se muestran porque no caben en el ancho de la consola, pero están en el data frame. Usa print(width = Inf) o View() para verlas todas.
Totales generales y subtotales
addmargins
Si tu tabla dinámica es un objeto de tipo tabla (creado con table, xtabs o tapply), addmargins añade los totales de filas y columnas en una sola línea, con la etiqueta Sum:
tab <- xtabs(ingresos ~ region + producto, data = ventas)
addmargins(tab)
producto
region Móvil Portátil Tablet Sum
Este 10000 8000 1200 19200
Oeste 12500 4000 1800 18300
Sur 0 8000 2100 10100
Sum 22500 20000 5100 47600
Usa margin = 1 para añadir solo la fila inferior de totales o margin = 2 para añadir solo la columna de totales. En la sección de addmargins del artículo sobre table tienes más ejemplos.
Totales en R base con rowSums y colSums
Sin addmargins (por ejemplo, si quieres llamar “Total” a los totales o estás trabajando con una matriz), añade tú mismo las sumas de filas y columnas:
pv <- tapply(ventas$ingresos, list(ventas$region, ventas$producto), sum, default = 0)
pv <- cbind(pv, Total = rowSums(pv))
pv <- rbind(pv, Total = colSums(pv))
pv
Móvil Portátil Tablet Total
Este 10000 8000 1200 19200
Oeste 12500 4000 1800 18300
Sur 0 8000 2100 10100
Total 22500 20000 5100 47600
El orden importa: primero se añade la columna de totales, así que colSums también la suma y la celda inferior derecha es el total general.
Totales en dplyr con bind_rows
Con un data frame, añade la columna de totales con rowSums dentro de mutate y la fila de totales sumando todas las columnas numéricas y uniéndola al final con bind_rows:
dinamica_tot <- dinamica %>%
mutate(Total = rowSums(across(where(is.numeric))))
totales <- dinamica_tot %>%
summarise(across(where(is.numeric), sum)) %>%
mutate(region = "Total")
bind_rows(dinamica_tot, totales)
# A tibble: 4 × 5
region Móvil Portátil Tablet Total
<chr> <dbl> <dbl> <dbl> <dbl>
1 Este 10000 8000 1200 19200
2 Oeste 12500 4000 1800 18300
3 Sur 0 8000 2100 10100
4 Total 22500 20000 5100 47600
bind_rows une las columnas por su nombre, así que no importa el orden de las columnas en totales.
Subtotales
Excel muestra un subtotal de cada grupo cuando pones dos campos en Filas. En R, calcula el detalle y los subtotales por separado, apílalos y ordénalos para que cada subtotal quede después de su grupo:
detalle <- ventas %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop")
subtotales <- detalle %>%
group_by(region) %>%
summarise(ingresos = sum(ingresos)) %>%
mutate(producto = "Subtotal")
bind_rows(detalle, subtotales) %>%
arrange(region, producto == "Subtotal")
# A tibble: 11 × 3
region producto ingresos
<chr> <chr> <dbl>
1 Este Móvil 10000
2 Este Portátil 8000
3 Este Tablet 1200
4 Este Subtotal 19200
5 Oeste Móvil 12500
6 Oeste Portátil 4000
7 Oeste Tablet 1800
8 Oeste Subtotal 18300
9 Sur Portátil 8000
10 Sur Tablet 2100
11 Sur Subtotal 10100
El truco está en arrange(region, producto == "Subtotal"): la comparación vale FALSE para los productos y TRUE para el subtotal, y como FALSE se ordena antes que TRUE, la fila del subtotal siempre queda la última dentro de su región.
Porcentaje del total de fila, de columna y general
El menú “Mostrar valores como” (Show Values As) de Excel permite ver el “% del total general”, el “% del total de fila” o el “% del total de columna”. En R, los tres se calculan con prop.table y su argumento margin (consulta prop.table):
# % del total general
round(prop.table(tab) * 100, 1)
producto
region Móvil Portátil Tablet
Este 21.0 16.8 2.5
Oeste 26.3 8.4 3.8
Sur 0.0 16.8 4.4
# % del total de fila (cada región suma 100)
round(prop.table(tab, margin = 1) * 100, 1)
producto
region Móvil Portátil Tablet
Este 52.1 41.7 6.2
Oeste 68.3 21.9 9.8
Sur 0.0 79.2 20.8
# % del total de columna (cada producto suma 100)
round(prop.table(tab, margin = 2) * 100, 1)
producto
region Móvil Portátil Tablet
Este 44.4 40.0 23.5
Oeste 55.6 20.0 35.3
Sur 0.0 40.0 41.2
Cada tabla responde a una pregunta distinta. Los porcentajes por fila dicen que el 79,2% de los ingresos del Sur viene de los portátiles, mientras que los porcentajes por columna dicen que el Sur genera el 40% de todos los ingresos de portátiles. Confundirlos es el error más habitual al leer este tipo de tablas, así que comprueba qué margen suma 100 antes de sacar conclusiones. Si envuelves el resultado en addmargins(..., margin = 2) se añade una columna de 100 que lo deja claro.
Con dplyr, calcula el porcentaje dentro de cada región con mutate y .by antes de ensanchar la tabla:
ventas %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop") %>%
mutate(pct = round(ingresos / sum(ingresos) * 100, 1), .by = region) %>%
select(-ingresos) %>%
pivot_wider(names_from = producto, values_from = pct, values_fill = 0)
# A tibble: 3 × 4
region Móvil Portátil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 52.1 41.7 6.2
2 Oeste 68.3 21.9 9.8
3 Sur 0 79.2 20.8
Quita .by = region para obtener el porcentaje sobre el total general o usa .by = producto para el porcentaje de cada columna.
Filtros (el filtro del informe)
El área de Filtros de una tabla dinámica limita las filas que se usan para construirla. En R simplemente filtras los datos antes de crear la tabla. Por ejemplo, los ingresos de enero y febrero:
ventas %>%
filter(mes %in% c("Ene", "Feb")) %>%
group_by(region, producto) %>%
summarise(ingresos = sum(ingresos), .groups = "drop") %>%
pivot_wider(names_from = producto, values_from = ingresos, values_fill = 0)
# A tibble: 3 × 4
region Móvil Portátil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 10000 8000 0
2 Oeste 7500 4000 1800
3 Sur 0 2000 2100
Consulta la función filter para ver todas las formas de escribir condiciones. En R base, xtabs y aggregate tienen un argumento subset que hace lo mismo:
xtabs(ingresos ~ region + producto, data = ventas, subset = mes == "Ene")
producto
region Móvil Portátil
Este 6000 5000
Oeste 7500 4000
Sur 0 2000
Observa que la columna Tablet ha desaparecido, porque en enero no se vendió ninguna. A diferencia de Excel, R solo crea columnas para los valores que aparecen en los datos filtrados. Si necesitas un conjunto fijo de columnas, convierte la variable en un factor con todos sus niveles antes de filtrar.
Ordenar la tabla dinámica
Para ordenar las filas según un valor, como “Ordenar de mayor a menor” en Excel, usa arrange sobre la tabla. Aquí ordenamos las regiones por sus ingresos totales:
dinamica_tot %>%
arrange(desc(Total))
# A tibble: 3 × 5
region Móvil Portátil Tablet Total
<chr> <dbl> <dbl> <dbl> <dbl>
1 Este 10000 8000 1200 19200
2 Oeste 12500 4000 1800 18300
3 Sur 0 8000 2100 10100
El orden de las columnas es otra historia. table y xtabs las ordenan alfabéticamente, y lo mismo ocurre con dplyr: group_by ordena los grupos alfabéticamente y pivot_wider crea las columnas en el orden en el que aparecen en ese resumen. Con nuestros meses (“Ene”, “Feb”, “Mar”) coincide con el orden correcto por casualidad, pero con “Abr”, “Ago” o “Dic” no sería así. Convierte la variable de las columnas en un factor con los niveles en el orden que quieras y tanto pivot_wider como table lo respetarán. Por ejemplo, para mostrar primero las tablets:
ventas %>%
mutate(producto = factor(producto, levels = c("Tablet", "Móvil", "Portátil"))) %>%
group_by(region, producto) %>%
summarise(unidades = sum(unidades), .groups = "drop") %>%
pivot_wider(names_from = producto, values_from = unidades, values_fill = 0)
# A tibble: 3 × 4
region Tablet Móvil Portátil
<chr> <dbl> <dbl> <dbl>
1 Este 4 20 8
2 Oeste 6 25 4
3 Sur 7 0 8
Si los meses son fechas reales, es mejor extraer el mes de la columna de fecha. Aprende cómo trabajar con fechas en R.
Exportar la tabla dinámica a Excel
Una tabla creada con dplyr ya es un data frame, así que puedes guardarla directamente como archivo XLSX, por ejemplo con el paquete writexl:
# install.packages("writexl")
library(writexl)
write_xlsx(bind_rows(dinamica_tot, totales), "tabla_ingresos.xlsx")
Un objeto de tipo tabla creado con table, xtabs o tapply hay que convertirlo primero en data frame. Ojo: as.data.frame(tab) lo devuelve al formato largo. Para mantener la disposición cruzada usa as.data.frame.matrix y pasa los nombres de las filas a una columna para no perderlos:
tab_df <- as.data.frame.matrix(addmargins(tab))
tab_df <- cbind(region = rownames(tab_df), tab_df)
rownames(tab_df) <- NULL
tab_df
region Móvil Portátil Tablet Sum
1 Este 10000 8000 1200 19200
2 Oeste 12500 4000 1800 18300
3 Sur 0 8000 2100 10100
4 Sum 22500 20000 5100 47600
Ahora write_xlsx(tab_df, "tabla_ingresos.xlsx") escribe la tabla tal y como la ves. En el artículo sobre exportar datos en R tienes otros paquetes y formatos, como CSV.
¿Qué método deberías usar?
| Método | Recuentos | Sumas | Cualquier función | Resultado | Ideal para |
|---|---|---|---|---|---|
table() |
Sí | No | No | Tabla | Recuentos rápidos de una o dos variables |
xtabs() |
Sí | Sí | No | Tabla | Recuentos y sumas con fórmula, addmargins y prop.table |
tapply() |
No | Sí | Sí | Matriz | Tabla cruzada de medias, medianas, etc. en R base |
aggregate() |
Sí | Sí | Sí | Data frame largo | Varios campos de valores sin paquetes |
group_by() + summarise() + pivot_wider() |
Sí | Sí | Sí | Data frame ancho | Tablas complejas, varios valores, exportar |
Como regla general, usa xtabs cuando necesites recuentos o sumas con totales y porcentajes, y dplyr con pivot_wider para todo lo demás, sobre todo si vas a reutilizar o exportar el resultado.
Tablas dinámicas interactivas con rpivotTable
Si echas de menos arrastrar y soltar campos, el paquete rpivotTable crea una tabla dinámica interactiva en el visor de RStudio o en un informe HTML con rpivotTable(ventas, rows = "region", cols = "producto", vals = "ingresos", aggregatorName = "Sum"). Después puedes reorganizar los campos con el ratón, cambiar la función de resumen e incluso pasar a un mapa de calor o a un gráfico. Es útil para explorar datos, pero el resultado no es un objeto de R con el que puedas seguir trabajando, así que para análisis reproducibles quédate con los métodos anteriores.
Errores comunes
Warning: values are not uniquely identified; output will contain list-cols
Este aviso aparece cuando aplicas pivot_wider a los datos originales en lugar de a los datos resumidos. Varias filas comparten la misma región y producto, así que pivot_wider no sabe qué valor poner en la celda y los guarda todos en una lista:
ventas %>%
select(region, producto, ingresos) %>%
pivot_wider(names_from = producto, values_from = ingresos)
# A tibble: 3 × 4
region Portátil Móvil Tablet
<chr> <list> <list> <list>
1 Este <dbl [2]> <dbl [2]> <dbl [1]>
2 Oeste <dbl [1]> <dbl [2]> <dbl [1]>
3 Sur <dbl [2]> <NULL> <dbl [1]>
Warning message:
Values from `ingresos` are not uniquely identified; output will contain
list-cols.
• Use `values_fn = list` to suppress this warning.
• Use `values_fn = {summary_fun}` to summarise duplicates.
• Use the following dplyr code to identify duplicates.
{data} %>%
dplyr::group_by(region, producto) %>%
dplyr::summarise(n = dplyr::n(), .groups = "drop") %>%
dplyr::filter(n > 1L)
Añade el paso de group_by y summarise antes de pivot_wider, o indica a pivot_wider cómo combinar los duplicados con values_fn:
ventas %>%
pivot_wider(id_cols = region, names_from = producto, values_from = ingresos,
values_fn = sum, values_fill = 0)
# A tibble: 3 × 4
region Portátil Móvil Tablet
<chr> <dbl> <dbl> <dbl>
1 Este 8000 10000 1200
2 Oeste 4000 12500 1800
3 Sur 8000 0 2100
id_cols = region es importante aquí: sin él, mes y unidades también se usarían para identificar las filas y obtendrías una fila por venta.
La tabla cuenta filas cuando querías sumar
table(ventas$region, ventas$producto) cuenta ventas (filas), no unidades. Si las celdas deben mostrar la suma de una columna, pon esa columna en el lado izquierdo de xtabs: xtabs(unidades ~ region + producto, data = ventas). Es la misma diferencia que hay entre “Cuenta de unidades” y “Suma de unidades” en Excel.