# Análisis conjoint basado en elecciones (CBC)
```{r}
#| echo: false
#| message: false
#| warning: false
source("script.R")
```
## Introducción: de valorar perfiles a elegir entre alternativas
El capítulo anterior desarrolló el conjoint tradicional, donde cada encuestado
valora perfiles de producto de forma aislada. Aunque sencillo de estimar, este
enfoque exige al encuestado una tarea cognitiva poco realista: en la práctica,
nadie compra un producto evaluándolo en solitario, sino comparándolo
directamente con las alternativas disponibles en el mercado.
El **análisis conjoint basado en elecciones** (*Choice-Based Conjoint*, CBC)
resuelve esta limitación: en cada tarea, se presentan al encuestado varios
perfiles de producto a la vez —normalmente entre 2 y 4— junto con la opción de
no elegir ninguno ("ninguna de las anteriores"), y se le pide que seleccione
cuál compraría. Cada encuestado responde a varias tareas de elección sucesivas,
cada una con una combinación distinta de perfiles. Esta dinámica simula de forma
mucho más realista el proceso real de decisión de compra, y es la razón por la
que el CBC ha desplazado al conjoint tradicional como estándar de la industria
desde la década de 1990.
::: callout-tip
## Una nota para quien viene de SPSS
El análisis CBC no tiene equivalente en SPSS Conjoint, que está orientado
exclusivamente al conjoint tradicional desarrollado en el capítulo anterior. El
CBC se aborda en R mediante modelos logit condicionales, siendo `mlogit` el
paquete de referencia académica más extendido y documentado para esta técnica.
:::
## El modelo estadístico: la teoría de la utilidad aleatoria
El fundamento estadístico del CBC es la **teoría de la utilidad aleatoria**
(*Random Utility Theory*), formalizada por Daniel McFadden, premio Nobel de
Economía por este trabajo [@McFadden1974]. Su idea central: cada individuo
asigna a cada alternativa una utilidad (un nivel de satisfacción o atractivo)
compuesta por una parte sistemática, explicable por los atributos observados, y
una parte aleatoria, que recoge factores no observados:
$$U_{ij} = V_{ij} + \varepsilon_{ij}$$
donde $U_{ij}$ es la utilidad total que el individuo $i$ asigna a la alternativa
$j$, $V_{ij}$ es la utilidad sistemática (función de los atributos del producto)
y $\varepsilon_{ij}$ es el componente aleatorio. El individuo elige la
alternativa que le proporciona la mayor utilidad total.
La utilidad sistemática se modela como una combinación lineal de los niveles de
los atributos:
$$V_{ij} = \beta_1 X_{1j} + \beta_2 X_{2j} + \dots + \beta_k X_{kj}$$
Cada coeficiente $\beta$ representa la **parte de utilidad** (*part-worth
utility*) que aporta cada nivel de cada atributo: cuánto contribuye, por
ejemplo, tener 16 GB en lugar de 8 GB a la utilidad total del producto.
Bajo el supuesto de que los errores $\varepsilon_{ij}$ siguen una distribución
de valores extremos (Gumbel), se obtiene el **modelo logit condicional**
(*conditional logit*), cuya probabilidad de elección sigue la forma:
$$P(j) = \frac{e^{V_j}}{\sum_{k=1}^{J} e^{V_k}}$$
Esta es la ecuación que estima el modelo: la probabilidad de elegir la
alternativa $j$ depende de su utilidad relativa frente a todas las demás
alternativas presentadas en esa tarea de elección.
## De los coeficientes a las decisiones de negocio
Una vez estimado el modelo, sus coeficientes ($\beta$) se traducen en tres tipos
de información directamente accionable para el negocio.
### Importancia relativa de los atributos
Para cada atributo, se calcula el rango de utilidad que abarca (la diferencia
entre su nivel más preferido y el menos preferido). Los atributos con mayor
rango son los que más influyen en la decisión de compra; normalizando estos
rangos a porcentajes se obtiene la **importancia relativa** de cada atributo.
### Disposición a pagar (Willingness to Pay)
Cuando el precio es uno de los atributos del modelo, su coeficiente puede usarse
como "tipo de cambio" entre utilidad y dinero. La disposición a pagar por
mejorar de un nivel a otro en cualquier atributo se calcula como el cociente
entre la diferencia de utilidad de esos niveles y el coeficiente del precio:
$$WTP = \frac{\beta_{atributo}}{-\beta_{precio}}$$
### Simulación de cuota de mercado
El resultado más potente del CBC: una vez estimado el modelo, se puede simular
cómo se repartiría la preferencia de los encuestados entre cualquier conjunto
hipotético de productos —incluyendo productos que aún no existen en el mercado—,
aplicando la ecuación de probabilidad de elección a un nuevo conjunto de
perfiles.
## Aplicación práctica: diseño de un lector de libros electrónicos
Se retoma el caso del capítulo anterior —el diseño de un nuevo lector de libros
electrónicos— pero ahora con un estudio CBC: en lugar de valorar perfiles
aislados, los encuestados eligen entre varios perfiles presentados
conjuntamente.
### Objetivo de la investigación
> **Objetivo**: estimar un modelo de elección discreta (CBC) a partir de los
> datos de una encuesta, para determinar la importancia relativa de cada
> atributo del producto, calcular la disposición a pagar por las mejoras de cada
> atributo, y simular la cuota de preferencia de distintas configuraciones de
> producto.
### Preparación de los datos
Se ha encuestado a 200 consumidores, cada uno respondiendo a 8 tareas de
elección entre 3 perfiles de lectores electrónicos. Los atributos testados son
los mismos del capítulo anterior: capacidad de almacenamiento (8 GB / 16 GB),
tamaño de pantalla (5" / 6" / 7"), color (negro / blanco / plateado) y precio
(99€ / 129€ / 159€).
Los datos de un estudio CBC se estructuran en **formato largo**: una fila por
cada alternativa presentada en cada tarea de elección, con una variable binaria
que indica si esa alternativa fue la elegida.
```{r}
#| echo: true
#| message: false
set.seed(7)
n_respondents <- 200
n_tasks <- 8
n_alternatives <- 3
storage_levels <- c("8GB", "16GB")
screen_levels <- c("5in", "6in", "7in")
color_levels <- c("Black", "White", "Silver")
price_levels <- c(99, 129, 159)
# True (simulated) part-worth utilities, used to generate realistic choices
true_utilities <- c(
storage16GB = 0.9,
screen6in = 0.5, screen7in = 0.3,
colorWhite = 0.2, colorSilver = 0.4,
price = -0.025 # negative: higher price reduces utility
)
# Build the full design: one row per alternative per task per respondent
ebook_design <- expand.grid(
resp_id = 1:n_respondents,
task_id = 1:n_tasks,
alt_id = 1:n_alternatives
) |>
dplyr::arrange(resp_id, task_id, alt_id) |>
dplyr::mutate(
storage = sample(storage_levels, dplyr::n(), replace = TRUE),
screen = sample(screen_levels, dplyr::n(), replace = TRUE),
color = sample(color_levels, dplyr::n(), replace = TRUE),
price = sample(price_levels, dplyr::n(), replace = TRUE)
)
# Compute systematic utility for each alternative using the true part-worths
ebook_design <- ebook_design |>
dplyr::mutate(
utility = dplyr::if_else(storage == "16GB", true_utilities["storage16GB"], 0) +
dplyr::case_when(
screen == "6in" ~ true_utilities["screen6in"],
screen == "7in" ~ true_utilities["screen7in"],
TRUE ~ 0
) +
dplyr::case_when(
color == "White" ~ true_utilities["colorWhite"],
color == "Silver" ~ true_utilities["colorSilver"],
TRUE ~ 0
) +
price * true_utilities["price"] +
rnorm(dplyr::n(), 0, 1) # random utility component
)
# Within each task, the respondent picks the alternative with the highest utility
ebook_data <- ebook_design |>
dplyr::group_by(resp_id, task_id) |>
dplyr::mutate(choice = as.integer(utility == max(utility))) |>
dplyr::ungroup() |>
dplyr::select(resp_id, task_id, alt_id, storage, screen, color, price, choice)
head(ebook_data, 9)
```
Cada bloque de 3 filas corresponde a una tarea de elección: las tres
alternativas presentadas a un encuestado en un momento dado, con un `1` en
`choice` para la alternativa seleccionada.
### Preparación de los datos para el modelo
Antes de construir el modelo, es necesario convertir explícitamente a `factor`
las variables categóricas del diseño (`storage`, `screen`, `color`), fijando
además el orden de sus niveles. Si estas variables permanecen como texto
(`character`), `mlogit()` puede nombrar los coeficientes estimados de forma
genérica y posicional (`storage1`, `screen1`, `screen2`...) en lugar de usar las
etiquetas reales de cada nivel (`storage16GB`, `screen6in`...), lo que rompe
cualquier referencia posterior a esos coeficientes por nombre:
```{r}
#| echo: true
# Explicitly convert categorical attributes to factors with a fixed level
# order. Without this step, mlogit() may name the estimated coefficients
# generically (storage1, screen1...) instead of using the actual level
# labels (storage16GB, screen6in...), breaking every downstream reference
# to those coefficients by name.
ebook_data <- ebook_data |>
dplyr::mutate(
storage = factor(storage, levels = c("8GB", "16GB")),
screen = factor(screen, levels = c("5in", "6in", "7in")),
color = factor(color, levels = c("Black", "White", "Silver"))
)
```
El paquete `mlogit` requiere transformar los datos a una estructura específica
con `mlogit.data()`, indicando qué variable contiene la elección y cómo se
agrupan las alternativas dentro de cada tarea:
```{r}
#| echo: true
ebook_mlogit <- mlogit::mlogit.data(
ebook_data,
choice = "choice",
shape = "long",
alt.var = "alt_id",
id.var = "resp_id"
)
```
::: callout-important
## Sobre el sistema de contrastes
R puede codificar las variables categóricas de un modelo con distintos sistemas
de **contrastes**. El más habitual, y el que asume el resto de este capítulo, es
`contr.treatment`: cada nivel se compara contra un nivel de referencia, y los
coeficientes del modelo se nombran con la etiqueta real de cada nivel (por
ejemplo, `storage8GB`). Si la sesión de R tiene activado otro sistema —como
`contr.sum`, los contrastes de suma—, `mlogit()` nombra los coeficientes de
forma genérica y posicional (`storage1`, `screen1`...), lo que rompe cualquier
referencia posterior a esos coeficientes por nombre. Para que el código de este
capítulo sea robusto frente a cualquier configuración previa de la sesión, se
fija explícitamente `contr.treatment` justo antes de estimar el modelo, y se
restaura la configuración original justo después.
:::
### Ejecución del modelo logit condicional
Se especifica el modelo con la sintaxis de fórmula de `mlogit()`. El sufijo
`| 0` indica que no se incluye una constante de alternativa (intercepto),
apropiado cuando las alternativas no tienen una identidad propia más allá de sus
atributos:
```{r}
#| echo: true
# Force standard treatment contrasts (dummy coding against a reference
# level), regardless of any global contrasts option set elsewhere in the
# session (e.g. contr.sum). This guarantees mlogit() names coefficients
# using actual level labels (storage8GB, screen6in...) instead of generic
# positional names (storage1, screen1...).
old_contrasts <- options("contrasts")
options(contrasts = c("contr.treatment", "contr.poly"))
cbc_model <- mlogit::mlogit(
choice ~ storage + screen + color + price | 0,
data = ebook_mlogit
)
options(old_contrasts) # restore the session's original setting
summary(cbc_model)
```
### Interpretación de los coeficientes
```{r}
#| echo: true
cbc_coefs <- coef(cbc_model)
cbc_coefs
```
- **Signo**: todos los coeficientes de atributos "deseables" (16 GB, pantallas
mayores, colores distintos del negro) son positivos, indicando que aumentan la
utilidad y, por tanto, la probabilidad de elección, frente a sus respectivas
categorías de referencia (8 GB, pantalla de 5", color negro). El coeficiente
de `price` es negativo, como cabe esperar: a mayor precio, menor utilidad.
- **Significación**: los p-valores (`Pr(>|z|)`) indican qué atributos influyen
de forma estadísticamente significativa en la elección. En este caso, todos
los coeficientes son significativos.
- **Magnitud**: el coeficiente de `storage16GB` es el más alto de los atributos
no relacionados con el precio, sugiriendo que la capacidad de almacenamiento
es el atributo con mayor impacto en la utilidad, después del propio precio.
### Importancia relativa de los atributos
Para cada atributo, se calcula el rango de utilidad que cubre (diferencia entre
el nivel de mayor y menor utilidad), y se normaliza a porcentaje sobre la suma
de todos los rangos:
```{r}
#| echo: true
attribute_ranges <- c(
storage = abs(unname(cbc_coefs["storage16GB"])),
screen = max(0, cbc_coefs["screen6in"], cbc_coefs["screen7in"]) -
min(0, cbc_coefs["screen6in"], cbc_coefs["screen7in"]),
color = max(0, cbc_coefs["colorWhite"], cbc_coefs["colorSilver"]) -
min(0, cbc_coefs["colorWhite"], cbc_coefs["colorSilver"]),
price = abs(cbc_coefs["price"] * (max(price_levels) - min(price_levels)))
)
importance_data <- data.frame(
attribute = names(attribute_ranges),
importance = round(100 * attribute_ranges / sum(attribute_ranges), 1)
)
importance_data
```
```{r}
#| label: fig-attribute-importance
#| fig-cap: Importancia relativa de los atributos del producto
#| echo: true
ggplot2::ggplot(importance_data, ggplot2::aes(x = reorder(attribute, importance), y = importance)) +
ggplot2::geom_col(fill = "#076fa2") +
ggplot2::geom_text(ggplot2::aes(label = paste0(importance, "%")), hjust = -0.1) +
ggplot2::coord_flip() +
ggplot2::scale_y_continuous(limits = c(0, max(importance_data$importance) * 1.2)) +
ggplot2::labs(x = NULL, y = "Importancia relativa (%)") +
ggplot2::theme_minimal()
```
El precio resulta ser el atributo más determinante en la decisión, seguido de la
capacidad de almacenamiento; el color es el atributo de menor peso relativo en
la elección.
### Disposición a pagar (Willingness to Pay)
Se calcula cuánto estaría dispuesto a pagar un cliente por mejorar de un nivel a
otro en cada atributo, usando el coeficiente del precio como referencia de
conversión entre utilidad y euros:
```{r}
#| echo: true
wtp_storage <- unname(cbc_coefs["storage16GB"] / -cbc_coefs["price"])
wtp_screen_6in <- unname(cbc_coefs["screen6in"] / -cbc_coefs["price"])
wtp_screen_7in <- unname(cbc_coefs["screen7in"] / -cbc_coefs["price"])
wtp_color_white <- unname(cbc_coefs["colorWhite"] / -cbc_coefs["price"])
wtp_color_silver <- unname(cbc_coefs["colorSilver"] / -cbc_coefs["price"])
wtp_table <- data.frame(
upgrade = c("8GB -> 16GB", "5in -> 6in", "5in -> 7in",
"Black -> White", "Black -> Silver"),
wtp_eur = round(c(wtp_storage, wtp_screen_6in, wtp_screen_7in,
wtp_color_white, wtp_color_silver), 1)
)
wtp_table
```
Los clientes estarían dispuestos a pagar un sobreprecio notable por pasar de 8 a
16 GB de almacenamiento, mientras que la disposición a pagar por el color es
mucho más modesta.
### Simulación de cuota de preferencia
```{r}
#| echo: true
# Define three hypothetical products competing in the market
market_scenario <- data.frame(
product = c("Economy", "Mainstream", "Premium"),
storage = c("8GB", "16GB", "16GB"),
screen = c("5in", "6in", "7in"),
color = c("Black", "White", "Silver"),
price = c(99, 129, 159)
)
# Compute the systematic utility of each product using the estimated coefficients
market_scenario <- market_scenario |>
dplyr::mutate(
utility = dplyr::if_else(storage == "16GB", unname(cbc_coefs["storage16GB"]), 0) +
dplyr::case_when(
screen == "6in" ~ cbc_coefs["screen6in"],
screen == "7in" ~ cbc_coefs["screen7in"],
TRUE ~ 0
) +
dplyr::case_when(
color == "White" ~ cbc_coefs["colorWhite"],
color == "Silver" ~ cbc_coefs["colorSilver"],
TRUE ~ 0
) +
price * cbc_coefs["price"]
)
# Apply the conditional logit choice probability formula
market_scenario <- market_scenario |>
dplyr::mutate(
market_share = round(100 * exp(utility) / sum(exp(utility)), 1)
)
market_scenario[, c("product", "storage", "screen", "color", "price", "market_share")]
```
```{r}
#| label: fig-market-share
#| fig-cap: Cuota de preferencia simulada entre tres configuraciones de producto
#| echo: true
ggplot2::ggplot(market_scenario, ggplot2::aes(x = product, y = market_share)) +
ggplot2::geom_col(fill = "#076fa2") +
ggplot2::geom_text(ggplot2::aes(label = paste0(market_share, "%")), vjust = -0.5) +
ggplot2::labs(x = NULL, y = "Cuota de preferencia simulada (%)") +
ggplot2::theme_minimal()
```
La simulación permite comparar distintas estrategias de producto antes de
fabricar nada: por ejemplo, evaluar si merece la pena el coste adicional de una
pantalla mayor frente a una reducción de precio, observando directamente el
impacto en la cuota de preferencia simulada.
### Conclusiones
El análisis conjoint basado en elecciones ha permitido transformar las
respuestas de una encuesta en información directamente accionable para el diseño
de producto:
1. **Importancia de los atributos**: el precio y la capacidad de almacenamiento
son los factores más determinantes en la decisión de compra; el color tiene
un peso relativo menor.
2. **Disposición a pagar**: se ha cuantificado en euros el valor que los
consumidores otorgan a cada mejora de atributo, información directamente
útil para decisiones de *pricing*.
3. **Simulación de mercado**: el modelo permite evaluar cualquier configuración
de producto antes de su lanzamiento, comparando estrategias de diseño y
precio de forma cuantitativa.
El capítulo siguiente retoma este mismo caso práctico para explorar dos paquetes
complementarios a `mlogit`: `cbcTools`, para el diseño y la evaluación
estadística de experimentos CBC antes de la recogida de datos, y `logitr`, para
la estimación de modelos logit con parametrización directa en espacio de
disposición a pagar.