Resumen

Mariana del Río Ramírez · Teoría de Probabilidades · 8 de septiembre de 2026

Column

Tipo de cerveza

Origen

Calificación

Precio

Column

Reseñas

1.586.614

Cervezas únicas

66.055

Marcas

5.743

Precio promedio

5,24

Cerveza importada

Cualitativas

Column

Tipo × Origen

Marcas

Column

Tipo (productos)

Tipo (reseñas)

Origen

Nacionalidad

Marcas

Tipo × Origen

Ordinales

Column

Las cinco valoraciones

Calificación por tipo

Column

Resumen ordinal

Calificación

Aroma

Apariencia

Paladar

Sabor

Cuantitativas

Column

Precio

Alcohol

Column

Indicadores (producto)

Indicadores (reseña)

Comparación de niveles

Reseñas por cerveza

Por tipo

Column

Precio

Valoraciones

Column

Precio

Calorías

Alcohol

Carbohidratos

Calificación

Todas las valoraciones

Por origen

Column

Precio

Nacionalidad

Column

Precio

Alcohol

Carbohidratos

Calorías

Valoraciones

Nacionalidad

Relaciones

Column

Calorías × Alcohol

Calificación × Precio

Calificación × Alcohol

Atributos sensoriales

Column

Correlaciones (producto)

Correlaciones (Spearman)

Mejor valoradas

Peor valoradas

Informe

Column

1. Objetivo y método

Propósito

La empresa quiere entender qué prefieren sus consumidores y qué características de una cerveza se asocian con mejores valoraciones. Este es un diagnóstico descriptivo: sirve para ver patrones y plantear hipótesis, no para probar qué causa qué.

La base de datos

1.586.614 reseñas, 15 variables, sin datos faltantes. Cada fila es una reseña, así que una misma cerveza aparece varias veces: en total son 66.055 cervezas distintas, 5.743 marcas y 10 nacionalidades.

Dos formas de mirar los mismos datos

Marca, precio, alcohol, carbohidratos, calorías, nacionalidad, tipo y origen no cambian entre las reseñas de una misma cerveza. Solo cambian las cinco valoraciones. Esto tiene una implicación directa: si se calcula el precio promedio sobre las 1.586.614 filas tal cual vienen, las cervezas con más reseñas pesan más en el resultado, y el número deja de describir el catálogo para describir la popularidad.

La popularidad, además, está repartida de forma muy desigual: la mediana es de 2 reseñas por producto, 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 usa 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 un mínimo de reseñas, para que un producto con una sola opinión no compita con otro que tiene cientos.

Cómo se calculó cada cosa

En las variables cualitativas (marca, nacionalidad, tipo, origen) y en las ordinales (calificación, aroma, apariencia, paladar, sabor) se armaron tablas de frecuencia absoluta y relativa, y se identificó la moda. En las ordinales se añadieron frecuencias acumuladas, mediana y cuartiles, que son los indicadores que le corresponden a una escala de orden; la media y la desviación se muestran solo como referencia adicional.

En las cuantitativas se calcularon media, mediana, cuartiles, deciles, desviación estándar, rango intercuartílico y coeficiente de variación, además de la asimetría, que es la que indica si conviene mirar más la media o la mediana en cada caso.

2. Variables cualitativas

Tipo de cerveza

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 %. Entre las dos suman más de tres de cada cuatro cervezas. “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.

A nivel de reseña la concentración es todavía mayor: “cerveza normal y helada” pasa del 49,5 % de los productos al 52,9 % de las reseñas. Sus cervezas se comentan más que el resto.

Origen

La moda es “importada”: 59.113 productos, 89,5 % del total; las nacionales quedan en 10,5 %. A nivel de reseña la proporción es casi la misma (89,0 % importadas), así que ningún origen se reseña más que otro.

Un punto importante sobre cómo está construida esta variable: origen depende totalmente de nacionalidad. Las 6.942 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.

Hay otra dependencia que afecta las comparaciones más adelante: “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.

Nacionalidad y marca

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 confirma que esta variable fue construida para el ejercicio.

Las marcas, en cambio, 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.

3. Variables ordinales

Las cinco valoraciones se comportan igual

Calificación, aroma, apariencia, paladar y sabor comparten el mismo patrón: en las cinco, la moda y la mediana son 4,0, y la mitad central de las respuestas cabe entre 3,5 y 4,0 o 4,5. La distribución es asimétrica hacia la izquierda (entre -1,02 y -0,84): hay puntuaciones bajas, pero son poco frecuentes.

En la calificación general, el valor 4,0 por sí solo reúne el 36,7 % de las reseñas, y el 82,0 % está en 3,5 o más. Solo el 3,9 % puntúa 2,0 o menos.

Esto es común en reseñas voluntarias, y tiene una consecuencia directa: la escala distingue poco entre las cervezas mejor valoradas. Una diferencia de una o dos décimas puede mover un producto varios puestos en un ranking, porque casi todas las respuestas están apretadas en el mismo tramo.

No todos los atributos valen lo mismo

Apariencia es el más generoso y el más parejo (media 3,84, desviación 0,62): casi todo producto la aprueba, 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 de los cinco (3,74). La diferenciación real pasa por sabor y aroma, no por apariencia.

4. Variables cuantitativas

Precio

El precio de la caja de seis unidades tiene una media de 5,24 y una mediana de 5,31, con desviación estándar 0,76 y coeficiente de variación de 14,5 %. Es la variable más homogénea del estudio: el rango intercuartílico es de solo 0,98 unidades. La asimetría es casi nula (-0,49), la distribución es simétrica, y media y desviación estándar la resumen bien.

Alcohol, carbohidratos y calorías

Estas tres se comportan distinto. Alcohol (media 16,72 g, mediana 15,34 g) y calorías (media 176,14, mediana 166,00) tienen asimetría positiva marcada (1,20 y 1,08) por unas pocas cervezas muy fuertes: el máximo de alcohol, 59,45 g, 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 del catálogo es 5,48 % vol. — una cerveza estándar.

Las calorías no aportan nada nuevo

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.

La unidad de análisis cambia los números

El precio medio a nivel de reseña es 5,36, frente a 5,24 a nivel de producto — 0,12 unidades de diferencia. 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.

