1 Planteamiento del problema

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?

1.1 Objetivos

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.

  1. Caracterizar la distribución del catálogo según tipo, origen, nacionalidad y marca.
  2. Describir las cinco valoraciones sensoriales mediante tablas de frecuencia, moda, mediana y cuartiles.
  3. Describir el precio, el alcohol, los carbohidratos y las calorías mediante medidas de tendencia central, dispersión y forma.
  4. Comparar los indicadores anteriores entre tipos y entre orígenes de cerveza.
  5. Explorar la relación entre las características de una cerveza y su valoración.

2 Descripción de la base de datos

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)
Tabla 1. Variables de la base de datos
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.

3 Carga y depuración de los datos

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

3.1 Verificación de la calidad de los datos

# 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)
Tabla 2. Datos faltantes por variable
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.

3.2 Dos unidades de análisis

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.

3.3 Funciones auxiliares

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"))

4 Análisis de variables cualitativas

4.1 Tipo de cerveza

f_tipo_p <- tabla_frec(prod$tipo, "Tipo")
tabla_kable(f_tipo_p, "Tabla 3. Distribución de cervezas según tipo")
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.

4.2 Origen

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")
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.

4.3 Nacionalidad

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")
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.

4.4 Marca

tab_marca <- tabla_frec(beer2$marca, "Marca")
tabla_kable(head(tab_marca, 10), "Tabla 6. Las 10 marcas con más reseñas")
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.

4.5 Relación entre tipo y origen

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")
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.

5 Análisis de variables ordinales

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)")
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")
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.

6 Análisis de variables cuantitativas

6.1 Indicadores generales

d_prod <- tabla_desc(prod, cuanti)
tabla_kable(d_prod, "Tabla 10. Indicadores estadísticos de precio, alcohol, carbohidratos y calorías (nivel producto)")
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.

6.2 Las calorías no aportan nada nuevo

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.

6.3 La unidad de análisis cambia los números

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")
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.

7 Comparaciones entre tipo y origen

7.1 Por tipo de cerveza

tabla_kable(medias_grupo(prod, "tipo"), "Tabla 12. Precio y composición nutricional según tipo de cerveza")
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")
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.

7.2 Por origen

tabla_kable(medias_grupo(prod, "origen"), "Tabla 14. Precio y composición nutricional según 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")
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.

8 Relaciones entre variables

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")
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)
Tabla 17. Correlación de Spearman entre valoraciones y características (muestra de 200.000 reseñas)
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)
Tabla 18. Las 10 cervezas mejor valoradas (con 100 o más reseñas)
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)
Tabla 19. Las 10 cervezas peor valoradas (con 100 o más reseñas)
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.

9 Conclusiones

  1. El tipo de cerveza es el criterio que más ordena el catálogo: separa precio, composición nutricional y valoración a la vez, más que el origen o la nacionalidad.
  2. La categoría baja en calorías / sin alcohol es el hallazgo más claro del estudio.Cumple su promesa nutricional pero recibe la peor valoración en las cinco escalas, casi un punto por debajo del resto.
  3. Sabor y paladar gobiernan la valoración general; la apariencia funciona como umbral, pero claramente no como un diferenciador.
  4. El origen separa precios, no satisfacción: las importadas cuestan alrededor de un 23 % más con la misma composición nutricional y una valoración prácticamente igual a las locales.
  5. Precio y calificación se mueven juntos, no es muy fuerte la conexión pero se mueven de forma ordenada. Es una hipótesis por observar, no está comprobada.
  6. Las calorías son redundantes: se derivan por completo del alcohol y los carbohidratos.
  7. La escala de valoración distingue poco en la parte alta. Conviene mirar la distribución completa y no solo la media al comparar categorías.

10 Limitaciones del estudio

  1. El precio es una variable simulada y describe solo esta base académica; ninguna cifra que dependa de él debería usarse como referencia de mercado real.
  2. Todos los resultados son descriptivos. Ninguno prueba causalidad: que las cervezas más caras tengan mejores calificaciones no significa que el precio mejore la experiencia.
  3. Tipo y origen están confundidos por construcción: “lager importada” es 100 % importada y “lager artesanal” es 100 % nacional, así que toda diferencia entre orígenes en esos dos grupos arrastra también una diferencia de tipo.4. La etiqueta “nacional” corresponde exactamente a Estados Unidos, no a Colombia.
  4. Las reseñas tienen sesgo de selección: las escribe quien decide opinar, no el consumidor promedio, y el 35,9 % de las cervezas tiene una sola reseña, así que sus promedios son poco confiables.
  5. “Lager artesanal” y “baja en calorías / sin alcohol” reúnen menos del 3 % de las cervezas cada una; sus indicadores son más inestables que los del resto.
  6. Las cinco valoraciones son ordinales: sus medias se reportan como referencia, pero las conclusiones se apoyan sobre todo en modas, medianas y cuartiles.

11 Referencias

  • Base de datos suministrada para el curso Teoría de Probabilidades (beer2.csv), con la variable precio simulada para fines académicos.