Una empresa del sector cervecero quiere entender qué prefieren sus consumidores y qué características de una cerveza acompañan a mejores valoraciones. Conocer las diferencias según el tipo, el origen, la composición nutricional, el precio y los atributos sensoriales puede orientar la selección del portafolio, la comunicación de los productos y futuras decisiones comerciales.
Esta es una primera aproximación, exclusivamente descriptiva: los resultados permiten identificar patrones y formular hipótesis para etapas posteriores, pero no establecen relaciones causales.
Pregunta de investigación: ¿cómo se distribuyen las valoraciones de los consumidores entre las cervezas de la base de datos, y qué diferencias se observan según el tipo, el origen y las características de cada producto?
General. Describir el comportamiento de las valoraciones de los consumidores y de las características de las cervezas de la base de datos, mediante tablas de frecuencia e indicadores de estadística descriptiva.
Específicos.
Fuente: base de datos suministrada para el curso,
beer2.csv. Cobertura: reseñas de
consumidores sobre cervezas de diez nacionalidades. Unidad de
análisis: una reseña individual; varias reseñas pueden
corresponder a la misma cerveza. Registros: 1.586.614
reseñas. Variables: 15. Cervezas
distintas: 66.055. Datos faltantes:
ninguno.
diccionario <- data.frame(
Variable = c("id", "marca", "precio", "alcohol", "carbohidratos", "calorias",
"nacionalidad", "calificacion", "aroma", "apariencia", "paladar",
"sabor", "nombre_cerveza", "tipo", "origen"),
Tipo = c("Identificador", "Cualitativa nominal", "Cuantitativa continua (simulada)",
"Cuantitativa continua", "Cuantitativa continua", "Cuantitativa discreta",
"Cualitativa nominal", "Cualitativa ordinal", "Cualitativa ordinal",
"Cualitativa ordinal", "Cualitativa ordinal", "Cualitativa ordinal",
"Cualitativa nominal", "Cualitativa nominal", "Cualitativa nominal dicotómica"),
Descripcion = c(
"Identificador de la cerveza; no es una variable de interés estadístico",
"Cervecería o marca productora",
"Precio de referencia de una caja de seis botellas de 355 ml",
"Gramos de alcohol puro en 355 ml",
"Gramos de carbohidratos en 355 ml",
"Calorías totales en 355 ml",
"País asignado a la cervecería",
"Experiencia general con la cerveza",
"Valoración del aroma",
"Valoración de la apariencia",
"Valoración de la sensación en el paladar",
"Valoración del sabor",
"Nombre comercial de la cerveza",
"Cerveza normal y helada, baja en calorías o sin alcohol, lager importada, clara artesanal o lager artesanal",
"Nacional o importada")
)
kable(diccionario, caption = "Tabla 1. Variables de la base de datos",
col.names = c("Variable", "Clasificación", "Descripción")) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
| Variable | Clasificación | Descripción |
|---|---|---|
| id | Identificador | Identificador de la cerveza; no es una variable de interés estadístico |
| marca | Cualitativa nominal | Cervecería o marca productora |
| precio | Cuantitativa continua (simulada) | Precio de referencia de una caja de seis botellas de 355 ml |
| alcohol | Cuantitativa continua | Gramos de alcohol puro en 355 ml |
| carbohidratos | Cuantitativa continua | Gramos de carbohidratos en 355 ml |
| calorias | Cuantitativa discreta | Calorías totales en 355 ml |
| nacionalidad | Cualitativa nominal | País asignado a la cervecería |
| calificacion | Cualitativa ordinal | Experiencia general con la cerveza |
| aroma | Cualitativa ordinal | Valoración del aroma |
| apariencia | Cualitativa ordinal | Valoración de la apariencia |
| paladar | Cualitativa ordinal | Valoración de la sensación en el paladar |
| sabor | Cualitativa ordinal | Valoración del sabor |
| nombre_cerveza | Cualitativa nominal | Nombre comercial de la cerveza |
| tipo | Cualitativa nominal | Cerveza normal y helada, baja en calorías o sin alcohol, lager importada, clara artesanal o lager artesanal |
| origen | Cualitativa nominal dicotómica | Nacional o importada |
Nota sobre la variable precio. Es una
variable simulada. Los resultados que la involucran describen únicamente
esta base académica y no deben trasladarse al mercado real.
ruta_csv <- "C:/Users/EXITO/OneDrive - PUJ Cali/Escritorio/Universidad/2026 - 2/Teoría de probabilidades/Taller beer/beer2.csv"
if (requireNamespace("data.table", quietly = TRUE)) {
beer2 <- as.data.frame(data.table::fread(ruta_csv, encoding = "UTF-8"))
} else {
beer2 <- read.csv(ruta_csv, stringsAsFactors = FALSE, encoding = "UTF-8")
}
dim(beer2)
## [1] 1586614 15
names(beer2)
## [1] "id" "marca" "precio" "alcohol"
## [5] "carbohidratos" "calorias" "nacionalidad" "calificacion"
## [9] "aroma" "apariencia" "paladar" "sabor"
## [13] "nombre_cerveza" "tipo" "origen"
# Se reordenan los niveles de "tipo" de menor a mayor tamaño de portafolio y se
# acortan las etiquetas para que quepan en los gráficos.
niv_tipo <- c("lager artesanal", "clara artesanal", "lager importada",
"cerveza normal y helada", "baja en calorías / sin alcohol")
labs_tp <- c("lager artesanal", "clara artesanal", "lager importada",
"normal y helada", "baja en calorías")
beer2$tipo <- factor(beer2$tipo, levels = niv_tipo, labels = labs_tp)
beer2$origen <- factor(beer2$origen, levels = c("nacional", "importada"))
ordinales <- c("calificacion", "aroma", "apariencia", "paladar", "sabor")
tit <- c("Calificación general", "Aroma", "Apariencia", "Paladar", "Sabor")
cuanti <- c("precio", "alcohol", "carbohidratos", "calorias")
# Grado alcohólico en % vol. (densidad del etanol = 0,789 g/ml; envase de 355 ml)
beer2$abv <- beer2$alcohol / (0.789 * 355) * 100
# 1. Valores faltantes
colSums(is.na(beer2)) %>%
kable(caption = "Tabla 2. Datos faltantes por variable", col.names = "Datos faltantes") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| Datos faltantes | |
|---|---|
| id | 0 |
| marca | 0 |
| precio | 0 |
| alcohol | 0 |
| carbohidratos | 0 |
| calorias | 0 |
| nacionalidad | 0 |
| calificacion | 0 |
| aroma | 0 |
| apariencia | 0 |
| paladar | 0 |
| sabor | 0 |
| nombre_cerveza | 0 |
| tipo | 0 |
| origen | 0 |
| abv | 0 |
# 2. Cervezas distintas frente a registros de reseñas
n_res_tot <- nrow(beer2)
n_prod <- length(unique(beer2$id))
n_marca <- length(unique(beer2$marca))
n_nombre <- length(unique(beer2$nombre_cerveza))
n_pais <- length(unique(beer2$nacionalidad))
c(reseñas = n_res_tot, cervezas_unicas = n_prod, marcas = n_marca,
nombres_comerciales = n_nombre, nacionalidades = n_pais)
## reseñas cervezas_unicas marcas nombres_comerciales
## 1586614 66055 5743 56857
## nacionalidades
## 10
# 3. ¿Los atributos del producto son constantes dentro de cada cerveza?
const_ok <- all(sapply(c("marca","precio","alcohol","carbohidratos",
"calorias","nacionalidad","tipo","origen"),
function(v) max(tapply(as.character(beer2[[v]]),
beer2$id, function(z) length(unique(z)))) == 1))
const_ok
## [1] TRUE
Hallazgo importante. Los atributos del
producto(marca, precio, alcohol, carbohidratos, calorías, nacionalidad,
tipo, origen) son constantes dentro de cada cerveza (id).
Lo único que varía entre las reseñas de una misma cerveza son las cinco
valoraciones. Esto tiene una implicación directa para el análisis:
calcular el precio promedio sobre las 1.586.614 filas tal cual vienen
pondera el resultado por popularidad, no por el catálogo real, porque
una cerveza con muchas reseñas se cuenta muchas veces.
prod <- beer2[!duplicated(beer2$id), ] # 1 fila = 1 cerveza
nres <- tapply(beer2$calificacion, beer2$id, length)
prom <- sapply(ordinales, function(v) tapply(beer2[[v]], beer2$id, mean))
idx <- match(as.character(prod$id), names(nres))
prod$n_res <- as.numeric(nres[idx])
for (v in ordinales) prod[[paste0("m_", v)]] <- as.numeric(prom[, v][idx])
prod_f <- prod[prod$n_res >= 30, ] # cervezas con respaldo suficiente
rc <- sort(as.numeric(nres), decreasing = TRUE)
pct_1res <- 100 * mean(rc == 1)
pct_top1 <- 100 * sum(rc[1:round(length(rc) * 0.01)]) / n_res_tot
c(mediana_reseñas_por_cerveza = median(prod$n_res),
pct_cervezas_con_1_reseña = round(pct_1res, 1),
pct_reseñas_del_1pct_top = round(pct_top1, 1))
## mediana_reseñas_por_cerveza pct_cervezas_con_1_reseña
## 2.0 35.9
## pct_reseñas_del_1pct_top
## 37.9
La popularidad está repartida de forma muy desigual: la mediana es de 2 reseñas por cerveza, el 35.9 % de las cervezas tiene una sola, y el 1 % más reseñado concentra el 37.9 % de todos los registros. Por eso este informe trabaja con dos unidades de análisis:
| Unidad | n | Qué describe | Para qué sirve |
|---|---|---|---|
| Producto (una fila por cerveza) | 66.055 | precio, alcohol, carbohidratos, calorías, tipo, origen, nacionalidad, marca | Muestra el catálogo, cada cerveza pesa igual |
| Reseña (una fila por opinión) | 1.586.614 | calificación, aroma, apariencia, paladar, sabor | Muestra lo que opinan los consumidores |
Como puente entre las dos, se usa la calificación media por producto, calculada solo sobre cervezas con 30 o más reseñas, para que un producto con una sola opinión no compita con otro que tiene cientos.
moda <- function(x) {
t <- table(x)
names(t)[which.max(t)]
}
asim <- function(x) {
m <- mean(x); s <- sd(x)
mean((x - m)^3) / s^3
}
# Tabla de frecuencias para variables cualitativas nominales
tabla_frec <- function(x, etiqueta = "Categoría", ordenar = TRUE) {
t <- table(x)
if (ordenar) t <- sort(t, decreasing = TRUE)
d <- data.frame(
Categoria = names(t),
fa = as.numeric(t),
fr = round(100 * as.numeric(t) / sum(t), 2)
)
names(d) <- c(etiqueta, "Frec. absoluta", "Frec. relativa (%)")
d
}
# Tabla de frecuencias para variables ordinales (incluye acumuladas)
tabla_ord <- function(x, etiqueta = "Puntuación") {
t <- table(x)
d <- data.frame(
Cat = names(t),
fa = as.numeric(t),
fr = round(100 * as.numeric(t) / sum(t), 2),
faa = cumsum(as.numeric(t)),
fra = round(cumsum(100 * as.numeric(t) / sum(t)), 2)
)
names(d) <- c(etiqueta, "Frec. absoluta", "Frec. relativa (%)",
"Frec. abs. acum.", "Frec. rel. acum. (%)")
d
}
# Indicadores de tendencia central, posición y dispersión
desc_num <- function(x) {
q <- quantile(x, c(.10, .25, .50, .75, .90))
c(n = length(x), Media = mean(x), Mediana = as.numeric(q[3]),
D1 = as.numeric(q[1]), Q1 = as.numeric(q[2]), Q3 = as.numeric(q[4]),
D9 = as.numeric(q[5]), Minimo = min(x), Maximo = max(x), DE = sd(x),
RI = as.numeric(q[4] - q[2]), CV = 100 * sd(x) / mean(x), Asimetria = asim(x))
}
tabla_desc <- function(datos, vars) {
m <- t(sapply(datos[vars], desc_num))
d <- data.frame(Variable = rownames(m), round(m, 3), row.names = NULL,
check.names = FALSE)
names(d)[names(d) == "Minimo"] <- "Mínimo"
names(d)[names(d) == "Maximo"] <- "Máximo"
names(d)[names(d) == "Asimetria"] <- "Asimetría"
d
}
# Medias de precio/alcohol/carbohidratos/calorías por grupo (tabla compacta)
medias_grupo <- function(datos, grupo) {
g <- datos[[grupo]]
data.frame(
Grupo = levels(g),
n = as.numeric(table(g)),
`Precio (media)` = round(tapply(datos$precio, g, mean), 2),
`Alcohol g (media)` = round(tapply(datos$alcohol, g, mean), 2),
`Carbohidratos g (media)` = round(tapply(datos$carbohidratos, g, mean), 2),
`Calorías (media)` = round(tapply(datos$calorias, g, mean), 1),
check.names = FALSE, row.names = NULL)
}
# Calificación media por grupo (moda, mediana, cuartiles, media, DE)
ord_grupo <- function(datos, var, grupo) {
s <- split(datos[[var]], datos[[grupo]])
data.frame(
Grupo = names(s), n = sapply(s, length), Moda = sapply(s, moda),
Mediana = sapply(s, median),
Q1 = sapply(s, function(z) as.numeric(quantile(z, .25))),
Q3 = sapply(s, function(z) as.numeric(quantile(z, .75))),
Media = round(sapply(s, mean), 3), DE = round(sapply(s, sd), 3),
row.names = NULL)
}
tabla_kable <- function(d, caption, scroll = FALSE, height = "350px") {
k <- kable(d, caption = caption, row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
if (scroll) k <- k %>% scroll_box(height = height)
k
}
fmt <- function(x, d = 2) formatC(x, format = "f", digits = d,
big.mark = ".", decimal.mark = ",")
fmi <- function(x) formatC(x, format = "d", big.mark = ".", decimal.mark = ",")
# Paleta del portafolio
colores <- c("#C68642", "#F1C40F", "#7B3F00", "#F39C12", "#BDC3C7")
col_origen <- c("#F1C40F", "#F39C12")
col_base <- "#F6A623"
col_borde <- "#7B3F00"
esc_ord <- colorRampPalette(c("#F7E3B8", "#7B3F00"))
f_tipo_p <- tabla_frec(prod$tipo, "Tipo")
tabla_kable(f_tipo_p, "Tabla 3. Distribución de cervezas según tipo")
| Tipo | Frec. absoluta | Frec. relativa (%) |
|---|---|---|
| normal y helada | 32693 | 49.49 |
| clara artesanal | 19030 | 28.81 |
| lager importada | 11499 | 17.41 |
| baja en calorías | 1579 | 2.39 |
| lager artesanal | 1254 | 1.90 |
par(mai = c(1, 2, 1, .3))
t1 <- table(prod$tipo)
b <- barplot(t1, horiz = TRUE, las = 1, col = colores, border = NA,
xlim = c(0, max(t1) * 1.18),
main = "Cervezas únicas por tipo (n = 66.055 productos)",
xlab = "Número de cervezas")
text(t1, b, labels = paste0(fmi(t1), " (", round(100 * t1 / sum(t1), 1), "%)"),
pos = 4, cex = 0.8, col = "#5A3210", xpd = TRUE)
grid()
El portafolio está concentrado en dos tipos. La moda es “cerveza normal y helada”, con 32.693 productos — el 49,5 % del catálogo — y “clara artesanal” le sigue con el 28,8 %. “Lager artesanal” y “baja en calorías / sin alcohol” no llegan al 3 % cada una, así que cualquier indicador calculado sobre esos dos grupos tiene poco respaldo.
f_origen_p <- tabla_frec(prod$origen, "Origen")
f_origen_r <- tabla_frec(beer2$origen, "Origen")
d_origen <- merge(f_origen_p, f_origen_r, by = "Origen",
suffixes = c(" — productos", " — reseñas"))
tabla_kable(d_origen, "Tabla 4. Distribución del origen: productos frente a reseñas")
| Origen | Frec. absoluta — productos | Frec. relativa (%) — productos | Frec. absoluta — reseñas | Frec. relativa (%) — reseñas |
|---|---|---|---|---|
| importada | 59113 | 89.49 | 1412449 | 89.02 |
| nacional | 6942 | 10.51 | 174165 | 10.98 |
pct_import_p <- 100 * sum(prod$origen == "importada") / n_prod
pct_import_r <- 100 * sum(beer2$origen == "importada") / n_res_tot
t2 <- table(prod$origen)
lab <- paste0(names(t2), "\n", fmi(t2), " (", round(100 * t2 / sum(t2), 1), "%)")
par(mai = c(.6, .6, 1, .6))
pie(t2, labels = lab, col = col_origen, border = "white",
main = "Distribución de las cervezas por origen")
La moda es “importada”: 89,5 % de las cervezas, frente al 10,5 % de nacionales. La proporción es casi idéntica a nivel de reseña (89,0 % importadas), así que ningún origen se reseña más que otro.
origen depende totalmente de nacionalidad:
las cervezas de Estados Unidos son, sin excepción, las nacionales; las
otras nueve nacionalidades son, todas, importadas. La variable está
definida desde la perspectiva de Estados Unidos, no de Colombia.
f_pais_p <- tabla_frec(prod$nacionalidad, "Nacionalidad")
tabla_kable(f_pais_p, "Tabla 5. Distribución de cervezas según nacionalidad de la cervecería")
| Nacionalidad | Frec. absoluta | Frec. relativa (%) |
|---|---|---|
| Estados Unidos | 6942 | 10.51 |
| Bélgica | 6868 | 10.40 |
| Francia | 6815 | 10.32 |
| Inglaterra | 6715 | 10.17 |
| Japón | 6698 | 10.14 |
| Canadá | 6695 | 10.14 |
| República Checa | 6488 | 9.82 |
| Austria | 6473 | 9.80 |
| Irlanda | 6246 | 9.46 |
| Alemania | 6115 | 9.26 |
Las diez nacionalidades están repartidas de forma pareja, entre el 9,3 % y el 10,5 % de los productos. Un mercado real no suele verse así de uniforme, lo que sugiere que esta variable fue construida para el ejercicio.
tab_marca <- tabla_frec(beer2$marca, "Marca")
tabla_kable(head(tab_marca, 10), "Tabla 6. Las 10 marcas con más reseñas")
| Marca | Frec. absoluta | Frec. relativa (%) |
|---|---|---|
| Boston Beer Company (Samuel Adams) | 39444 | 2.49 |
| Dogfish Head Brewery | 33839 | 2.13 |
| Stone Brewing Co. | 33066 | 2.08 |
| Sierra Nevada Brewing Co. | 28751 | 1.81 |
| Bell’s Brewery, Inc. | 25191 | 1.59 |
| Rogue Ales | 24083 | 1.52 |
| Founders Brewing Company | 20004 | 1.26 |
| Victory Brewing Company | 19479 | 1.23 |
| Lagunitas Brewing Company | 16837 | 1.06 |
| Avery Brewing Company | 16107 | 1.02 |
par(mai = c(1, 2.6, 1, .3))
t <- sort(table(beer2$marca), decreasing = TRUE)[15:1]
names(t) <- ifelse(nchar(names(t)) > 26, paste0(substr(names(t), 1, 24), "…"), names(t))
barplot(t, horiz = TRUE, las = 1, col = col_base, border = NA,
main = "15 marcas con más reseñas", xlab = "Número de reseñas", cex.axis = 0.7)
grid()
Las marcas sí se comportan como un mercado real: 5.743 marcas se reparten 66.055 productos, con muchísimas marcas de un solo producto y unas pocas que concentran la mayoría de las reseñas.
ct <- table(prod$tipo, prod$origen)
d_cruce <- data.frame(Tipo = rownames(ct),
Nacional = as.numeric(ct[, "nacional"]),
Importada = as.numeric(ct[, "importada"]),
`% nacional` = round(100 * ct[, "nacional"] / rowSums(ct), 1),
`% importada` = round(100 * ct[, "importada"] / rowSums(ct), 1),
check.names = FALSE, row.names = NULL)
tabla_kable(d_cruce, "Tabla 7. Composición del origen dentro de cada tipo")
| Tipo | Nacional | Importada | % nacional | % importada |
|---|---|---|---|---|
| lager artesanal | 1254 | 0 | 100.0 | 0.0 |
| clara artesanal | 2019 | 17011 | 10.6 | 89.4 |
| lager importada | 0 | 11499 | 0.0 | 100.0 |
| normal y helada | 3510 | 29183 | 10.7 | 89.3 |
| baja en calorías | 159 | 1420 | 10.1 | 89.9 |
par(mai = c(1.9, 1.2, 1, .4))
b <- barplot(t(prop.table(ct, 1)) * 100, col = col_origen, border = "white", las = 2,
ylim = c(0, 110), ylab = "Porcentaje dentro del tipo (%)",
main = "Composición del origen dentro de cada tipo",
names.arg = levels(prod$tipo), cex.names = 0.8)
legend(x = mean(range(b)), y = 125.5, xjust = 0.5, horiz = TRUE,
legend = c("nacional", "importada"), fill = col_origen,
border = NA, bty = "n", xpd = NA)
“Lager importada” es 100 % importada y “lager artesanal” es 100 % nacional. En esos dos tipos, tipo y origen son la misma información contada dos veces, así que cualquier diferencia entre orígenes ahí arrastra también una diferencia de tipo. Esta dependencia condiciona las comparaciones de la sección 6.
Calificación, aroma, apariencia, paladar y sabor se valoran en una escala de 0 a 5, en pasos de 0,5.
res_ord <- data.frame(
Variable = ordinales, n = n_res_tot,
Moda = sapply(ordinales, function(v) moda(beer2[[v]])),
Mediana = sapply(ordinales, function(v) median(beer2[[v]])),
Q1 = sapply(ordinales, function(v) as.numeric(quantile(beer2[[v]], .25))),
Q3 = sapply(ordinales, function(v) as.numeric(quantile(beer2[[v]], .75))),
Media = round(sapply(ordinales, function(v) mean(beer2[[v]])), 3),
DE = round(sapply(ordinales, function(v) sd(beer2[[v]])), 3),
Asimetria = round(sapply(ordinales, function(v) asim(beer2[[v]])), 3),
row.names = NULL)
names(res_ord)[names(res_ord) == "Asimetria"] <- "Asimetría"
tabla_kable(res_ord, "Tabla 8. Indicadores de las cinco valoraciones (n = 1.586.614 reseñas)")
| Variable | n | Moda | Mediana | Q1 | Q3 | Media | DE | Asimetría |
|---|---|---|---|---|---|---|---|---|
| calificacion | 1586614 | 4 | 4 | 3.5 | 4.5 | 3.816 | 0.721 | -1.024 |
| aroma | 1586614 | 4 | 4 | 3.5 | 4.0 | 3.736 | 0.698 | -0.839 |
| apariencia | 1586614 | 4 | 4 | 3.5 | 4.0 | 3.842 | 0.616 | -0.902 |
| paladar | 1586614 | 4 | 4 | 3.5 | 4.0 | 3.744 | 0.682 | -0.869 |
| sabor | 1586614 | 4 | 4 | 3.5 | 4.5 | 3.793 | 0.732 | -0.973 |
par(mfrow = c(2, 3), mai = c(.55, .55, .45, .15))
for (i in seq_along(ordinales)) {
t <- table(beer2[[ordinales[i]]])
barplot(100 * t / sum(t), col = esc_ord(length(t)), border = NA, las = 1,
main = tit[i], xlab = "Puntuación", ylab = "%", cex.main = 1.05, cex.axis = 0.75)
abline(h = 0)
}
plot.new()
legend("center", legend = c("n = 1.586.614 reseñas", "Escala 0 a 5",
"Moda = 4,0 en las cinco"), bty = "n", text.col = "#5A3210")
par(mfrow = c(1, 1))
tabla_kable(tabla_ord(beer2$calificacion, "Calificación"),
"Tabla 9. Distribución de frecuencias de la calificación general")
| Calificación | Frec. absoluta | Frec. relativa (%) | Frec. abs. acum. | Frec. rel. acum. (%) |
|---|---|---|---|---|
| 0 | 7 | 0.00 | 7 | 0.00 |
| 1 | 10954 | 0.69 | 10961 | 0.69 |
| 1.5 | 12975 | 0.82 | 23936 | 1.51 |
| 2 | 38225 | 2.41 | 62161 | 3.92 |
| 2.5 | 58523 | 3.69 | 120684 | 7.61 |
| 3 | 165644 | 10.44 | 286328 | 18.05 |
| 3.5 | 301817 | 19.02 | 588145 | 37.07 |
| 4 | 582764 | 36.73 | 1170909 | 73.80 |
| 4.5 | 324385 | 20.45 | 1495294 | 94.24 |
| 5 | 91320 | 5.76 | 1586614 | 100.00 |
Las cinco variables comparten el mismo patrón: en todas, moda y mediana son 4,0, y el rango intercuartílico va de 3,5 a 4,0 o 4,5. La distribución es asimétrica hacia la izquierda (entre -1,02 y -0,84): las puntuaciones bajas existen, pero son la excepción. En la calificación general, el valor 4,0 por sí solo reúne el 36,7 % de las reseñas.
Esto es común en reseñas voluntarias, y tiene una consecuencia práctica: la escala distingue poco entre las cervezas mejor valoradas, porque casi todas las respuestas están apretadas en el mismo tramo.
No todos los atributos pesan igual. Apariencia es el más generoso y el más parejo (media 3,84), así que aporta poco para diferenciar una cerveza de otra. Sabor es el más exigente (desviación 0,73) y aroma tiene la media más baja (3,74). La diferenciación real pasa por sabor y aroma, no por apariencia.
d_prod <- tabla_desc(prod, cuanti)
tabla_kable(d_prod, "Tabla 10. Indicadores estadísticos de precio, alcohol, carbohidratos y calorías (nivel producto)")
| Variable | n | Media | Mediana | D1 | Q1 | Q3 | D9 | Mínimo | Máximo | DE | RI | CV | Asimetría |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| precio | 66055 | 5.242 | 5.31 | 4.24 | 4.79 | 5.77 | 6.15 | 2.20 | 8.03 | 0.758 | 0.98 | 14.467 | -0.492 |
| alcohol | 66055 | 16.718 | 15.34 | 12.42 | 13.75 | 18.94 | 23.82 | 0.03 | 59.45 | 4.588 | 5.19 | 27.445 | 1.201 |
| carbohidratos | 66055 | 13.278 | 13.20 | 10.80 | 11.80 | 14.50 | 16.00 | 3.50 | 30.20 | 2.130 | 2.70 | 16.038 | 0.261 |
| calorias | 66055 | 176.139 | 166.00 | 139.00 | 151.00 | 195.00 | 233.00 | 21.00 | 541.00 | 39.292 | 44.00 | 22.308 | 1.083 |
d_prod_m <- t(sapply(prod[cuanti], desc_num)) # versión matriz para las citas en el texto
par(mfrow = c(2, 1), mai = c(.85, 1, .7, .3))
hist(prod$precio, breaks = 40, col = col_base, border = "white", las = 1,
main = "Precio (caja de seis unidades) — nivel producto",
xlab = "Precio", ylab = "Frecuencia")
abline(v = mean(prod$precio), col = col_borde, lwd = 2)
abline(v = median(prod$precio), col = "#C0392B", lwd = 2, lty = 2)
legend("topleft", c("Media", "Mediana"), col = c(col_borde, "#C0392B"),
lwd = 2, lty = c(1, 2), bty = "n", cex = 0.8)
grid()
boxplot(prod$precio, horizontal = TRUE, col = col_base, border = col_borde,
las = 1, xlab = "Precio")
grid()
par(mfrow = c(1, 1))
El precio tiene una media de 5,24 y una mediana de 5,31, con un coeficiente de variación de 14,5 %: es la variable más homogénea del estudio. La asimetría es casi nula (-0,49), la distribución es simétrica, y aquí media y desviación estándar funcionan bien como resumen.
par(mfrow = c(2, 1), mai = c(.85, 1, .7, .3))
hist(prod$alcohol, breaks = 60, col = col_base, border = "white", las = 1,
main = "Alcohol puro (g por 355 ml) — nivel producto",
xlab = "Gramos de alcohol", ylab = "Frecuencia")
abline(v = mean(prod$alcohol), col = col_borde, lwd = 2)
abline(v = median(prod$alcohol), col = "#C0392B", lwd = 2, lty = 2)
grid()
boxplot(prod$alcohol, horizontal = TRUE, col = col_base, border = col_borde,
las = 1, xlab = "Gramos de alcohol")
grid()
par(mfrow = c(1, 1))
Alcohol y calorías se comportan distinto: tienen asimetría positiva marcada (1,20 y 1,08) por unas pocas cervezas muy fuertes — el máximo de alcohol equivale a cerca de 21 % vol. Con esa cola, la mediana y el rango intercuartílico describen mejor el catálogo que la media, que los extremos inflan. El grado alcohólico mediano es 5,48 % vol., una cerveza estándar.
cor_prod <- cor(prod[cuanti])
ident_cal <- mean(round(7 * prod$alcohol + 4 * prod$carbohidratos + 6) == prod$calorias)
c(pct_que_cumple_la_formula = round(100 * ident_cal, 1),
correlacion_alcohol_calorias = round(cor_prod["alcohol", "calorias"], 3))
## pct_que_cumple_la_formula correlacion_alcohol_calorias
## 100.000 0.992
Las calorías se calculan directamente a partir del alcohol y los carbohidratos. En el 100 % de los productos se cumple, exacto, la fórmula
\[\text{calorías} = \text{redondeo}(7 \times \text{alcohol} + 4 \times \text{carbohidratos} + 6)\]
7 y 4 son los factores calóricos estándar por gramo de alcohol y de carbohidratos. La correlación entre alcohol y calorías es 0,992: prácticamente perfecta. Para cualquier análisis posterior conviene usar alcohol y carbohidratos, y dejar las calorías solo como dato de etiqueta.
d_res <- tabla_desc(beer2, cuanti)
d_res_m <- t(sapply(beer2[cuanti], desc_num))
d_niveles <- data.frame(
Variable = cuanti,
`Media producto` = round(d_prod_m[, "Media"], 3),
`Media reseña` = round(d_res_m[, "Media"], 3),
Diferencia = round(d_res_m[, "Media"] - d_prod_m[, "Media"], 3),
check.names = FALSE, row.names = NULL)
tabla_kable(d_niveles, "Tabla 11. Comparación de medias entre nivel producto y nivel reseña")
| Variable | Media producto | Media reseña | Diferencia |
|---|---|---|---|
| precio | 5.242 | 5.363 | 0.121 |
| alcohol | 16.718 | 18.712 | 1.994 |
| carbohidratos | 13.278 | 14.048 | 0.769 |
| calorias | 176.139 | 193.173 | 17.034 |
El precio medio a nivel de reseña es 5,36, frente a 5,24 a nivel de producto. Las cervezas más reseñadas son más caras y más alcohólicas que el catálogo completo, así que trabajar solo con reseñas sobrerrepresenta ese segmento.
tabla_kable(medias_grupo(prod, "tipo"), "Tabla 12. Precio y composición nutricional según tipo de cerveza")
| Grupo | n | Precio (media) | Alcohol g (media) | Carbohidratos g (media) | Calorías (media) |
|---|---|---|---|---|---|
| lager artesanal | 1254 | 4.43 | 15.32 | 13.52 | 167.3 |
| clara artesanal | 19030 | 5.60 | 16.41 | 13.91 | 176.5 |
| lager importada | 11499 | 5.08 | 15.34 | 12.52 | 163.5 |
| normal y helada | 32693 | 5.16 | 17.82 | 13.41 | 184.4 |
| baja en calorías | 1579 | 4.41 | 8.84 | 8.18 | 100.6 |
par(mai = c(1, 2, 1, .3))
boxplot(prod$precio ~ prod$tipo, horizontal = TRUE, col = colores, las = 1,
xlab = "Precio", ylab = " ", main = "Distribución del precio por tipo de cerveza")
grid()
m <- sapply(ordinales, function(v) round(tapply(beer2[[v]], beer2$tipo, mean), 3))
d_valtipo <- data.frame(Tipo = rownames(m), m, row.names = NULL, check.names = FALSE)
names(d_valtipo)[-1] <- tit
tabla_kable(d_valtipo, "Tabla 13. Valoración media de cada atributo según tipo de cerveza")
| Tipo | Calificación general | Aroma | Apariencia | Paladar | Sabor |
|---|---|---|---|---|---|
| lager artesanal | 3.640 | 3.366 | 3.591 | 3.485 | 3.492 |
| clara artesanal | 3.876 | 3.784 | 3.870 | 3.785 | 3.821 |
| lager importada | 3.610 | 3.340 | 3.546 | 3.469 | 3.479 |
| normal y helada | 3.862 | 3.852 | 3.936 | 3.827 | 3.897 |
| baja en calorías | 2.954 | 2.585 | 2.817 | 2.681 | 2.644 |
cal_tipo <- ord_grupo(beer2, "calificacion", "tipo")
par(mai = c(1.9, 1.1, 1, .4))
m2 <- sapply(ordinales, function(v) tapply(beer2[[v]], beer2$tipo, mean))
b <- barplot(t(m2), beside = TRUE, col = esc_ord(5), border = NA, las = 2,
ylim = c(0, 5.6), yaxt = "n", ylab = "Puntuación media",
main = "Puntuación media de cada atributo por tipo",
names.arg = levels(prod$tipo), cex.names = 0.8)
axis(2, at = 0:4, las = 1)
legend(x = mean(range(b)), y = 5.9, xjust = 0.5, ncol = 3, legend = tit,
fill = esc_ord(5), border = NA, bty = "n", xpd = NA, cex = 0.85)
abline(h = 0)
El tipo de cerveza marca diferencias claras, sobre todo en el precio y la composición. La clara artesanal es la más costosa, mientras que la lager artesanal y la baja en calorías son las más económicas. Además, la cerveza baja en calorías o sin alcohol se distingue por tener aproximadamente la mitad de calorías y alcohol que las demás.
También es la que recibe las calificaciones más bajas. Mientras los otros tipos tienen puntuaciones bastante parecidas, la baja en calorías queda casi un punto por debajo y presenta valoraciones más variadas. En pocas palabras, es el tipo de cerveza que más se aleja del resto del catálogo.
tabla_kable(medias_grupo(prod, "origen"), "Tabla 14. Precio y composición nutricional según origen")
| Grupo | n | Precio (media) | Alcohol g (media) | Carbohidratos g (media) | Calorías (media) |
|---|---|---|---|---|---|
| nacional | 6942 | 4.34 | 16.62 | 13.40 | 176.0 |
| importada | 59113 | 5.35 | 16.73 | 13.26 | 176.2 |
par(mai = c(1, 1.6, 1, .3))
boxplot(prod$precio ~ prod$origen, horizontal = TRUE, col = col_origen, las = 1,
xlab = "Precio", ylab = " ", main = "Distribución del precio por origen")
grid()
cal_pais <- data.frame(
Nacionalidad = names(tapply(beer2$calificacion, beer2$nacionalidad, mean)),
n = as.numeric(tapply(beer2$calificacion, beer2$nacionalidad, length)),
Media = round(as.numeric(tapply(beer2$calificacion, beer2$nacionalidad, mean)), 3),
Mediana = as.numeric(tapply(beer2$calificacion, beer2$nacionalidad, median)),
DE = round(as.numeric(tapply(beer2$calificacion, beer2$nacionalidad, sd)), 3))
cal_pais <- cal_pais[order(-cal_pais$Media), ]
tabla_kable(cal_pais, "Tabla 15. Calificación media según nacionalidad de la cervecería")
| Nacionalidad | n | Media | Mediana | DE |
|---|---|---|---|---|
| Inglaterra | 162644 | 3.883 | 4 | 0.700 |
| Francia | 181574 | 3.853 | 4 | 0.713 |
| Estados Unidos | 174165 | 3.851 | 4 | 0.679 |
| Bélgica | 188677 | 3.845 | 4 | 0.693 |
| Irlanda | 120742 | 3.825 | 4 | 0.700 |
| Alemania | 121684 | 3.816 | 4 | 0.713 |
| Japón | 155273 | 3.816 | 4 | 0.686 |
| República Checa | 120406 | 3.790 | 4 | 0.740 |
| Austria | 201941 | 3.765 | 4 | 0.782 |
| Canadá | 159508 | 3.706 | 4 | 0.766 |
cal_origen <- ord_grupo(beer2, "calificacion", "origen")
par(mai = c(1, 1.8, 1, .3))
d <- cal_pais[order(cal_pais$Media), ]
b <- barplot(d$Media, horiz = TRUE, names.arg = d$Nacionalidad, las = 1,
col = col_base, border = NA, xlim = c(0, 4.6), xlab = "Calificación media",
main = "Calificación media según nacionalidad de la cervecería")
text(d$Media, b, labels = fmt(d$Media), pos = 4, cex = 0.8, col = "#5A3210", xpd = TRUE)
grid()
En precio la diferencia es clara: las importadas cuestan alrededor de un 23 % más que las nacionales. En composición nutricional casi no hay diferencia. En valoración tampoco: 3,85 las nacionales frente a 3,81 las importadas, con la misma moda y mediana. El origen marca el precio, no la satisfacción.
d_corprod <- data.frame(Variable = rownames(cor_prod), round(cor_prod, 3),
row.names = NULL, check.names = FALSE)
tabla_kable(d_corprod, "Tabla 16. Correlación de Pearson entre precio, alcohol, carbohidratos y calorías")
| Variable | precio | alcohol | carbohidratos | calorias |
|---|---|---|---|---|
| precio | 1.000 | 0.121 | 0.174 | 0.137 |
| alcohol | 0.121 | 1.000 | 0.803 | 0.992 |
| carbohidratos | 0.174 | 0.803 | 1.000 | 0.873 |
| calorias | 0.137 | 0.992 | 0.873 | 1.000 |
par(mai = c(1.1, 1.2, 1, .3))
set.seed(2024)
s <- prod[sample(nrow(prod), 8000), ]
plot(s$alcohol, s$calorias, pch = 21, bg = col_base, col = col_borde, cex = .7, lwd = .5,
las = 1, xlab = "Alcohol puro (g por 355 ml)", ylab = "Calorías (por 355 ml)",
main = "Calorías frente a alcohol (muestra de 8.000 cervezas)")
legend("topleft", paste0("r de Pearson = ", fmt(cor_prod["alcohol", "calorias"], 3)), bty = "n")
grid()
set.seed(2024)
mrho <- beer2[sample(n_res_tot, 200000), c(ordinales, cuanti)]
cor_res <- cor(mrho, method = "spearman")
d_corres <- data.frame(Variable = rownames(cor_res), round(cor_res, 3),
row.names = NULL, check.names = FALSE)
tabla_kable(d_corres, "Tabla 17. Correlación de Spearman entre valoraciones y características (muestra de 200.000 reseñas)", scroll = TRUE)
| Variable | calificacion | aroma | apariencia | paladar | sabor | precio | alcohol | carbohidratos | calorias |
|---|---|---|---|---|---|---|---|---|---|
| calificacion | 1.000 | 0.558 | 0.454 | 0.654 | 0.730 | 0.242 | 0.178 | 0.157 | 0.180 |
| aroma | 0.558 | 1.000 | 0.496 | 0.564 | 0.669 | 0.246 | 0.384 | 0.327 | 0.384 |
| apariencia | 0.454 | 0.496 | 1.000 | 0.515 | 0.492 | 0.205 | 0.311 | 0.254 | 0.309 |
| paladar | 0.654 | 0.564 | 0.515 | 1.000 | 0.689 | 0.238 | 0.333 | 0.285 | 0.333 |
| sabor | 0.730 | 0.669 | 0.492 | 0.689 | 1.000 | 0.249 | 0.348 | 0.295 | 0.347 |
| precio | 0.242 | 0.246 | 0.205 | 0.238 | 0.249 | 1.000 | 0.139 | 0.171 | 0.153 |
| alcohol | 0.178 | 0.384 | 0.311 | 0.333 | 0.348 | 0.139 | 1.000 | 0.817 | 0.990 |
| carbohidratos | 0.157 | 0.327 | 0.254 | 0.285 | 0.295 | 0.171 | 0.817 | 1.000 | 0.885 |
| calorias | 0.180 | 0.384 | 0.309 | 0.333 | 0.347 | 0.153 | 0.990 | 0.885 | 1.000 |
par(mai = c(1.2, 1.2, 1, .3))
r <- cor_res["calificacion", c("sabor", "paladar", "aroma", "apariencia")]
b <- barplot(as.numeric(r), col = esc_ord(4), border = NA, las = 1, ylim = c(0, 1),
names.arg = c("Sabor", "Paladar", "Aroma", "Apariencia"),
ylab = "Correlación de Spearman con la calificación general",
main = "Asociación de cada atributo con la calificación general")
text(b, as.numeric(r), labels = fmt(as.numeric(r), 3), pos = 3, cex = 0.85,
col = "#5A3210", xpd = TRUE)
abline(h = 0)
La calificación general se mueve sobre todo con el sabor (correlación0,730) y el paladar (0,654); la apariencia pesa casi nada (0,454). Sabor y paladar son los que realmente definen la experiencia.
q_precio <- cut(beer2$precio, breaks = quantile(beer2$precio, seq(0, 1, .2)),
include.lowest = TRUE,
labels = c("Q1 (más bajo)", "Q2", "Q3", "Q4", "Q5 (más alto)"))
q_alc <- cut(beer2$alcohol, breaks = quantile(beer2$alcohol, seq(0, 1, .2)),
include.lowest = TRUE,
labels = c("Q1 (menor)", "Q2", "Q3", "Q4", "Q5 (mayor)"))
cal_qp <- tapply(beer2$calificacion, q_precio, mean)
cal_qa <- tapply(beer2$calificacion, q_alc, mean)
par(mfrow = c(1, 2), mai = c(1.1, 1.1, 1, .2))
b <- barplot(as.numeric(cal_qp), col = esc_ord(5), border = NA, las = 1, ylim = c(0, 4.5),
names.arg = names(cal_qp), cex.names = 0.75, ylab = "Calificación media",
main = "Según quintil de precio")
text(b, as.numeric(cal_qp), labels = fmt(as.numeric(cal_qp)), pos = 3, cex = 0.8,
col = "#5A3210", xpd = TRUE)
abline(h = 0)
b <- barplot(as.numeric(cal_qa), col = esc_ord(5), border = NA, las = 1, ylim = c(0, 4.5),
names.arg = names(cal_qa), cex.names = 0.75, ylab = "Calificación media",
main = "Según quintil de alcohol")
text(b, as.numeric(cal_qa), labels = fmt(as.numeric(cal_qa)), pos = 3, cex = 0.8,
col = "#5A3210", xpd = TRUE)
abline(h = 0)
par(mfrow = c(1, 1))
La relación entre el precio y la calificación es positiva, aunque moderada.A medida que aumenta el quintil de precio, también lo hace la calificación promedio, pasando de 3,48 a 4,04 en el más costoso. Sin embargo,este resultado debe interpretarse con cuidado porque probablemente refleja diferencias entre segmentos y perfiles de los productos, más que un efecto directo del precio sobre la satisfacción que es lo que buscamos medir.
En el caso del alcohol, el comportamiento es distinto. La calificación mejora principalmente en los primeros quintiles, al pasar de 3,58 a 3,89, pero a partir del tercer quintil el aumento es mínimo y la tendencia prácticamente se estabiliza. Esto podría estar relacionado con la menor valoración de los productos sin alcohol, mientras que entre los productos con niveles medios o altos de alcohol las diferencias en calificación son muy reducidas.
top_p <- prod[prod$n_res >= 100, ]
top_p <- top_p[order(-top_p$m_calificacion), ]
rank_alto <- head(data.frame(Cerveza = top_p$nombre_cerveza, Marca = top_p$marca,
Tipo = as.character(top_p$tipo), Origen = as.character(top_p$origen),
Precio = top_p$precio, `N reseñas` = top_p$n_res,
`Calificación media` = round(top_p$m_calificacion, 3),
check.names = FALSE), 10)
bot_p <- top_p[order(top_p$m_calificacion), ]
rank_bajo <- head(data.frame(Cerveza = bot_p$nombre_cerveza, Marca = bot_p$marca,
Tipo = as.character(bot_p$tipo), Origen = as.character(bot_p$origen),
Precio = bot_p$precio, `N reseñas` = bot_p$n_res,
`Calificación media` = round(bot_p$m_calificacion, 3),
check.names = FALSE), 10)
tabla_kable(rank_alto, "Tabla 18. Las 10 cervezas mejor valoradas (con 100 o más reseñas)", scroll = TRUE)
| Cerveza | Marca | Tipo | Origen | Precio | N reseñas | Calificación media |
|---|---|---|---|---|---|---|
| Citra DIPA | Kern River Brewing Company | clara artesanal | importada | 5.31 | 252 | 4.631 |
| Cantillon Blåbær Lambik | Brasserie Cantillon | normal y helada | importada | 5.73 | 156 | 4.628 |
| Heady Topper | The Alchemist | clara artesanal | importada | 7.05 | 469 | 4.626 |
| Deviation - Bottleworks 9th Anniversary | Russian River Brewing Company | normal y helada | importada | 6.43 | 112 | 4.621 |
| Trappist Westvleteren 12 | Brouwerij Westvleteren (Sint-Sixtusabdij van Westvleteren) | normal y helada | importada | 5.91 | 1272 | 4.618 |
| Pliny The Younger | Russian River Brewing Company | clara artesanal | importada | 6.51 | 610 | 4.600 |
| Founders CBS Imperial Stout | Founders Brewing Company | normal y helada | importada | 6.37 | 637 | 4.591 |
| Pliny The Elder | Russian River Brewing Company | clara artesanal | importada | 5.94 | 2527 | 4.590 |
| Live Oak HefeWeizen | Live Oak Brewing Company | clara artesanal | importada | 6.35 | 322 | 4.562 |
| Portsmouth Kate The Great | Portsmouth Brewery | normal y helada | importada | 5.66 | 428 | 4.547 |
tabla_kable(rank_bajo, "Tabla 19. Las 10 cervezas peor valoradas (con 100 o más reseñas)", scroll = TRUE)
| Cerveza | Marca | Tipo | Origen | Precio | N reseñas | Calificación media |
|---|---|---|---|---|---|---|
| Crazy Ed’s Cave Creek Chili Beer | Chili Beer Co. | normal y helada | importada | 3.49 | 267 | 1.489 |
| Original C Cave Creek Chili Beer - Cerveza Con Chili | Chili Beer Co. | normal y helada | importada | 3.37 | 132 | 1.682 |
| Bud Light Chelada | Anheuser-Busch | normal y helada | importada | 3.89 | 114 | 1.711 |
| Wild Blue (Blueberry Lager) | Anheuser-Busch | normal y helada | importada | 4.23 | 482 | 1.924 |
| Budweiser Chelada | Anheuser-Busch | normal y helada | importada | 3.38 | 138 | 1.931 |
| Bud Extra | Anheuser-Busch | normal y helada | importada | 3.79 | 128 | 1.953 |
| Corona Light | Grupo Modelo S.A. de C.V. | baja en calorías | importada | 3.49 | 342 | 1.958 |
| Brewer Patriot Collection - 1790 Root Beer Brew | Boston Beer Company (Samuel Adams) | normal y helada | importada | 4.24 | 196 | 2.089 |
| Michelob Ultra | Anheuser-Busch | baja en calorías | importada | 4.10 | 523 | 2.160 |
| Budweiser Select 55 | Anheuser-Busch | baja en calorías | importada | 3.76 | 106 | 2.226 |
Las cervezas mejor valoradas con suficientes reseñas son, en su mayoría, producciones artesanales de tirada limitada y precio alto. Las peor valoradas son, de forma recurrente, productos de gran volumen de la categoría baja en calorías. Estos rankings reflejan la opinión de los compradores que escriben reseñas no de la poblaciónq ue consume en general.
beer2.csv), con la variable precio simulada
para fines académicos.