5. Comparaciones

Por tipo de cerveza

El tipo es el criterio que más separa el catálogo, en precio y en composición.

En precio, la clara artesanal es la más cara (mediana 5,66); la lager artesanal y la baja en calorías son las más baratas (medianas 4,49 y 4,43). La diferencia entre extremos ronda las 1,2 unidades sobre una mediana general de 5,31.

En composición, la baja en calorías / sin alcohol marca el contraste más fuerte: mediana de 105 calorías frente a 172 de la normal y helada, y la mitad de alcohol. Cumple exactamente lo que promete.

En valoración, en cambio, la baja en calorías queda última con diferencia: media 2,95, casi un punto por debajo del resto, con la mayor desviación (1,08) y una moda de 3 — la única categoría cuya moda no es 4,0. En aroma y sabor su media cae por debajo de 2,6. Las mejor valoradas son la clara artesanal (3,88) y la normal y helada (3,86).

El resto de categorías se mueve en una franja de dos décimas. La baja en calorías se desprende casi un punto entero: es el contraste más nítido de todo el estudio.

Por origen

En precio la diferencia es clara: las importadas tienen una mediana de 5,39 frente a 4,37 de las nacionales, un 23 % más caras. En composición nutricional casi no hay diferencia entre las dos.

En valoración tampoco: 3,85 las nacionales frente a 3,81 las importadas, con la misma moda y mediana (4,0). Una brecha de 0,040 puntos no significa nada en la práctica. El origen marca el precio, no la satisfacción.

Entre nacionalidades el rango va de 3,88 (Inglaterra) a 3,71 (Canadá): apenas 0,177 puntos de diferencia.

6. Relaciones entre variables

Qué mueve la calificación general

La calificación general se mueve sobre todo con el sabor (correlación 0,730) y el paladar (0,654); el aroma pesa menos (0,558) y la apariencia casi nada (0,454). Tiene sentido: la apariencia es el atributo más parejo entre cervezas, así que es el que menos explica por qué una queda mejor calificada que otra. Sabor y paladar son los que realmente definen la experiencia.

Precio y valoración

Hay una relación positiva entre precio y calificación, débil pero ordenada (correlación 0,242). Por quintiles de precio, la calificación sube sin excepción: de 3,48 en el quintil más barato a 4,04 en el más caro.

Esa brecha —0,55 puntos— es grande para una escala tan comprimida. Pero el precio es simulado, y lo más probable es que precio, tipo y perfil de producto se muevan juntos: el precio funciona como etiqueta de segmento, no como causa de la satisfacción.

Alcohol y valoración

Con el alcohol el patrón cambia de forma: la calificación sube de 3,58 en el quintil más bajo a 3,89 en el central, y ahí se aplana (3,93 y 3,94). La mejora se concentra en la parte baja y desaparece del tercer quintil en adelante — coherente con la fuerte penalización que ya vimos en la categoría sin alcohol. Lo que castiga al consumidor parece ser la ausencia de alcohol, más que lo que premia es tener más.

Productos extremos

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 y variantes saborizadas de precio bajo. Estos rankings reflejan la opinión de quien escribe una reseña, no necesariamente la del comprador promedio.

7. 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. Vale la pena revisar esta categoría con el equipo de producto.

  3. Sabor y paladar gobiernan la valoración general; la apariencia funciona como umbral, no como diferenciador.

  4. El origen separa precios, no satisfacción: las importadas cuestan un 23 % más con la misma composición nutricional y una valoración prácticamente igual.

  5. Precio y calificación se mueven juntos, débil pero de forma ordenada. Es una hipótesis para explorar, no una relación probada.

  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.

8. Limitaciones

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. Tampoco es la única señal de que la base fue construida para el ejercicio: las calorías se derivan exactamente de alcohol y carbohidratos, y las diez nacionalidades están repartidas casi en partes iguales, algo que no ocurre en datos reales.

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; que la categoría sin alcohol quede peor valorada no explica por qué. Son patrones para plantear preguntas, no respuestas cerradas.

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. Y la etiqueta “nacional” corresponde exactamente a Estados Unidos, no a Colombia.

Las reseñas también tienen sesgo: las escribe quien decide opinar, no el consumidor promedio, lo que sobrerrepresenta al aficionado y al producto artesanal. Además, el 35,9 % de las cervezas tiene una sola reseña, así que sus promedios son poco confiables.

Por último, “lager artesanal” y “baja en calorías / sin alcohol” reúnen menos del 3 % de las cervezas cada una, así que sus indicadores son más inestables que los del resto. Y las cinco valoraciones son ordinales: sus medias se reportan como referencia, pero las conclusiones de este informe se apoyan sobre todo en modas, medianas y cuartiles.

Datos

Column

Muestra de reseñas

Catálogo con respaldo

Column

Ficha técnica

Archivo beer2.csv
Reseñas (filas) 1.586.614
Variables 15
Cervezas únicas 66.055
Nombres comerciales 56.857
Marcas 5.743
Nacionalidades 10
Tipos 5
Datos faltantes Ninguno
Atributos constantes por id Verificado
Unidad de análisis — oferta Producto (n = 66.055)
Unidad de análisis — demanda Reseña (n = 1.586.614)
Reseñas por cerveza (mediana) 2
Cervezas con una sola reseña 35,9 %
Variable simulada precio

Diccionario de variables

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
---
title: "Caso Cervecería — Informe descriptivo"
author: "Mariana del Rio Ramirez — Teoría de Probabilidades"
date: "8/09/2026"
output: 
  flexdashboard::flex_dashboard:
    orientation: columns
    vertical_layout: fill
    source_code: embed
---


```{r setup, include=FALSE}
library(flexdashboard)
library(DT)
library(knitr)

knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)

# ---------------------------------------------------------------
# 1. LECTURA
# ---------------------------------------------------------------
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")
}

# ---------------------------------------------------------------
# 2. ESTILO (paleta y tamaños heredados del tablero base)
# ---------------------------------------------------------------
colores <- c(
  "#C68642", # 1-lager artesanal   - ámbar
  "#F1C40F", # 2-clara artesanal   - dorado
  "#7B3F00", # 3-lager importada   - marrón
  "#F39C12", # 4-normal y helada   - amarillo intenso
  "#BDC3C7"  # 5-baja en calorías  - gris claro
)
col_origen <- c("#F1C40F", "#F39C12")   # nacional / importada
col_base   <- "#F6A623"                 # relleno general
col_borde  <- "#7B3F00"                 # borde general

tam_titulo    <- 1.2
tam_ejes      <- 0.9
tam_etiquetas <- 0.8
tam_subtitulo <- 0.9

# Escala secuencial para las variables ordinales (ámbar -> marrón)
esc_ord <- colorRampPalette(c("#F7E3B8", "#7B3F00"))

# ---------------------------------------------------------------
# 3. RECODIFICACIÓN
# ---------------------------------------------------------------
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

# ---------------------------------------------------------------
# 4. UNIDADES DE ANÁLISIS
#    Los atributos del producto (precio, alcohol, carbohidratos, calorías,
#    marca, nacionalidad, tipo, origen) son CONSTANTES dentro de cada `id`:
#    solo varían las valoraciones. Por eso se trabaja en dos niveles.
# ---------------------------------------------------------------
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, ]              # productos con respaldo suficiente

# Verificación de la constancia por producto (se reporta en el informe)
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))

# ---------------------------------------------------------------
# 5. 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
}

# Resumen de una variable numérica por grupos
por_grupo <- function(datos, var, grupo) {
  s <- split(datos[[var]], datos[[grupo]])
  m <- t(sapply(s, desc_num))
  d <- data.frame(Grupo = 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
}

# Resumen ordinal por grupos (moda, mediana, cuartiles)
ord_grupo <- function(datos, var, grupo) {
  s <- split(datos[[var]], datos[[grupo]])
  d <- 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)
  d
}

dt <- function(d, pl = 10) {
  DT::datatable(d, rownames = FALSE, fillContainer = FALSE,
                options = list(pageLength = pl, dom = "tip",
                               scrollX = TRUE, language = list(
                                 search = "Buscar:",
                                 paginate = list(previous = "Ant.", `next` = "Sig."),
                                 info = "_START_ a _END_ de _TOTAL_")))
}

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

# ---------------------------------------------------------------
# 6. OBJETOS PRECALCULADOS PARA EL INFORME
# ---------------------------------------------------------------
n_res_tot <- nrow(beer2)
n_prod    <- nrow(prod)
n_marca   <- length(unique(beer2$marca))
n_nombre  <- length(unique(beer2$nombre_cerveza))
n_pais    <- length(unique(beer2$nacionalidad))

d_prod <- t(sapply(prod[cuanti], desc_num))     # matriz: filas = variables
d_res  <- t(sapply(beer2[cuanti], desc_num))

f_tipo_p   <- tabla_frec(prod$tipo,   "Tipo", ordenar = TRUE)
f_tipo_r   <- tabla_frec(beer2$tipo,  "Tipo", ordenar = TRUE)
f_origen_p <- tabla_frec(prod$origen, "Origen", ordenar = TRUE)
f_origen_r <- tabla_frec(beer2$origen,"Origen", ordenar = TRUE)
f_pais_p   <- tabla_frec(prod$nacionalidad, "Nacionalidad")

pct_import_p <- 100 * sum(prod$origen == "importada") / n_prod
pct_import_r <- 100 * sum(beer2$origen == "importada") / n_res_tot

# Concentración de las reseñas
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

# Resumen de las ordinales a nivel reseña
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))),
  RI       = sapply(ordinales, function(v) as.numeric(diff(quantile(beer2[[v]], c(.25,.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"

cal_tipo   <- ord_grupo(beer2, "calificacion", "tipo")
cal_origen <- ord_grupo(beer2, "calificacion", "origen")

# Calificación media por nacionalidad
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), ]

# Quintiles de precio y de alcohol frente a la calificación
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)

# Correlaciones
cor_prod <- cor(prod[cuanti])
set.seed(2024)
mrho <- beer2[sample(n_res_tot, 200000), c(ordinales, cuanti)]
cor_res  <- cor(mrho, method = "spearman")

# Identidad calorías = 7*alcohol + 4*carbohidratos + 6 (redondeado)
ident_cal <- mean(round(7 * prod$alcohol + 4 * prod$carbohidratos + 6) == prod$calorias)

# Rankings de producto (con respaldo mínimo de reseñas)
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), 15)
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), 15)
```

Resumen
=======================================================================

*Mariana del Río Ramírez · Teoría de Probabilidades · 8 de septiembre de 2026*

Column {.tabset}
-----------------------------------------------------------------------

### Tipo de cerveza

```{r resumen-tipo}
par(mai = c(1, 2, 1.2, .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",
             cex.main = tam_titulo, cex.lab = tam_ejes,
             cex.axis = tam_etiquetas, cex.sub = tam_subtitulo)
text(t1, b, labels = paste0(fmi(t1), "  (",
                            round(100 * t1 / sum(t1), 1), "%)"),
     pos = 4, cex = 0.75, col = "#5A3210", xpd = TRUE)
grid()
```

### Origen

```{r resumen-origen}
t2  <- table(prod$origen)
lab <- paste0(names(t2), "\n", fmi(t2), " (",
              round(100 * t2 / sum(t2), 1), "%)")
par(mai = c(.6, .6, 1.2, .6))
pie(t2, labels = lab, col = col_origen, border = "white",
    main = "Distribución de las cervezas por origen",
    cex = 0.9, cex.main = tam_titulo)
```

### Calificación

```{r resumen-cal}
par(mai = c(1.1, 1.1, 1.2, .3))
t3 <- table(beer2$calificacion)
b <- barplot(t3, col = esc_ord(length(t3)), border = NA, las = 1,
             main = "Calificación general (n = 1.586.614 reseñas)",
             xlab = "Puntuación", ylab = "Número de reseñas",
             cex.main = tam_titulo, cex.lab = tam_ejes,
             cex.axis = tam_etiquetas)
abline(h = 0)
grid(nx = NA, ny = NULL)
```

### Precio

```{r resumen-precio}
par(mai = c(1.1, 1.1, 1.2, .3))
hist(prod$precio, breaks = 40, col = col_base, border = "white", las = 1,
     main = "Precio de la caja de seis unidades (productos)",
     xlab = "Precio (unidades monetarias)", ylab = "Número de cervezas",
     cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
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()
```

Column {data-width=200}
-----------------------------------------------------------------------

### Reseñas

```{r vb-reseñas}
valueBox(fmi(n_res_tot), caption = "Reseñas", icon = "ion-chatbubbles",
         color = "#7B3F00")
```

### Cervezas únicas

```{r vb-cervezas}
valueBox(fmi(n_prod), caption = "Cervezas únicas", icon = "ion-beer",
         color = "#C68642")
```

### Marcas

```{r vb-marcas}
valueBox(fmi(n_marca), caption = "Marcas", icon = "ion-briefcase",
         color = "#F39C12")
```

### Precio promedio

```{r vb-precio}
valueBox(fmt(mean(prod$precio)), caption = "Precio promedio (producto)",
         icon = "ion-pricetag", color = "#F1C40F")
```

### Cerveza importada

```{r gauge-import}
gauge(round(pct_import_p, 1), min = 0, max = 100, symbol = "%",
      label = "importadas",
      sectors = gaugeSectors(success = c(80, 100), warning = c(40, 79),
                             danger = c(0, 39)))
```

Cualitativas
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Tipo × Origen

```{r cual-cruce}
par(mai = c(1.9, 1.2, 1.3, .4))
ct <- table(prod$tipo, prod$origen)
b <- barplot(t(prop.table(ct, 1)) * 100, beside = FALSE, horiz = FALSE,
             col = col_origen, border = "white", las = 2, ylim = c(0, 100),
             ylab = "Porcentaje dentro del tipo (%)",
             main = "Composición del origen dentro de cada tipo",
             cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas,
             names.arg = labs_tp, cex.names = 0.75)
legend(x = mean(range(b)), y = 135.5, xjust = 0.5, horiz = TRUE,
       legend = c("nacional", "importada"), fill = col_origen,
       border = NA, bty = "n", xpd = NA, cex = 0.9)
```

### Marcas

```{r cual-marca}
par(mai = c(1, 2.6, 1.2, .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.main = tam_titulo, cex.lab = tam_ejes, cex.axis = 0.7)
grid()
```

Column {.tabset}
-----------------------------------------------------------------------

### Tipo (productos)

```{r tab-tipo-p}
dt(f_tipo_p, 6)
```

### Tipo (reseñas)

```{r tab-tipo-r}
dt(f_tipo_r, 6)
```

### Origen

```{r tab-origen}
d <- merge(f_origen_p, f_origen_r, by = "Origen", suffixes = c(" — productos", " — reseñas"))
dt(d, 4)
```

### Nacionalidad

```{r tab-pais}
dt(f_pais_p, 11)
```

### Marcas

```{r tab-marca}
d <- tabla_frec(beer2$marca, "Marca")
dt(d, 12)
```

### Tipo × Origen

```{r tab-cruce}
ct <- table(prod$tipo, prod$origen)
d <- data.frame(Tipo = rownames(ct),
                Nacional = as.numeric(ct[, "nacional"]),
                Importada = as.numeric(ct[, "importada"]),
                Total = as.numeric(rowSums(ct)),
                `% nacional` = round(100 * ct[, "nacional"] / rowSums(ct), 2),
                `% importada` = round(100 * ct[, "importada"] / rowSums(ct), 2),
                check.names = FALSE, row.names = NULL)
dt(d, 6)
```

Ordinales
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Las cinco valoraciones

```{r ord-panel}
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.lab = 0.8, 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", cex = 0.95, text.col = "#5A3210")
par(mfrow = c(1, 1))
```

### Calificación por tipo

```{r ord-caltipo}
par(mai = c(1.1, 1.2, 1.2, .5))
m <- sapply(levels(beer2$tipo), function(g)
  100 * table(factor(beer2$calificacion[beer2$tipo == g], levels = seq(0, 5, .5))) /
    sum(beer2$tipo == g))
matplot(seq(0, 5, .5), m, type = "b", pch = 16, lty = 1, lwd = 2, col = colores,
        las = 1, xlab = "Calificación", ylab = "Porcentaje dentro del tipo (%)",
        main = "Calificación general según tipo de cerveza",
        cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
legend("topleft", legend = labs_tp, col = colores,
       lwd = 2, pch = 16, bty = "n", cex = 0.85)
grid()
```

Column {.tabset}
-----------------------------------------------------------------------

### Resumen ordinal

```{r tab-ordres}
dt(res_ord, 6)
```

### Calificación

```{r tab-ordcal}
dt(tabla_ord(beer2$calificacion, "Calificación"), 11)
```

### Aroma

```{r tab-ordaroma}
dt(tabla_ord(beer2$aroma, "Aroma"), 11)
```

### Apariencia

```{r tab-ordapa}
dt(tabla_ord(beer2$apariencia, "Apariencia"), 11)
```

### Paladar

```{r tab-ordpal}
dt(tabla_ord(beer2$paladar, "Paladar"), 11)
```

### Sabor

```{r tab-ordsab}
dt(tabla_ord(beer2$sabor, "Sabor"), 11)
```

Cuantitativas
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Precio

```{r cuan-precio}
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", cex.main = 1.1)
abline(v = mean(prod$precio), col = col_borde, lwd = 2)
abline(v = median(prod$precio), col = "#C0392B", lwd = 2, lty = 2)
grid()
boxplot(prod$precio, horizontal = TRUE, col = col_base, border = col_borde,
        las = 1, xlab = "Precio", cex.axis = tam_etiquetas)
grid()
par(mfrow = c(1, 1))
```

### Alcohol

```{r cuan-alcohol}
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", cex.main = 1.1)
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", cex.axis = tam_etiquetas)
grid()
par(mfrow = c(1, 1))
```

Column {.tabset}
-----------------------------------------------------------------------

### Indicadores (producto)

```{r tab-descprod}
dt(tabla_desc(prod, cuanti), 5)
```

### Indicadores (reseña)

```{r tab-descres}
dt(tabla_desc(beer2, cuanti), 5)
```

### Comparación de niveles

```{r tab-niveles}
d <- data.frame(
  Variable = cuanti,
  `Media producto`  = round(d_prod[, "Media"], 3),
  `Media reseña`    = round(d_res[, "Media"], 3),
  `Diferencia`      = round(d_res[, "Media"] - d_prod[, "Media"], 3),
  `Mediana producto`= round(d_prod[, "Mediana"], 3),
  `Mediana reseña`  = round(d_res[, "Mediana"], 3),
  check.names = FALSE, row.names = NULL)
dt(d, 5)
```

### Reseñas por cerveza

```{r tab-nres}
d <- tabla_desc(prod, "n_res")
d$Variable <- "Reseñas por cerveza"
dt(d, 3)
```

Por tipo
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Precio

```{r bt-precio}
par(mai = c(1, 2, 1.2, .3))
boxplot(prod$precio ~ prod$tipo, horizontal = TRUE, col = colores, las = 1,
        xlab = "Precio", ylab = " ", names = labs_tp,
        main = "Distribución del precio por tipo de cerveza",
        cex.main = tam_titulo, cex.lab = tam_ejes,
        cex.axis = tam_etiquetas, cex.sub = tam_subtitulo)
grid()
```

### Valoraciones

```{r bt-valor}
par(mai = c(1.9, 1.1, 1.3, .4))
m <- sapply(ordinales, function(v) tapply(beer2[[v]], beer2$tipo, mean))
b <- barplot(t(m), 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",
             cex.main = tam_titulo, cex.lab = tam_ejes,
             names.arg = labs_tp, cex.names = 0.75)
axis(2, at = 0:4, las = 1, cex.axis = tam_etiquetas)
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.8)
abline(h = 0)
```

Column {.tabset}
-----------------------------------------------------------------------

### Precio

```{r tt-precio}
dt(por_grupo(prod, "precio", "tipo"), 5)
```

### Calorías

```{r tt-calorias}
dt(por_grupo(prod, "calorias", "tipo"), 5)
```

### Alcohol

```{r tt-alcohol}
dt(por_grupo(prod, "alcohol", "tipo"), 5)
```

### Carbohidratos

```{r tt-carb}
dt(por_grupo(prod, "carbohidratos", "tipo"), 5)
```

### Calificación

```{r tt-cal}
dt(cal_tipo, 5)
```

### Todas las valoraciones

```{r tt-todas}
m <- sapply(ordinales, function(v) round(tapply(beer2[[v]], beer2$tipo, mean), 3))
d <- data.frame(Tipo = rownames(m), m, row.names = NULL, check.names = FALSE)
names(d)[-1] <- tit
dt(d, 5)
```

Por origen
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Precio

```{r bo-precio}
par(mai = c(1, 1.6, 1.2, .3))
boxplot(prod$precio ~ prod$origen, horizontal = TRUE, col = col_origen, las = 1,
        xlab = "Precio", ylab = " ",
        main = "Distribución del precio por origen (productos)",
        cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
grid()
```

### Nacionalidad

```{r bo-pais}
par(mai = c(1, 1.8, 1.2, .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",
             cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
text(d$Media, b, labels = fmt(d$Media), pos = 4, cex = 0.75,
     col = "#5A3210", xpd = TRUE)
grid()
```

Column {.tabset}
-----------------------------------------------------------------------

### Precio

```{r to-precio}
dt(por_grupo(prod, "precio", "origen"), 3)
```

### Alcohol

```{r to-alcohol}
dt(por_grupo(prod, "alcohol", "origen"), 3)
```

### Carbohidratos

```{r to-carb}
dt(por_grupo(prod, "carbohidratos", "origen"), 3)
```

### Calorías

```{r to-calorias}
dt(por_grupo(prod, "calorias", "origen"), 3)
```

### Valoraciones

```{r to-valor}
m <- sapply(ordinales, function(v) round(tapply(beer2[[v]], beer2$origen, mean), 3))
d <- data.frame(Origen = rownames(m), m, row.names = NULL, check.names = FALSE)
names(d)[-1] <- tit
dt(d, 3)
```

### Nacionalidad

```{r to-pais}
dt(cal_pais, 11)
```

Relaciones
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Calorías × Alcohol

```{r rel-calalc}
par(mai = c(1.1, 1.2, 1.2, .3))
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)",
     cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
legend("topleft", paste0("r de Pearson = ", fmt(cor_prod["alcohol", "calorias"], 3)),
       bty = "n", cex = 0.9)
grid()
```

### Calificación × Precio

```{r rel-calprecio}
par(mai = c(1.2, 1.2, 1.2, .3))
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.8,
             ylab = "Calificación media",
             main = "Calificación media según quintil de precio",
             cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
text(b, as.numeric(cal_qp), labels = fmt(as.numeric(cal_qp)), pos = 3,
     cex = 0.85, col = "#5A3210", xpd = TRUE)
abline(h = 0)
```

### Calificación × Alcohol

```{r rel-calalcohol}
par(mai = c(1.2, 1.2, 1.2, .3))
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.8,
             ylab = "Calificación media",
             main = "Calificación media según quintil de alcohol",
             cex.main = tam_titulo, cex.lab = tam_ejes, cex.axis = tam_etiquetas)
text(b, as.numeric(cal_qa), labels = fmt(as.numeric(cal_qa)), pos = 3,
     cex = 0.85, col = "#5A3210", xpd = TRUE)
abline(h = 0)
```

### Atributos sensoriales

```{r rel-sensorial}
par(mai = c(1.2, 1.2, 1.2, .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",
             cex.main = tam_titulo, cex.lab = 0.8, cex.axis = tam_etiquetas)
text(b, as.numeric(r), labels = fmt(as.numeric(r), 3), pos = 3,
     cex = 0.85, col = "#5A3210", xpd = TRUE)
abline(h = 0)
```

Column {.tabset}
-----------------------------------------------------------------------

### Correlaciones (producto)

```{r tab-corprod}
d <- data.frame(Variable = rownames(cor_prod), round(cor_prod, 3),
                row.names = NULL, check.names = FALSE)
dt(d, 5)
```

### Correlaciones (Spearman)

```{r tab-corres}
d <- data.frame(Variable = rownames(cor_res), round(cor_res, 3),
                row.names = NULL, check.names = FALSE)
dt(d, 10)
```

### Mejor valoradas

```{r tab-mejores}
dt(rank_alto, 8)
```

### Peor valoradas

```{r tab-peores}
dt(rank_bajo, 8)
```

Informe
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### 1. Objetivo y método

#### Propósito

La empresa quiere entender qué prefieren sus consumidores y qué características de una
cerveza se asocian con mejores valoraciones. Este es un diagnóstico descriptivo: sirve
para ver patrones y plantear hipótesis, no para probar qué causa qué.

#### La base de datos

`r fmi(n_res_tot)` reseñas, 15 variables, sin datos faltantes. Cada fila es una reseña,
así que una misma cerveza aparece varias veces: en total son `r fmi(n_prod)` cervezas
distintas, `r fmi(n_marca)` marcas y `r n_pais` nacionalidades.

#### Dos formas de mirar los mismos datos

Marca, precio, alcohol, carbohidratos, calorías, nacionalidad, tipo y origen no cambian
entre las reseñas de una misma cerveza. Solo cambian las cinco valoraciones. Esto tiene
una implicación directa: si se calcula el precio promedio sobre las
`r fmi(n_res_tot)` filas tal cual vienen, las cervezas con más reseñas pesan más en el
resultado, y el número deja de describir el catálogo para describir la popularidad.

La popularidad, además, está repartida de forma muy desigual: la mediana es de
`r median(prod$n_res)` reseñas por producto, el `r fmt(pct_1res, 1)` % de las cervezas
tiene una sola, y el 1 % más reseñado concentra el `r fmt(pct_top1, 1)` % de todos los
registros. Por eso este informe usa dos unidades de análisis:

| Unidad | n | Qué describe | Para qué sirve |
|---|---|---|---|
| **Producto** (una fila por cerveza) | `r fmi(n_prod)` | precio, alcohol, carbohidratos, calorías, tipo, origen, nacionalidad, marca | Muestra el catálogo, cada cerveza pesa igual |
| **Reseña** (una fila por opinión) | `r fmi(n_res_tot)` | 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 un mínimo de reseñas, para que un producto con una sola opinión no
compita con otro que tiene cientos.

#### Cómo se calculó cada cosa

En las variables cualitativas (marca, nacionalidad, tipo, origen) y en las ordinales
(calificación, aroma, apariencia, paladar, sabor) se armaron tablas de frecuencia
absoluta y relativa, y se identificó la moda. En las ordinales se añadieron frecuencias
acumuladas, mediana y cuartiles, que son los indicadores que le corresponden a una
escala de orden; la media y la desviación se muestran solo como referencia adicional.

En las cuantitativas se calcularon media, mediana, cuartiles, deciles, desviación
estándar, rango intercuartílico y coeficiente de variación, además de la asimetría, que
es la que indica si conviene mirar más la media o la mediana en cada caso.

### 2. Variables cualitativas

#### Tipo de cerveza

El portafolio está concentrado en dos tipos. La moda es "cerveza normal y helada", con
`r fmi(f_tipo_p[1, 2])` productos — el `r fmt(f_tipo_p[1, 3], 1)` % del catálogo — y
"clara artesanal" le sigue con el
`r fmt(f_tipo_p[f_tipo_p$Tipo == "clara artesanal", 3], 1)` %. Entre las dos suman más
de tres de cada cuatro cervezas. "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.

A nivel de reseña la concentración es todavía mayor: "cerveza normal y helada" pasa del
`r fmt(f_tipo_p[1, 3], 1)` % de los productos al `r fmt(f_tipo_r[1, 3], 1)` % de las
reseñas. Sus cervezas se comentan más que el resto.

#### Origen

La moda es "importada": `r fmi(f_origen_p[f_origen_p$Origen == "importada", 2])`
productos, `r fmt(pct_import_p, 1)` % del total; las nacionales quedan en
`r fmt(100 - pct_import_p, 1)` %. A nivel de reseña la proporción es casi la misma
(`r fmt(pct_import_r, 1)` % importadas), así que ningún origen se reseña más que otro.

Un punto importante sobre cómo está construida esta variable: `origen` depende
totalmente de `nacionalidad`. Las `r fmi(f_pais_p[f_pais_p$Nacionalidad == "Estados Unidos", 2])`
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.

Hay otra dependencia que afecta las comparaciones más adelante: "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.

#### Nacionalidad y marca

Las diez nacionalidades están repartidas de forma pareja, entre el
`r fmt(min(f_pais_p[, 3]), 1)` % y el `r fmt(max(f_pais_p[, 3]), 1)` % de los productos.
Un mercado real no suele verse así de uniforme, lo que confirma que esta variable fue
construida para el ejercicio.

Las marcas, en cambio, sí se comportan como un mercado real: `r fmi(n_marca)` marcas se
reparten `r fmi(n_prod)` productos, con muchísimas marcas de un solo producto y unas
pocas que concentran la mayoría de las reseñas.

### 3. Variables ordinales

#### Las cinco valoraciones se comportan igual

Calificación, aroma, apariencia, paladar y sabor comparten el mismo patrón: en las
cinco, la moda y la mediana son 4,0, y la mitad central de las respuestas cabe entre
3,5 y 4,0 o 4,5. La distribución es asimétrica hacia la izquierda (entre
`r fmt(min(res_ord[["Asimetría"]]), 2)` y `r fmt(max(res_ord[["Asimetría"]]), 2)`): hay
puntuaciones bajas, pero son poco frecuentes.

En la calificación general, el valor 4,0 por sí solo reúne el
`r fmt(100 * sum(beer2$calificacion == 4) / n_res_tot, 1)` % de las reseñas, y el
`r fmt(100 * mean(beer2$calificacion >= 3.5), 1)` % está en 3,5 o más. Solo el
`r fmt(100 * mean(beer2$calificacion <= 2), 1)` % puntúa 2,0 o menos.

Esto es común en reseñas voluntarias, y tiene una consecuencia directa: la escala
distingue poco entre las cervezas mejor valoradas. Una diferencia de una o dos décimas
puede mover un producto varios puestos en un ranking, porque casi todas las respuestas
están apretadas en el mismo tramo.

#### No todos los atributos valen lo mismo

Apariencia es el más generoso y el más parejo (media
`r fmt(res_ord$Media[res_ord$Variable == "apariencia"])`, desviación
`r fmt(res_ord$DE[res_ord$Variable == "apariencia"])`): casi todo producto la aprueba,
así que aporta poco para diferenciar una cerveza de otra. Sabor es el más exigente
(desviación `r fmt(res_ord$DE[res_ord$Variable == "sabor"])`) y aroma tiene la media
más baja de los cinco (`r fmt(res_ord$Media[res_ord$Variable == "aroma"])`). La
diferenciación real pasa por sabor y aroma, no por apariencia.

### 4. Variables cuantitativas

#### Precio

El precio de la caja de seis unidades tiene una media de
`r fmt(d_prod["precio", "Media"])` y una mediana de `r fmt(d_prod["precio", "Mediana"])`,
con desviación estándar `r fmt(d_prod["precio", "DE"])` y coeficiente de variación de
`r fmt(d_prod["precio", "CV"], 1)` %. Es la variable más homogénea del estudio: el rango
intercuartílico es de solo `r fmt(d_prod["precio", "RI"])` unidades. La asimetría es
casi nula (`r fmt(d_prod["precio", "Asimetria"], 2)`), la distribución es simétrica, y
media y desviación estándar la resumen bien.

#### Alcohol, carbohidratos y calorías

Estas tres se comportan distinto. Alcohol (media
`r fmt(d_prod["alcohol", "Media"])` g, mediana `r fmt(d_prod["alcohol", "Mediana"])` g)
y calorías (media `r fmt(d_prod["calorias", "Media"])`, mediana
`r fmt(d_prod["calorias", "Mediana"])`) tienen asimetría positiva marcada
(`r fmt(d_prod["alcohol", "Asimetria"], 2)` y `r fmt(d_prod["calorias", "Asimetria"], 2)`)
por unas pocas cervezas muy fuertes: el máximo de alcohol,
`r fmt(d_prod["alcohol", "Maximo"])` g, equivale a cerca de
`r fmt(max(prod$abv), 0)` % 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 del catálogo es `r fmt(median(prod$abv))` % vol. — una
cerveza estándar.

#### Las calorías no aportan nada nuevo

Las calorías se calculan directamente a partir del alcohol y los carbohidratos. En el
`r fmt(100 * ident_cal, 0)` % 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 `r fmt(cor_prod["alcohol", "calorias"], 3)`:
prácticamente perfecta. Para cualquier análisis posterior conviene usar alcohol y
carbohidratos, y dejar las calorías solo como dato de etiqueta.

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

El precio medio a nivel de reseña es `r fmt(d_res["precio", "Media"])`, frente a
`r fmt(d_prod["precio", "Media"])` a nivel de producto — `r fmt(d_res["precio", "Media"] - d_prod["precio", "Media"])`
unidades de diferencia. 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.

### 5. Comparaciones

#### Por tipo de cerveza

El tipo es el criterio que más separa el catálogo, en precio y en composición.

En precio, la clara artesanal es la más cara (mediana
`r fmt(median(prod$precio[prod$tipo == "clara artesanal"]))`); la lager artesanal y la
baja en calorías son las más baratas (medianas
`r fmt(median(prod$precio[prod$tipo == "lager artesanal"]))` y
`r fmt(median(prod$precio[prod$tipo == "baja en calorías"]))`). La diferencia entre
extremos ronda las 1,2 unidades sobre una mediana general de
`r fmt(d_prod["precio", "Mediana"])`.

En composición, la baja en calorías / sin alcohol marca el contraste más fuerte: mediana
de `r fmt(median(prod$calorias[prod$tipo == "baja en calorías"]), 0)` calorías frente a
`r fmt(median(prod$calorias[prod$tipo == "normal y helada"]), 0)` de la normal y helada,
y la mitad de alcohol. Cumple exactamente lo que promete.

En valoración, en cambio, la baja en calorías queda última con diferencia: media
`r fmt(cal_tipo$Media[cal_tipo$Grupo == "baja en calorías"])`, casi un punto por debajo
del resto, con la mayor desviación
(`r fmt(cal_tipo$DE[cal_tipo$Grupo == "baja en calorías"])`) y una moda de
`r cal_tipo$Moda[cal_tipo$Grupo == "baja en calorías"]` — la única categoría cuya moda
no es 4,0. En aroma y sabor su media cae por debajo de 2,6. Las mejor valoradas son la
clara artesanal (`r fmt(cal_tipo$Media[cal_tipo$Grupo == "clara artesanal"])`) y la
normal y helada (`r fmt(cal_tipo$Media[cal_tipo$Grupo == "normal y helada"])`).

El resto de categorías se mueve en una franja de dos décimas. La baja en calorías se
desprende casi un punto entero: es el contraste más nítido de todo el estudio.

#### Por origen

En precio la diferencia es clara: las importadas tienen una mediana de
`r fmt(median(prod$precio[prod$origen == "importada"]))` frente a
`r fmt(median(prod$precio[prod$origen == "nacional"]))` de las nacionales, un
`r fmt(100 * (median(prod$precio[prod$origen == "importada"]) / median(prod$precio[prod$origen == "nacional"]) - 1), 0)` %
más caras. En composición nutricional casi no hay diferencia entre las dos.

En valoración tampoco: `r fmt(cal_origen$Media[cal_origen$Grupo == "nacional"])` las
nacionales frente a `r fmt(cal_origen$Media[cal_origen$Grupo == "importada"])` las
importadas, con la misma moda y mediana (4,0). Una brecha de
`r fmt(abs(diff(cal_origen$Media)), 3)` puntos no significa nada en la práctica. El
origen marca el precio, no la satisfacción.

Entre nacionalidades el rango va de `r fmt(max(cal_pais$Media))`
(`r cal_pais$Nacionalidad[1]`) a `r fmt(min(cal_pais$Media))`
(`r cal_pais$Nacionalidad[nrow(cal_pais)]`): apenas
`r fmt(max(cal_pais$Media) - min(cal_pais$Media), 3)` puntos de diferencia.

### 6. Relaciones entre variables

#### Qué mueve la calificación general

La calificación general se mueve sobre todo con el sabor (correlación
`r fmt(cor_res["calificacion", "sabor"], 3)`) y el paladar
(`r fmt(cor_res["calificacion", "paladar"], 3)`); el aroma pesa menos
(`r fmt(cor_res["calificacion", "aroma"], 3)`) y la apariencia casi nada
(`r fmt(cor_res["calificacion", "apariencia"], 3)`). Tiene sentido: la apariencia es el
atributo más parejo entre cervezas, así que es el que menos explica por qué una queda
mejor calificada que otra. Sabor y paladar son los que realmente definen la experiencia.

#### Precio y valoración

Hay una relación positiva entre precio y calificación, débil pero ordenada
(correlación `r fmt(cor_res["calificacion", "precio"], 3)`). Por quintiles de precio, la
calificación sube sin excepción: de `r fmt(cal_qp[1])` en el quintil más barato a
`r fmt(cal_qp[5])` en el más caro.

Esa brecha —`r fmt(cal_qp[5] - cal_qp[1], 2)` puntos— es grande para una escala tan
comprimida. Pero el precio es simulado, y lo más probable es que precio, tipo y perfil
de producto se muevan juntos: el precio funciona como etiqueta de segmento, no como
causa de la satisfacción.

#### Alcohol y valoración

Con el alcohol el patrón cambia de forma: la calificación sube de
`r fmt(cal_qa[1])` en el quintil más bajo a `r fmt(cal_qa[3])` en el central, y ahí se
aplana (`r fmt(cal_qa[4])` y `r fmt(cal_qa[5])`). La mejora se concentra en la parte baja
y desaparece del tercer quintil en adelante — coherente con la fuerte penalización que
ya vimos en la categoría sin alcohol. Lo que castiga al consumidor parece ser la
ausencia de alcohol, más que lo que premia es tener más.

#### Productos extremos

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 y variantes
saborizadas de precio bajo. Estos rankings reflejan la opinión de quien escribe una
reseña, no necesariamente la del comprador promedio.

### 7. 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. Vale la pena revisar esta categoría con el
   equipo de producto.

3. Sabor y paladar gobiernan la valoración general; la apariencia funciona como umbral,
   no como diferenciador.

4. El origen separa precios, no satisfacción: las importadas cuestan un 23 % más con
   la misma composición nutricional y una valoración prácticamente igual.

5. Precio y calificación se mueven juntos, débil pero de forma ordenada. Es una
   hipótesis para explorar, no una relación probada.

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.

### 8. Limitaciones

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. Tampoco es la única
señal de que la base fue construida para el ejercicio: las calorías se derivan
exactamente de alcohol y carbohidratos, y las diez nacionalidades están repartidas casi
en partes iguales, algo que no ocurre en datos reales.

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;
que la categoría sin alcohol quede peor valorada no explica por qué. Son patrones para
plantear preguntas, no respuestas cerradas.

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. Y la etiqueta "nacional" corresponde
exactamente a Estados Unidos, no a Colombia.

Las reseñas también tienen sesgo: las escribe quien decide opinar, no el consumidor
promedio, lo que sobrerrepresenta al aficionado y al producto artesanal. Además, el
`r fmt(pct_1res, 1)` % de las cervezas tiene una sola reseña, así que sus promedios son
poco confiables.

Por último, "lager artesanal" y "baja en calorías / sin alcohol" reúnen menos del 3 %
de las cervezas cada una, así que sus indicadores son más inestables que los del resto.
Y las cinco valoraciones son ordinales: sus medias se reportan como referencia, pero las
conclusiones de este informe se apoyan sobre todo en modas, medianas y cuartiles.

Datos
=======================================================================

Column {.tabset}
-----------------------------------------------------------------------

### Muestra de reseñas

```{r datos-muestra}
set.seed(1)
m <- beer2[sample(n_res_tot, 1000), c("id", "marca", "nombre_cerveza", "tipo",
                                      "origen", "nacionalidad", "precio", "alcohol",
                                      "carbohidratos", "calorias", "calificacion",
                                      "aroma", "apariencia", "paladar", "sabor")]
DT::datatable(m, rownames = FALSE, fillContainer = FALSE,
              caption = "Muestra aleatoria de 1.000 reseñas (la base completa tiene 1.586.614)",
              options = list(pageLength = 10, scrollX = TRUE))
```

### Catálogo con respaldo

```{r datos-prod}
p <- prod_f[, c("id", "marca", "nombre_cerveza", "tipo", "origen", "nacionalidad",
                "precio", "alcohol", "carbohidratos", "calorias", "n_res",
                "m_calificacion")]
p$m_calificacion <- round(p$m_calificacion, 2)
names(p)[11:12] <- c("Reseñas", "Calificación media")
p <- p[order(-p$`Calificación media`), ]
DT::datatable(p, rownames = FALSE, fillContainer = FALSE,
              caption = paste0("Cervezas con 30 o más reseñas (", fmi(nrow(p)),
                               " de las ", fmi(n_prod), " del catálogo), ",
                               "ordenadas por calificación media"),
              options = list(pageLength = 10, scrollX = TRUE))
```

Column
-----------------------------------------------------------------------

### Ficha técnica

```{r ficha}
d <- data.frame(
  Elemento = c("Archivo", "Reseñas (filas)", "Variables", "Cervezas únicas",
               "Nombres comerciales", "Marcas", "Nacionalidades", "Tipos",
               "Datos faltantes", "Atributos constantes por id",
               "Unidad de análisis — oferta", "Unidad de análisis — demanda",
               "Reseñas por cerveza (mediana)", "Cervezas con una sola reseña",
               "Variable simulada"),
  Valor = c("beer2.csv",
            fmi(n_res_tot), "15", fmi(n_prod), fmi(n_nombre), fmi(n_marca),
            as.character(n_pais), "5", "Ninguno",
            ifelse(const_ok, "Verificado", "No se cumple"),
            paste0("Producto (n = ", fmi(n_prod), ")"),
            paste0("Reseña (n = ", fmi(n_res_tot), ")"),
            as.character(median(prod$n_res)),
            paste0(fmt(pct_1res, 1), " %"),
            "precio"))
knitr::kable(d, col.names = c("", ""))
```

### Diccionario de variables

```{r diccionario}
d <- 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"))
knitr::kable(d, col.names = c("Variable", "Clasificación", "Descripción"))
```