How to use MDSConjoint

Jordi L. Sintas

2016-11-08

Introducción

MDSConjoint (Marketing Data Science Conjoint) tiene como objetivo mostrar a los estudiantes de grado y posgrado, de marketing avanzado o modelos de comercialización, cómo analizar los datos procedentes de un análisis del conjunto de los atributos de un producto (Conjoint Analysis, CA). Esto es, pone énfasis en la estimación de las utilidades parciales, en medir la importancia de los atributos (en qué medida aportan utilidad al individuo) y en la toma de decisiones basadas en datos, a la simulación de las cuotas de mercado que cualquier definición del nuevo producto puede obtener en el mercado dados los perfiles de producto existentes, a la búsqueda del perfil de producto que maximiza la cuota de mercado o el perfil de producto que maximiza el margen de contribución dada la estructura del mercado (Green, Krieger & Wind 2001).

Todo análisis conjunto se inicia con la identificación de los atributos y niveles relevantes (mediante reuniones con un grupo de consumidores, por ejemplo), sigue el diseño de un experimento que nos permita estimar las utilidades parciales de los niveles (efectos principales del modelo) y tal vez algunas interacciones entre atributos relevantes, como la marca y el nivel de precio, aunque todo ello con un número de perfiles de producto reducido. Después se realiza el trabajo de campo (evaluación de los perfiles de producto por parte de los individuos); finalmente se realiza la estimación de las utilidades parciales y la simulación de resultados probables con el objeto de facilitar la toma de decisiones. (ver la documentación del programa SAS sobre marketing)

El objetivo de MDSConjoint es facilitar las tareas estimación de las utilidades parciales de los niveles de los atributos, la importancia de los atributos, y la simulación de los resultados orientados a la toma de decisiones. Existen otros paquetes en R, como conjoint, faisalconjoint o support.CEs. Los dos primeros no abordan la toma de decisiones -la optimización de cuota de mercado o del margen de contribución- y no es fácil la preparación de los datos para la estimación de las utilidades parciales cuando no han sido generados por el propio programa. El último, support.CEs, está orientado a una clase de modelos de análisis conjunto denominados modelos de elección discreta o choice base conjoint models, CBC (Aizaki 2012).

Diseño de un nuevo producto basado en el análisis conjunto

El modelo métrico del análisis de un conjunto de atributos de un producto (metric conjoint analysis, MCA) se propone medir la utilidad que los diferentes niveles de los atributos de un producto aportan a un individuo o consumidor (Green & Srinivassan 1978). Durante el trabajo de campo los investigadores presentan a los individuos un conjunto de productos definidos por los niveles de los atributos que lo componen, que llamaremos perfiles de producto, y se les pide que valoren la utilidad (o probabilidad de compra) que cada perfil de producto les reportaría (en el modelo métrico) o bien que ordene de mayor a menor su preferencia por los productos que le han presentado (modelo no métrico, NMCA), dando lugar a un ranking de perfiles de producto.

Para medir la aportación de cada nivel de atributo a la utilidad de cada individuo, estimamos un modelo lineal donde regresamos la utilidad o probabilidad de compra sobre los niveles de los atributos. De hecho cuando todos los atributos son nominales, el modelo lineal es equivalente a un modelo anova de efectos principales (Venables & RIPLEY 2002, chapter 6) con los resultados presentados de cierta manera.

En el modelo MCA los niveles de los atributos toman el papel de variables independientes y las valoraciones que realiza cada uno de los individuos tienen el papel de la variable independiente, la utilidad. Los coeficientes estimados nos proporcionan la utilidad parcial con la que cada nivel contribuye a la utilidad (o probabilidad de compra) de cada perfil de producto:

\(U_i = X\beta + \epsilon_i\).

Para facilitar la interpretación de los parámetros del modelo, en la estimación de los coeficientes del modelo, para cada atributo, la suma de los parámetros \(\beta\) se restringe a cero:

\(∑\beta_1j =∑\beta_2j=...=∑\beta_al=0\)

De esa manera, \(\beta_0\) se interpreta como la utilidad media general que aportan los perfiles de producto al individuo (o la probabilidad media de compra), y los parámetros de cada nivel de atributo, que llamaremos utilidades parciales, indican las variaciones en la utilidad media por el hecho de que el producto incorpore ese nivel en su perfil. La variación máxima en el nivel de utilidad dentro de cada atributo es la aportación del atributo a la utilidad del individuo y se calcula como la diferencia entre el nivel que más utilidad aporta y el nivel que menos utilidad aporta al individuo. Finalmente, \(\epsilon_i\) es el error del modelo en la predicción de las utilidades del individuo i, esto es, la diferencia entre las utilidades reportadas por el individuo y las que predice el modelo.

Cuando los individuos, en lugar de reportar la utilidad que creen obtener de cada perfil de producto únicamente ordenan el listado de perfiles de producto, de mayor a menor preferencia, decimos que estamos ante el caso de un modelo no métrico de análisis conjunto, NMCA. En este caso, en lugar de estimar un modelo de regresión ordinario, realizamos una transformación monótona de la ordenación de los perfiles en niveles de utilidades.

\(\gamma(y_i) = X\beta + \epsilon_i\).

Concretamente realizamos la transformación que proponen en el informe técnico de SAS (SAS, 1993). Debido a la transformación realizada, el ajuste del modelo no métrico siempre será tan bueno o mejor que el ajuste de un modelo métrico con los mismos datos (Young, 1981; Gifi, 1990).

## Loading required package: support.CEs
## Loading required package: DoE.base
## Loading required package: grid
## Loading required package: conf.design
## 
## Attaching package: 'DoE.base'
## The following objects are masked from 'package:stats':
## 
##     aov, lm
## The following object is masked from 'package:graphics':
## 
##     plot.design
## The following object is masked from 'package:base':
## 
##     lengths
## Loading required package: MASS
## Loading required package: simex
## Loading required package: RCurl
## Loading required package: bitops
## Loading required package: XML

El paquete MDSConjoint se encuentra en el respositorio github.com. Para instalarlo necesitamos el paquete devtools que nos proporciona la función install_github(). Esta función necesita como argumento el usuario, jlopezsi, del repositorio github.com y el nombre del repositorio, MDSConjoint, todo ello entre comillas, así: install_github("jlopezsi/MDSConjoint"). Una vez instalado podemos cargarlo así:

library(MDSConjoint)

El paquete también nos proporciona un conjunto de datos con los que podemos probar y comprobar el funcionamiento de las funciones que nos proorciona. Concretamente nos proporciona una base de datos para el diseño de un neumático y otra base de datos para el diseño de un tienda de suministros para las oficinas.

#install_github("jlopezsi/MDSConjoint")
data("MDSConjointData")
names(MDSConjointData)
## [1] "osc"  "tire"
tire<-MDSConjointData$tire
tire$design
## $Brand
## [1] "Goodstone" "Pirogi"    "Machismo" 
## 
## $Price
## [1] "$69,99" "S79,99" "$89,99"
## 
## $Life
## [1] "50000km" "60000km" "70000km"
## 
## $Hazard
## [1] "Yes" "No"

Veamos un ejemplo. En este paquete hemos incorporado los datos que proporciona el programa SAS sobre el diseño de un neumático. Se consideran cuatro atributos: Brand, Price, Life, Hazard. (SAS tire data exemple)

Para el atributo Brand tendremos encuenta tres marcas, (Goodstone, Pirogi, Machismo), tres niveles para el atributo Price, ($69,99, S79,99, $89,99), tres para la vida media del neumático Life, (50000km, 60000km, 70000km), y dos niveles para la garantía Hazard, (Yes, No)

Para medir los efectos principales de los niveles de los atributos y los efectos de todas las interacciones entre los niveles es necesario que el consumidor valore todas las combinaciones posibles, en este caso: 3x3x3x2=54. Pero es un número muy elevado de perfiles de producto. La función expand.grid() nos proporciona todas las combinaciones posibles entre los niveles de producto.

experiment = expand.grid(
  tire$design
)
class(experiment)
## [1] "data.frame"
head(experiment)
##       Brand  Price    Life Hazard
## 1 Goodstone $69,99 50000km    Yes
## 2    Pirogi $69,99 50000km    Yes
## 3  Machismo $69,99 50000km    Yes
## 4 Goodstone S79,99 50000km    Yes
## 5    Pirogi S79,99 50000km    Yes
## 6  Machismo S79,99 50000km    Yes
tail(experiment)
##        Brand  Price    Life Hazard
## 49 Goodstone S79,99 70000km     No
## 50    Pirogi S79,99 70000km     No
## 51  Machismo S79,99 70000km     No
## 52 Goodstone $89,99 70000km     No
## 53    Pirogi $89,99 70000km     No
## 54  Machismo $89,99 70000km     No
length(experiment)
## [1] 4

Esto es lo que llamamos el experimento factorial completo, full design. Para reducirlo buscamos una fracción del experimento completo que tenga la propiedad de que las variables sean independientes, es decir, que su correlación sea cero.

Por ello normalmente se busca el número de perfiles necesario para poder estimar los efectos principales de los niveles de los atributos y en algunos casos, también, las interacciones de primer nivel entre los niveles de algunos atributos, como puede ser la marca y el precio.

Para estimar los efectos principales de los niveles de los atributos en la utilidad del consumidor escogemos los perfiles de producto necesarios para estimarlos de manera que el error sea mínimo una vez eliminadas las interacciones que no queremos estimar. Este experimento reducido nos lo proporciona una fracción de perfiles de producto que tienen la propiedad de que las variables son independientes.

Utilizamos la función Lma.design() del paquete support.CEs. La función Lma.desgin() nos proporciona un conjunto ortogonal de perfiles de producto que nos permite estimar los efectos principales de los niveles de los atributos. El resultado lo guardamos en el objeto tire.survey, en este caso.

# generate a balanced set of product profiles for survey
tire.survey <- Lma.design(attribute.names = 
                                tire$design, 
nalternatives = 1, nblocks=1, seed=9999)
## The columns of the array have been used in order of appearance. 
## For designs with relatively few columns, 
## the properties can sometimes be substantially improved 
## using option columns with min3 or even min34.
names(tire.survey)
## [1] "alternatives"       "candidate"          "design.information"
tire.survey
## 
## Choice sets:
## alternative 1 in each choice set
##    BLOCK QES ALT     Brand  Price    Life Hazard
## 1      1   1   1 Goodstone $69,99 50000km    Yes
## 2      1   2   1  Machismo $89,99 50000km    Yes
## 3      1   3   1 Goodstone $69,99 60000km    Yes
## 4      1   4   1    Pirogi S79,99 60000km    Yes
## 5      1   5   1 Goodstone S79,99 50000km    Yes
## 6      1   6   1    Pirogi $89,99 60000km    Yes
## 7      1   7   1 Goodstone $89,99 70000km     No
## 8      1   8   1 Goodstone S79,99 70000km     No
## 9      1   9   1  Machismo $69,99 70000km    Yes
## 10     1  10   1    Pirogi $89,99 50000km     No
## 11     1  11   1    Pirogi $69,99 50000km     No
## 12     1  12   1    Pirogi $69,99 70000km     No
## 13     1  13   1  Machismo $89,99 70000km    Yes
## 14     1  14   1 Goodstone $89,99 60000km     No
## 15     1  15   1  Machismo $69,99 60000km     No
## 16     1  16   1    Pirogi S79,99 70000km    Yes
## 17     1  17   1  Machismo S79,99 50000km     No
## 18     1  18   1  Machismo S79,99 60000km     No
## 
## Candidate design:
##    A B C D
## 1  3 1 2 2
## 2  1 3 3 2
## 3  2 1 1 2
## 4  2 2 2 1
## 5  1 2 3 2
## 6  1 3 2 2
## 7  1 2 1 1
## 8  2 1 3 2
## 9  3 2 2 2
## 10 3 1 3 1
## 11 3 3 3 1
## 12 1 1 1 1
## 13 1 1 2 1
## 14 2 3 1 2
## 15 2 3 2 1
## 16 3 2 1 2
## 17 2 2 3 1
## 18 3 3 1 1
## class=design, type= oa 
## 
## Design information:
## number of blocks = 1 
## number of questions per block = 18 
## number of alternatives per choice set = 1 
## number of attributes per alternative = 4

El paquete support.CEs, además, nos proporciona una función para preparar el cuestionario que utilizaremos para que los consumidores valoren el conjunto de perfiles.

print(questionnaire(tire.survey))  # print survey design for review
## 
## Block 1 
##  
## Question 1 
##        alt.1      
## Brand  "Goodstone"
## Price  "$69,99"   
## Life   "50000km"  
## Hazard "Yes"      
## 
## Question 2 
##        alt.1     
## Brand  "Machismo"
## Price  "$89,99"  
## Life   "50000km" 
## Hazard "Yes"     
## 
## Question 3 
##        alt.1      
## Brand  "Goodstone"
## Price  "$69,99"   
## Life   "60000km"  
## Hazard "Yes"      
## 
## Question 4 
##        alt.1    
## Brand  "Pirogi" 
## Price  "S79,99" 
## Life   "60000km"
## Hazard "Yes"    
## 
## Question 5 
##        alt.1      
## Brand  "Goodstone"
## Price  "S79,99"   
## Life   "50000km"  
## Hazard "Yes"      
## 
## Question 6 
##        alt.1    
## Brand  "Pirogi" 
## Price  "$89,99" 
## Life   "60000km"
## Hazard "Yes"    
## 
## Question 7 
##        alt.1      
## Brand  "Goodstone"
## Price  "$89,99"   
## Life   "70000km"  
## Hazard "No"       
## 
## Question 8 
##        alt.1      
## Brand  "Goodstone"
## Price  "S79,99"   
## Life   "70000km"  
## Hazard "No"       
## 
## Question 9 
##        alt.1     
## Brand  "Machismo"
## Price  "$69,99"  
## Life   "70000km" 
## Hazard "Yes"     
## 
## Question 10 
##        alt.1    
## Brand  "Pirogi" 
## Price  "$89,99" 
## Life   "50000km"
## Hazard "No"     
## 
## Question 11 
##        alt.1    
## Brand  "Pirogi" 
## Price  "$69,99" 
## Life   "50000km"
## Hazard "No"     
## 
## Question 12 
##        alt.1    
## Brand  "Pirogi" 
## Price  "$69,99" 
## Life   "70000km"
## Hazard "No"     
## 
## Question 13 
##        alt.1     
## Brand  "Machismo"
## Price  "$89,99"  
## Life   "70000km" 
## Hazard "Yes"     
## 
## Question 14 
##        alt.1      
## Brand  "Goodstone"
## Price  "$89,99"   
## Life   "60000km"  
## Hazard "No"       
## 
## Question 15 
##        alt.1     
## Brand  "Machismo"
## Price  "$69,99"  
## Life   "60000km" 
## Hazard "No"      
## 
## Question 16 
##        alt.1    
## Brand  "Pirogi" 
## Price  "S79,99" 
## Life   "70000km"
## Hazard "Yes"    
## 
## Question 17 
##        alt.1     
## Brand  "Machismo"
## Price  "S79,99"  
## Life   "50000km" 
## Hazard "No"      
## 
## Question 18 
##        alt.1     
## Brand  "Machismo"
## Price  "S79,99"  
## Life   "60000km" 
## Hazard "No"      
## 
## NULL
#sink("questions_for_survey.txt")  # send survey to external text file
#questionnaire(tire.survey)
#sink() # send output back to the screen

La evaluación de los perfiles valorados por los consumidores

Esos nrows(tire.survey$alternatives) perfiles de producto pueden ser evaluados de diferentes maneras. Los individuos pueden simplemente ordenar la lista de mayor a menor preferencia, de manera que el perfil que está en la primera posición del ranking es el perfil de producto con mayor preferencia. Esta forma de evaluar los perfiles de producto daría lugar a un análisis conjunto no métrico.

Por otro lado, los individuos también pueden evaluar los perfiles en función de la utilidad que creen que les reportaran, repartiendo, por ejemplo 100 puntos entre los perfiles de producto evaluados.

El programa TRANSREG del paquete de análisis de datos SAS, en cambio, propone evaluar los perfiles en función de la probabilidad de compra, que es equivalente a distribuir 100 puntos entre los perfiles de producto evaluados. (ver)

  1. For your next tire purchase, how likely are you to buy this product?

Pirogi brand tires at $79.99,
with a 50,000 tread life guarantee,
and without road hazard insurance.

Definitely Would Definitely Would
Not Purchase Purchase

1 2 3 4 5 6 7 8 9

La estimación de las utilidades parciales de los niveles de los atributos

El modelo de análisis conjunto se estima para cada individuo de la muestra. La función conjoint.estimation() estima un modelo lineal para cada uno de los individuos y reporta el resultado detallado del modelo lineal únicamente si se lo pedimos expresamente con el argumento rs=1. Después muestra una tabla de datos resumen de los parámetros estimados para toda la muestra y finalmente una tabla resumen de las utilidades parciales de cada uno de los niveles de los atributos incorporados en el modelo así como una tabla con las utilidades (o probabilidades) teóricas que predice el modelo. La correlación entre esta última tabla y la tabla con las valoraciones empíricas proporcionadas por los individuos nos da una medida del ajuste medio de las estimaciones.

Estimación del modelo para toda la muestra individuos

La función conjoint.estimation() toma como argumentos:

  1. Una base de datos rectangular con las valoraciones de los individuos de los perfiles de producto, que llamamos ratings, con tantas filas como individuos han valorado el conjunto de perfiles de producto, n, y tantas columnas como perfiles de producto hayamos presentado a los individuos, p. Esta tabla de datos tendrá un formato de data frame y una dimensión de n,p.

  2. Una base de datos con los perfiles de productos que han valorado los individuos, que llamamos bundles, con tantas filas como perfiles de producto hayan valorado los individuos, p, y tantas columnas como atributos tengan los perfiles de producto, a. Esta matriz que tendrá un formato de data frame y una dimensión de p,a.

  3. Una lista con los atributos y niveles de los atributos que forman los perfiles valorados. Esta lista que tendrá un formato de list.

  4. Si las valoraciones de los individuos toman la forma de una ordenación de preferencias, entonces rank=1 indica al programa que estamos ante un modelo no métrico y es necesario realizar una transformación de las preferencias en utilidades. Si el modelo es métrico, entonces no es necesario indicar nada.

  5. Finalmente si queremos que la función nos facilite los resultados detallados de la estimación para algún individuo, podemos utilizar el argumento rs=1. Cuídado con este argumento pues al retener todos los resultados consume mucha memoria. En general con más de 20 individuos no es aconsejable. Se incluye en el función con el objeto de que se puedan obtener resultados detallados para casos concretos en los que queremos hacer un diagnóstico de la estimación del modelo.

tires.partWorthsAll<-conjoint.estimation(tire$ratings, tire$bundles, tire$design, rs=1) 
names(tires.partWorthsAll)
## [1] "summary"     "fit"         "part.worths" "prediction"

La función conjoint.estimation() nos devuelve un objeto con formato list y cuatro objetos si hemos usados el argumento rs=1: summary, fit, part.worths, prediction.

Si hemos utilizado el argumento `rs=1, el objeto summary, al que se puede acceder añadiendo summary al nombre del objeto que hemos creado con la función conjoint.estimation(). Como el objeto creado con el resultado se llama tires.partWorthsAll la orden quedaría así: tires.partWorthsAll$Summary. La consola nos proporciona los resultados detallados del modelo para cada uno de los individuos de la muestra. Para acceder a ellos sólo es necesario añadir el nombre del individuo a la orden anterior: tires.partWorthsAll$Summary$Subj1 en este ejemplo. Si únicamente queremos acceder a los coeficientes estimados, tenemos que añadir $coefficients a la orden anterior: tires.partWorthsAll$Summary$Subj1$coefficients. En el trozo de código siguiente podemos ver los nombres de los individuos, names(tires.partWorthsAll$summary) y podemos observar los resultados para el individuo Subj2:

names(tires.partWorthsAll$summary)
## [1] "Subj1" "Subj2" "Subj3" "Subj4" "Subj5"
tires.partWorthsAll$summary$Subj2
## $Subj1
## $Subj1$call
## lm(formula = y ~ ., data = df)
## 
## $Subj1$terms
## y ~ Brand + Price + Life + Hazard
## attr(,"variables")
## list(y, Brand, Price, Life, Hazard)
## attr(,"factors")
##        Brand Price Life Hazard
## y          0     0    0      0
## Brand      1     0    0      0
## Price      0     1    0      0
## Life       0     0    1      0
## Hazard     0     0    0      1
## attr(,"term.labels")
## [1] "Brand"  "Price"  "Life"   "Hazard"
## attr(,"order")
## [1] 1 1 1 1
## attr(,"intercept")
## [1] 1
## attr(,"response")
## [1] 1
## attr(,".Environment")
## <environment: 0x7fafc12d0038>
## attr(,"predvars")
## list(y, Brand, Price, Life, Hazard)
## attr(,"dataClasses")
##           y       Brand       Price        Life      Hazard 
##   "numeric" "character" "character" "character" "character" 
## 
## $Subj1$residuals
##     bundle1     bundle2     bundle3     bundle4     bundle5     bundle6 
##  0.22222222  0.05555556 -0.44444444  0.05555556  0.55555556  0.55555556 
##     bundle7     bundle8     bundle9    bundle10    bundle11    bundle12 
##  0.55555556 -0.44444444 -0.27777778 -0.44444444 -0.27777778  0.05555556 
##    bundle13    bundle14    bundle15    bundle16    bundle17    bundle18 
##  0.55555556  0.05555556 -0.44444444  0.05555556  0.05555556 -0.44444444 
## 
## $Subj1$coefficients
##               Estimate Std. Error    t value     Pr(>|t|)
## (Intercept)  5.1111111  0.1165343 43.8592791 9.123564e-13
## Brand1      -0.1111111  0.1648044 -0.6741999 5.154662e-01
## Brand2      -0.6111111  0.1648044 -3.7080992 4.053994e-03
## Price1       2.5555556  0.1648044 15.5065968 2.539858e-08
## Price2      -0.1111111  0.1648044 -0.6741999 5.154662e-01
## Life1       -1.2777778  0.1648044 -7.7532984 1.547559e-05
## Life2        0.2222222  0.1648044  1.3483997 2.072755e-01
## Hazard1     -0.3333333  0.1165343 -2.8603878 1.694563e-02
## 
## $Subj1$aliased
## (Intercept)      Brand1      Brand2      Price1      Price2       Life1 
##       FALSE       FALSE       FALSE       FALSE       FALSE       FALSE 
##       Life2     Hazard1 
##       FALSE       FALSE 
## 
## $Subj1$sigma
## [1] 0.4944132
## 
## $Subj1$df
## [1]  8 10  8
## 
## $Subj1$r.squared
## [1] 0.9759825
## 
## $Subj1$adj.r.squared
## [1] 0.9591703
## 
## $Subj1$fstatistic
##    value    numdf    dendf 
## 58.05195  7.00000 10.00000 
## 
## $Subj1$cov.unscaled
##               (Intercept)        Brand1        Brand2        Price1
## (Intercept)  5.555556e-02 -1.453789e-18  7.268947e-19 -3.583931e-34
## Brand1      -1.453789e-18  1.111111e-01 -5.555556e-02 -2.124830e-18
## Brand2       7.268947e-19 -5.555556e-02  1.111111e-01  5.139921e-18
## Price1      -3.583931e-34 -2.124830e-18  5.139921e-18  1.111111e-01
## Price2       1.560571e-36  1.936696e-18 -5.653914e-18 -5.555556e-02
## Life1        7.268947e-19 -2.889356e-18 -8.223874e-18 -2.698326e-17
## Life2       -1.453789e-18  1.263491e-18  4.111937e-18  2.951632e-17
## Hazard1     -1.210523e-34  1.027984e-18 -2.055969e-18 -1.273909e-17
##                    Price2         Life1         Life2       Hazard1
## (Intercept)  1.560571e-36  7.268947e-19 -1.453789e-18 -1.210523e-34
## Brand1       1.936696e-18 -2.889356e-18  1.263491e-18  1.027984e-18
## Brand2      -5.653914e-18 -8.223874e-18  4.111937e-18 -2.055969e-18
## Price1      -5.555556e-02 -2.698326e-17  2.951632e-17 -1.273909e-17
## Price2       1.111111e-01  4.111937e-18 -2.055969e-18  4.111937e-18
## Life1        4.111937e-18  1.111111e-01 -5.555556e-02 -1.123402e-17
## Life2       -2.055969e-18 -5.555556e-02  1.111111e-01  8.223874e-18
## Hazard1      4.111937e-18 -1.123402e-17  8.223874e-18  5.555556e-02
## 
## 
## $call
## lm(formula = y ~ ., data = df)
## 
## $terms
## y ~ Brand + Price + Life + Hazard
## attr(,"variables")
## list(y, Brand, Price, Life, Hazard)
## attr(,"factors")
##        Brand Price Life Hazard
## y          0     0    0      0
## Brand      1     0    0      0
## Price      0     1    0      0
## Life       0     0    1      0
## Hazard     0     0    0      1
## attr(,"term.labels")
## [1] "Brand"  "Price"  "Life"   "Hazard"
## attr(,"order")
## [1] 1 1 1 1
## attr(,"intercept")
## [1] 1
## attr(,"response")
## [1] 1
## attr(,".Environment")
## <environment: 0x7fafc12d0038>
## attr(,"predvars")
## list(y, Brand, Price, Life, Hazard)
## attr(,"dataClasses")
##           y       Brand       Price        Life      Hazard 
##   "numeric" "character" "character" "character" "character" 
## 
## $residuals
##       bundle1       bundle2       bundle3       bundle4       bundle5 
##  2.777778e-01 -7.222222e-01 -7.222222e-01  1.666667e-01  2.777778e-01 
##       bundle6       bundle7       bundle8       bundle9      bundle10 
##  1.040834e-16  2.151057e-16  1.666667e-01  7.777778e-01  7.632783e-17 
##      bundle11      bundle12      bundle13      bundle14      bundle15 
##  2.777778e-01  2.777778e-01 -6.938894e-18 -3.333333e-01 -6.938894e-18 
##      bundle16      bundle17      bundle18 
## -2.222222e-01 -2.222222e-01 -6.245005e-17 
## 
## $coefficients
##                Estimate Std. Error    t value     Pr(>|t|)
## (Intercept)  4.94444444  0.1111111 44.5000000 7.897209e-13
## Brand1       1.22222222  0.1571348  7.7781746 1.505056e-05
## Brand2       0.05555556  0.1571348  0.3535534 7.310138e-01
## Price1       2.72222222  0.1571348 17.3241161 8.696310e-09
## Price2      -0.11111111  0.1571348 -0.7071068 4.956475e-01
## Life1       -0.11111111  0.1571348 -0.7071068 4.956475e-01
## Life2       -0.11111111  0.1571348 -0.7071068 4.956475e-01
## Hazard1     -0.05555556  0.1111111 -0.5000000 6.278936e-01
## 
## $aliased
## (Intercept)      Brand1      Brand2      Price1      Price2       Life1 
##       FALSE       FALSE       FALSE       FALSE       FALSE       FALSE 
##       Life2     Hazard1 
##       FALSE       FALSE 
## 
## $sigma
## [1] 0.4714045
## 
## $df
## [1]  8 10  8
## 
## $r.squared
## [1] 0.9792208
## 
## $adj.r.squared
## [1] 0.9646753
## 
## $fstatistic
##    value    numdf    dendf 
## 67.32143  7.00000 10.00000 
## 
## $cov.unscaled
##               (Intercept)        Brand1        Brand2        Price1
## (Intercept)  5.555556e-02 -1.453789e-18  7.268947e-19 -3.583931e-34
## Brand1      -1.453789e-18  1.111111e-01 -5.555556e-02 -2.124830e-18
## Brand2       7.268947e-19 -5.555556e-02  1.111111e-01  5.139921e-18
## Price1      -3.583931e-34 -2.124830e-18  5.139921e-18  1.111111e-01
## Price2       1.560571e-36  1.936696e-18 -5.653914e-18 -5.555556e-02
## Life1        7.268947e-19 -2.889356e-18 -8.223874e-18 -2.698326e-17
## Life2       -1.453789e-18  1.263491e-18  4.111937e-18  2.951632e-17
## Hazard1     -1.210523e-34  1.027984e-18 -2.055969e-18 -1.273909e-17
##                    Price2         Life1         Life2       Hazard1
## (Intercept)  1.560571e-36  7.268947e-19 -1.453789e-18 -1.210523e-34
## Brand1       1.936696e-18 -2.889356e-18  1.263491e-18  1.027984e-18
## Brand2      -5.653914e-18 -8.223874e-18  4.111937e-18 -2.055969e-18
## Price1      -5.555556e-02 -2.698326e-17  2.951632e-17 -1.273909e-17
## Price2       1.111111e-01  4.111937e-18 -2.055969e-18  4.111937e-18
## Life1        4.111937e-18  1.111111e-01 -5.555556e-02 -1.123402e-17
## Life2       -2.055969e-18 -5.555556e-02  1.111111e-01  8.223874e-18
## Hazard1      4.111937e-18 -1.123402e-17  8.223874e-18  5.555556e-02

Si queremos publicar el resultado de manera más elegante podemos utilizar la función kable del paquete knitr para publicar los coeficientes o cualquier dato en formato tabla. Veamos un ejemplo:

knitr::kable(tires.partWorthsAll$summary$Subj2$coefficients, digits=2, caption = 'Coeficientes del modelo estimado para el individuos Subj2' )
Coeficientes del modelo estimado para el individuos Subj2
Estimate Std. Error t value Pr(>|t|)
(Intercept) 4.94 0.11 44.50 0.00
Brand1 1.22 0.16 7.78 0.00
Brand2 0.06 0.16 0.35 0.73
Price1 2.72 0.16 17.32 0.00
Price2 -0.11 0.16 -0.71 0.50
Life1 -0.11 0.16 -0.71 0.50
Life2 -0.11 0.16 -0.71 0.50
Hazard1 -0.06 0.11 -0.50 0.63

Si queremos acceder a todos los demás objetos que forman los resultados del modelo, sólo tenemos que sustituir coefficients por el nombre del resultado que queremos inspeccionar (ver el resultado de Subj1, call, terms, residuals, coefficients, aliased, sigma, df, r.squared, adj.r.squared, fstatistic, cov.unscaled).

Resumen de las estimaciones

La tabla de datos con el resumen de los coeficientes estimados para toda la muestra está disponible en el objeto fit. Para acceder sólo tenemos que utilizar la orden siguiente: tires.partWorthsAll$fit. Nos proporciona una base de datos con tantas filas como individuos han valorado el conjunto de perfiles de producto y tantas columnas como niveles de los atributos menos uno. En este ejemplo, tendríamos (3-1)+(3-1)+(3-1)+(2-1)+1=8. Si no queremos ver toda la base de datos podemos inspeccionar su inicio y final con las funciones head() y tail(); entre los paréntesis sólo tenemos que poner el nombre del objeto, tires.partWorthsAll$fit en este caso. Utilizamos la función kable() para dar formato a la tabla de datos como vemos en el siguiente trozo de código.

#, echo=FALSE, results='asis'
knitr::kable(head(tires.partWorthsAll$fit), digits=2, caption = 'Estimaciones: Resultados de los 6 primeros individuos' )
Estimaciones: Resultados de los 6 primeros individuos
(Intercept) Brand1 Brand2 Price1 Price2 Life1 Life2 Hazard1
Subj1 5.11 -0.11 -0.61 2.56 -0.11 -1.28 0.22 -0.33
Subj2 4.94 1.22 0.06 2.72 -0.11 -0.11 -0.11 -0.06
Subj3 5.00 -0.17 -0.50 1.00 -0.33 -2.83 0.17 -0.22
Subj4 5.11 0.72 0.39 0.72 -0.11 -2.61 0.06 -0.44
Subj5 5.06 -0.06 -0.39 2.61 -0.06 -1.39 0.11 -0.17

Estimación de las utilidades parciales

Para completar los resultados de las estimaciones, la función conjoint.estimation() nos proporciona una base de datos con las utilidades parciales de cada uno de los niveles, incluidos los que se han eliminado para poder estimar el modelo (si no los hubiéramos eliminado, el modelo no se podría haber estimado). La tabla de datos tiene tantas filas como perfiles de producto, p, y tantas columnas como niveles de atributos hemos considerado en el análisis: 3+3+3+2=11 en este caso de los neumáticos. Esta forma de mostrar las utilidades parciales de cada nivel nos facilita el cálculo de la importancia de cada atributo como veremos después. De nuevo utilizamos la función kable() para dar formato a la tabla de datos.

knitr::kable(head(tires.partWorthsAll$part.worths), digits=2, caption = 'Utilidades parciales: Resultados de los 5 primeros individuos')
Utilidades parciales: Resultados de los 5 primeros individuos
Goodstone Pirogi Machismo $69,99 S79,99 $89,99 50000km 60000km 70000km Yes No
Subj1 -0.11 -0.61 0.72 2.56 -0.11 -2.44 -1.28 0.22 1.06 -0.33 0.33
Subj2 1.22 0.06 -1.28 2.72 -0.11 -2.61 -0.11 -0.11 0.22 -0.06 0.06
Subj3 -0.17 -0.50 0.67 1.00 -0.33 -0.67 -2.83 0.17 2.67 -0.22 0.22
Subj4 0.72 0.39 -1.11 0.72 -0.11 -0.61 -2.61 0.06 2.56 -0.44 0.44
Subj5 -0.06 -0.39 0.44 2.61 -0.06 -2.56 -1.39 0.11 1.28 -0.17 0.17

Finalmente la tabla de datos con las predicciones teóricas de la utilidad que reportan los perfiles de producto para toda la muestra se pueden obtener con el objeto prediction. Para acceder sólo tenemos que utilizar la orden siguiente: tires.partWorthsAll$prediction. Nos proporciona una tabla de datos con tantas filas como individuos han valorado el conjunto de perfiles de producto, n, y tantas columnas como perfiles de producto han valorado los individiuos, p. Si no queremos ver toda la base de datos, podemos inspeccionar su inicio y final con las funciones head() y tail(); entre paréntesis sólo tenemos que poner el nombre del objeto, tires.partWorthsAll$prediction en este caso. Utilizamos la función kable() para dar formato a la tabla de datos.

knitr::kable(head(t(tires.partWorthsAll$prediction)), digits=2, caption = 'Estimaciones: Resultados de los 6 primeros individuos' )
Estimaciones: Resultados de los 6 primeros individuos
Subj1 Subj2 Subj3 Subj4 Subj5
bundle1 2.78 4.72 1.11 2.33 3.06
bundle2 5.94 8.72 2.78 3.50 6.06
bundle3 6.44 3.72 7.78 6.00 6.56
bundle4 3.94 3.83 7.06 8.22 3.89
bundle5 6.44 3.72 7.78 6.00 6.56
bundle6 8.44 8.00 8.39 9.22 8.72
## Estimac ión del modelo ú nicament e para u n individuo

La función conjoint.estimation() también se puede utilizar para estimar el modelo sólo para un individuo o para un grupo de invididuos. Esta posibilidad es interesante para valorar las utilidades parciales de un determinado segmento. En este caso sólo tenemos que indicar que únicamente queremos que estime el modelo para un individuo en concreto o para un grupo de individuos, por ejemplo, para el primer individuo de la muestra. Para ello utilizamos los corchetes y dentro de ellos indicamos en número de la fila en la que está el individuo y una coma que indicada que utilizaremos todas las columnas de la base de datos: [1,]. En el caso de estimar el modelo para un subconjunto de la muestra lo indicaríamos asi: [1:20,] en el caso de seleccionar a los primeros 20 individuos.

La estimación del modelo nos devuelve los mismos objetos: una lista con los resultados detallados, una tabla de datos con los coeficientes de la estimación, una tabla de datos con las utilidades parciales de cada nivel de atributo para el individuo o el subconjunto de individuos, y una tabla de datos con las predicciones del modelo, o utilidades teóricas.

Aquí vemos primero la estimación del modelo para el primer individuo y los datos de la estimación que tenemos a nuestra disposición.

tires.partWorths1<-conjoint.estimation(tire$ratings[1,], tire$bundles, tire$design)
names(tires.partWorths1)
## [1] "fit"         "part.worths" "prediction"
names(tires.partWorths1$summary$Subj1)
## NULL

Seguidamente mostramos la tabla con los coeficientes del modelo para el primer individuo de la muestra.

knitr::kable(tires.partWorths1$summary$Subj1$coefficients, digits=2, caption = 'Coeficientes del modelo estimado para el individuos Subj1' )

Table: Coeficientes del modelo estimado para el individuos Subj1

Ahora mostramos el resumen de la estimación del modelo para toda la muestra:

knitr::kable(head(tires.partWorths1$fit), digits=2, caption = 'Estimaciones: Resultados de *j* individuos' )
Estimaciones: Resultados de j individuos
(Intercept) Brand1 Brand2 Price1 Price2 Life1 Life2 Hazard1
Subj1 5.11 -0.11 -0.61 2.56 -0.11 -1.28 0.22 -0.33

Y finalmente presentamos el inicio de la tabla de datos con las utilidades teóricas que predice el modelo para cada individuo.

knitr::kable(head(tires.partWorths1$part.worths), digits=2, caption = 'Utilidades parciales: Resultados de *j* individuos' )
Utilidades parciales: Resultados de j individuos
Goodstone Pirogi Machismo $69,99 S79,99 $89,99 50000km 60000km 70000km Yes No
Subj1 -0.11 -0.61 0.72 2.56 -0.11 -2.44 -1.28 0.22 1.06 -0.33 0.33

Cálculo de la importancia de los atributos

La función importance.of.attributes() nos proporciona tres tablas de datos. La primera es el resumen de los coeficientes estimados, fit, la segunda es la tabla de las utilidades parciales, part.worths, y la tercera la tabla resumen de la importancia de los atributos para los individuos de la muestra, imp.

tires.imp <- importance.of.attributes(tire$ratings, tire$bundles, tire$design)
names(tires.imp)
## [1] "fit"         "part.worths" "imp"

Para valorar la importancia de los atributos en la utilidad que reportan los perfiles de producto nos interesan las dos últimas tablas, la tabla part-worths y la tabla imp.

knitr::kable(head(tires.imp$part.worths),digits=2, caption = 'Utilidades parciales: Resultados de los 5 primeros individuos' )
Utilidades parciales: Resultados de los 5 primeros individuos
Goodstone Pirogi Machismo $69,99 S79,99 $89,99 50000km 60000km 70000km Yes No
Subj1 -0.11 -0.61 0.72 2.56 -0.11 -2.44 -1.28 0.22 1.06 -0.33 0.33
Subj2 1.22 0.06 -1.28 2.72 -0.11 -2.61 -0.11 -0.11 0.22 -0.06 0.06
Subj3 -0.17 -0.50 0.67 1.00 -0.33 -0.67 -2.83 0.17 2.67 -0.22 0.22
Subj4 0.72 0.39 -1.11 0.72 -0.11 -0.61 -2.61 0.06 2.56 -0.44 0.44
Subj5 -0.06 -0.39 0.44 2.61 -0.06 -2.56 -1.39 0.11 1.28 -0.17 0.17

Importancia de los atributos

La primera tabla nos permite calcular la importancia de los atributos como la diferencia entre el valor mínimo y el máximo de las utilidades parciales reportadas por los niveles de cada atributo. La tabla imp nos proporciona el resumen para todos los individuos, en porcentaje.

knitr::kable(head(tires.imp$imp),digits=2, caption = 'Importancia de los atributos: Resultados de los 5 primeros individuos (en %)' )
Importancia de los atributos: Resultados de los 5 primeros individuos (en %)
Brand Price Life Hazard
Subj1 14.29 53.57 25.00 7.14
Subj2 30.20 64.43 4.03 1.34
Subj3 13.29 18.99 62.66 5.06
Subj4 19.88 14.46 56.02 9.64
Subj5 9.26 57.41 29.63 3.70

Finalmente si queremos conocer la importancia media para toda la muestra, sólo tenemos que calcular la media de las columnas de la tabla imp. Para ello utilizamos la función apply() y como argumentos la tabla de datos tires.imp$imp, el número 2 que indica que queremos trabajar con las columnas de la tabla, y la función mean para indicar que queremos calcular la media de las columnas, apply(tires.imp$imp, 2, mean). Después utilizamos la función kable() para dar formato a la tabla.

mean(tires.imp$imp$Brand)
## [1] 17.38339
class(tires.imp$imp$Brand)
## [1] "numeric"
knitr::kable(apply(tires.imp$imp, 2, mean),digits=2, caption = 'Importancia media de los atributos' )
Importancia media de los atributos
Brand 17.38
Price 41.77
Life 35.47
Hazard 5.38

Visualización de la importancia

Finalmente podemos visualizar la importancia media de los atributos con la función visualize.importance() que necesita como argumentos la tabla de datos con las utilidades parciales, tires.imp$part.worths, la tabla de datos con las importancias en porcentajes, tires.imp$imp, y la lista con los nombres de los atributos y niveles, tires$design.

visualize.importance(tires.imp$part.worths,tires.imp$imp, tire$design)

Importance of AttributesImportance of AttributesImportance of AttributesImportance of AttributesImportance of Attributes

Simulación de la respuesta del mercado

El paquete MDSConjoiont también nos proporciona un conjunto de funciones para tomar decisiones con los resultados del análisis conjunto. Concretamente podemos simular las cuotas de mercado que ciertos perfiles de producto obtendrían en el mercado, e identificar el perfil de producto que maximizaría la cuota de mercado de la empresa, dados los perfiles de producto existentes en el mercado. Veámoslo con el ejemplo sintético de Office System que proporciona el conjunto de materiales didácticos del manual Marketing Engineering (Lilien & Ramaswamy 2003).

Description Office System data

This artificial data is about the design of an office store, the kind of products to offer–office supplies, forniture, and computers and software–and the location of the store. The levels of each attribute can be browsed with the design object of the list osc.

Vamos a repetir todo el proceso de estimación del modelo y después abordaremos la toma de decisiones con los resultados.

Para estimar el modelo, primero seleccionaremos los datos del ejemplo Office System asignándolos al objeto osc, y comprobaremos que disponemos de todos los datos necesarios para estimar el modelo.

osc<-MDSConjointData$osc
names(osc)
## [1] "bundles"         "design"          "ratings"         "market.profiles"
## [5] "full"            "constrains"      "revenue"

Estimación del modelo

Seguidamente estimaremos el modelo para todos los individuos de la muestra utilizando la función conjoint.estimation() y como argumentos las valoraciones de los individuos, osc$ratings, la descripción de los perfiles de producto valorados, osc$bundles, y la descripción de los atributos y niveles, osc$design. En el siguiente trozo de código mostramos la clase del objeto que hemos creado osc.partWorthsAll, y comprobaremos todos los datos disponibles, names(osc.partWorthsAll). Al no haber incluido el argumento rs=1 la función no ha registrado los datos detallados de la estimación del modelo para cada individuo, por ello no aparece el objeto summary en la lista, ni los datos correspondientes al primer individuo Respondent1 o cualquier otro.

osc.partWorthsAll<-conjoint.estimation(osc$ratings, osc$bundles, osc$design) 
class(osc.partWorthsAll)
## [1] "list"
names(osc.partWorthsAll)
## [1] "fit"         "part.worths" "prediction"

Resumimos las estimaciones en una tabla de datos

En esta tabla podemos ver el inicio de la tabla de datos con las estimaciones de los 6 primeros individuos de la muestra,

knitr::kable(t(head(osc.partWorthsAll$fit)), digits=2, caption = 'Estimaciones: Resultados de los 6 primeros individuos' )
Estimaciones: Resultados de los 6 primeros individuos
Respondent1 Respondent2 Respondent3 Respondent4 Respondent5 Respondent6
(Intercept) 58.64 53.85 58.56 70.71 70.86 63.24
Location1 9.34 11.74 5.28 0.07 6.58 15.45
Location2 3.81 0.27 -3.05 3.62 -1.92 -3.41
OfficeSupplies1 1.05 2.90 1.59 -0.32 3.29 0.14
OfficeSupplies2 -1.35 -2.60 0.19 -10.92 -14.61 -6.16
Furniture1 -20.36 -0.82 -1.46 -1.46 0.81 -6.82
Computers1 -2.35 -15.89 -20.70 -6.54 1.29 1.32
Computers2 -2.98 -15.27 -6.95 2.21 -3.08 0.07

Resumimos las utilidades parciales en una tabla de datos

El tercer objeto que nos proporciona la función conjoint.estimation() es la tabla de datos con las utilidades parciales de los 6 primeros individuos si utilizamos la función head().

knitr::kable(t(head(osc.partWorthsAll$part.worths)), digits=2, caption = 'Estimaciones: Resultados de los 6 primeros individuos' )
Estimaciones: Resultados de los 6 primeros individuos
Respondent1 Respondent2 Respondent3 Respondent4 Respondent5 Respondent6
Less2Miles 9.34 11.74 5.28 0.07 6.58 15.45
W2-5Miles 3.81 0.27 -3.05 3.62 -1.92 -3.41
W5-10Miles -13.16 -12.01 -2.22 -3.69 -4.66 -12.05
VLAssortment 1.05 2.90 1.59 -0.32 3.29 0.14
LAssortment -1.35 -2.60 0.19 -10.92 -14.61 -6.16
LimAssortment 0.29 -0.30 -1.77 11.24 11.33 6.02
Yes -20.36 -0.82 -1.46 -1.46 0.81 -6.82
No 20.36 0.82 1.46 1.46 -0.81 6.82
NoComputers -2.35 -15.89 -20.70 -6.54 1.29 1.32
Software -2.98 -15.27 -6.95 2.21 -3.08 0.07
SoftwareAndComputers 5.34 31.16 27.64 4.32 1.78 -1.40

Las utilidades teóricas que predice el modelo

El cuarto objeto que nos proporciona la función es la tabla resumen de las predicciones teóricas del modelo, prediction. La correlación entre las utilidades teóricas proporcionadas por el modelo y las empíricas proporcionadas por los individuos nos da una medida sintética de la bondad del ajuste del modelo.

Importancia de los atributos

Como en el ejemplo de los neumáticos, la función importance.of.attributes() nos proporciona tres tablas de datos, una con el resumen de los coeficientes estimados, fit, otra con las utilidades parciales, part.worths, y la tercera con la importancia de cada atributo para cada individuo, en porcentaje.

Con la función kable() del paquete knitr podemos presentarlos en una tabla bien formateada.

osc.imp <- importance.of.attributes(osc$ratings, osc$bundles, osc$design)
names(osc.imp)
## [1] "fit"         "part.worths" "imp"
knitr::kable(head(osc.imp$imp),digits=2, caption = 'Importancia de los atributos: Resultados de los 5 primeros individuos (en %)' )
Importancia de los atributos: Resultados de los 5 primeros individuos (en %)
Location OfficeSupplies Furniture Computers
Respondent1 30.43 3.25 55.08 11.25
Respondent2 30.47 7.06 2.12 60.36
Respondent3 13.23 5.34 4.64 76.79
Respondent4 16.89 51.23 6.77 25.11
Respondent5 25.75 59.38 3.73 11.14
Respondent6 49.07 21.73 24.34 4.85

Para obtener la importancia media de cada uno de los atributos que dan forma a los perfiles de producto valorados, sólo tenemos que calcular la media de las columnas de la tabla imp. Para ello utilizamos la función apply() y como argumentos la tabla de datos tires.imp$imp, el número 2 que indica que queremos trabajar con las columnas de la tabla, y la función mean para decir que queremos calcular la media de las columnas. Utilizamos la función kable() para dar formato a la tabla.

mean(osc.imp$imp$Location)
## [1] 27.29945
class(osc.imp$imp$Location)
## [1] "numeric"
knitr::kable(apply(osc.imp$imp, 2, mean), digits=2, caption = 'Resumen importancia atributos' )
Resumen importancia atributos
Location 27.30
OfficeSupplies 25.15
Furniture 16.47
Computers 31.08

Visualización importancia atributos

Finalmente podemos visualizar la importancia media de los atributos con la función visualize.importance() que necesita como argumentos la tabla de datos con las utilidades parciales, osc.imp$part.worths, la tabla de datos con las importancias en porcentajes, osc.imp$imp, y la lista con los nombres de los atributos y niveles, osc$design.

visualize.importance(osc.imp$part.worths, osc.imp$imp, osc$design)

Importance of AttributesImportance of AttributesImportance of AttributesImportance of AttributesImportance of Attributes

Simulación de las cuotas de mercado

Para simular las cuotas de mercado necesitamos información acerca de los perfiles de producto de la competencia que existe en el mercado, osc$market.profiles en este caso. Esta tabla de datos tienes tantas filas como marcas compiten en el mercado y tantas columnas como atributos forman el producto que ofrecen.

dim(osc$market.profiles)
## [1] 2 4
knitr::kable(osc$market.profiles, digits=2, caption = 'Market profiles')
Market profiles
Location OfficeSupplies Furniture Computers
OfficeEquipment W2-5Miles LAssortment Yes SoftwareAndComputers
DepartmentStore W2-5Miles LimAssortment No Software

También necesitamos la tabla de datos con la descripción de los perfiles de producto valorados por los individuos, osc$bundles en este caso.

knitr::kable(osc$bundles, digits=2, caption = 'Perfiles de producto que han sido valorados')
Perfiles de producto que han sido valorados
Location OfficeSupplies Furniture Computers
Bundle1 Less2Miles VLAssortment Yes NoComputers
Bundle2 Less2Miles LAssortment No Software
Bundle3 Less2Miles LimAssortment No SoftwareAndComputers
Bundle4 Less2Miles LAssortment Yes Software
Bundle5 W2-5Miles VLAssortment Yes Software
Bundle6 W2-5Miles LAssortment No NoComputers
Bundle7 W2-5Miles LimAssortment No Software
Bundle8 W2-5Miles LAssortment Yes SoftwareAndComputers
Bundle9 W5-10Miles VLAssortment No SoftwareAndComputers
Bundle10 W5-10Miles LAssortment Yes Software
Bundle11 W5-10Miles LimAssortment Yes NoComputers
Bundle12 W5-10Miles LAssortment No Software
Bundle13 W2-5Miles VLAssortment No Software
Bundle14 W2-5Miles VLAssortment Yes SoftwareAndComputers
Bundle15 W2-5Miles LimAssortment Yes Software
Bundle16 W2-5Miles LAssortment No NoComputers

Para completar los datos necesitamos también la valoración que han realizado los consumidores de los perfiles de productos, osc$ratings.

knitr::kable(t(head(osc$ratings)), digits=2, caption = 'Valoración de los perfiles de producto: Utilidad empírica')
Valoración de los perfiles de producto: Utilidad empírica
Respondent1 Respondent2 Respondent3 Respondent4 Respondent5 Respondent6
Bundle1 90 50 40 75 90 95
Bundle2 50 55 60 80 80 70
Bundle3 50 95 90 60 70 70
Bundle4 80 50 60 70 70 80
Bundle5 85 50 45 90 80 80
Bundle6 40 40 35 65 75 50
Bundle7 40 40 45 60 50 50
Bundle8 90 85 85 85 75 70
Bundle9 30 75 80 85 80 40
Bundle10 60 35 55 70 75 60
Bundle11 60 20 40 55 50 55
Bundle12 30 25 45 60 60 50
Bundle13 30 25 50 90 80 60
Bundle14 90 85 85 85 75 70
Bundle15 80 35 50 70 50 50
Bundle16 40 45 35 65 75 55

La función utilities.of.profiles() nos predice la utilidad que obtendría cada uno de los perfiles de producto de la competencia existente en el mercado, esto es, la utilidad teórica del modelo. Esta función proporciona una tabla de datos con tantas filas como individuos tenemos en la muestra, n, y tantas columnas como marcas, m, tenemos en el objeto osc$market.profiles. Aunque podemos utilizar esta función, normalmente es utilizada por las funciones que estiman las cuotas de mercado y las funciones que buscan el perfil óptimo.

dim(utilities.of.profiles(osc$market.profiles, osc$ratings, osc$bundles))
## [1] 20  2
knitr::kable(head(utilities.of.profiles(osc$market.profiles, osc$ratings, osc$bundles)), digits=2, caption = 'Utilidad teórica de las marcas existentes en el mercado')
Utilidad teórica de las marcas existentes en el mercado
OfficeEquipment DepartmentStore
Respondent1 89.2 37.77
Respondent2 89.0 35.42
Respondent3 86.2 47.29
Respondent4 79.8 64.16
Respondent5 73.2 52.06
Respondent6 65.4 46.93

Para predecir las cuotas de mercado primero tenemos que decidir qué regla de elección con mayor probabilidad utilizarán los consumidores a la hora de realizar sus elecciones: la regla de la máxima utilidad (primera elección), la regla de la cuota de preferencia (también denominada BTL), y la regla logit. Para el primer caso disponemos de la función ms.fe.conjoint(), para el segundo, ms.us.conjoint(), y para el tercero, ms.logit.conjoint() (Green & Krieger 1995).

La probabilidad de compra resultante de cada regla se calcula de la siguiente manera:

Máxima utilidad (1ª elección): \(p_i(J_k)= 1, si U(J_k)=max(U(J_k))\), si no \(p_i(J_k)= 0\)

Cuota de utilidad (BTL): \(p_i(J_k)= U(J_k)/∑U(J_k)\)

Regla logit: \(p_i(J_k)= exp(U(J_k))/∑exp(U(J_k))\)

Un vez realizados los cálculos para toda la muestra, podemos predecir las cuotas de mercado si todos los individuos de la muestra tienen el mismo volumen de compra y si todas las empresas tienen la misma capacidad de comunicación y distribuión. Pero esto no suele ser así, las empresas con mayor cuota tienen una mayor capacidad de comunicación y distribución. Por ello normalmente después es necesario ajustar las cuotas que predice el modelo. Si la regla de la máxima utilidad es adecuada para situaciones de compra poco frecuente, la regla de la cuota de utilidad lo es para la compra frecuente, y la regla logit realiza un ajusta favoreciendo a las marcas con mayor cuota de preferencia, ajustando ya las cuotas a favor de las marcas con mayor preferencia.

Las tres funciones necesitan como argumento las tres tablas de datos: la tabla con los perfiles de mercado para los que vamos a simular las cuotas de mercado, osc$market.profiles, la base de datos con las valoraciones realizadas por los individuos, osc$ratings, y la base de datos con la descripción de los perfiles de producto valorados, osc$bundles, en este caso.

En el caso de la máxima utilidad o primera elección la cuota de mercado para las dos marcas existentes en el mercado es:

knitr::kable(ms.fe.conjoint(osc$market.profiles, osc$ratings, osc$bundles), digits=2, caption = 'Cuota de mercado: Regla de la maxima utildad')
Cuota de mercado: Regla de la maxima utildad
ms
OfficeEquipment 100
DepartmentStore 0
ms.fe.conjoint(osc$market.profiles, osc$ratings, osc$bundles)
##                  ms
## OfficeEquipment 100
## DepartmentStore   0

Si los individuos utilizaran la regla de la cuota de preferencia, las cuotas resultantes serían:

knitr::kable(ms.us.conjoint(osc$market.profiles, osc$ratings, osc$bundles), digits=2, caption = 'Cuota de mercado: Regla de la cuota de preferencia')
Cuota de mercado: Regla de la cuota de preferencia
ms
OfficeEquipment 62
DepartmentStore 38

Si los individuos utilizaran la regla logit, las cuotas estimadas serían:

knitr::kable(ms.logit.conjoint(osc$market.profiles, osc$ratings, osc$bundles), digits=2, caption = 'Cuota de mercado: Regla logit')
Cuota de mercado: Regla logit
ms
OfficeEquipment 100
DepartmentStore 0

Búsqueda del producto que maximiza el la cuota de mercado

Si estamos interesados en identificar el perfil que maximiza la cuota de mercado de la empresa, dados los perfiles de producto de la competencia, disponemos de las funciones optim.ms.first.choice(), optim.ms.utility.share() y optim.ms.logit() que utilizaremos en función de la regla de decisión que utilicen los individuos.

En todos los casos, la función necesita las valoraciones de los consumidores, osc$ratings, la descripción de los perfiles valorados, osc$bundles, los perfiles de producto de la competencia, osc$market.profiles, la descripción de todos los atributos y niveles que pueden formar los perfiles de producto, osc$design, y el argumento hide progress bar, hpb=1, en el caso de que no queramos ver la barra de progreso del proceso de optimización (la identificación del óptimo puede tardar un poco dependiendo de la capacidad de proceso del ordenador). Este último argumento es interesante cuando utilizamos la función en un documento rmarkdown como en este caso.

La función nos devuelve una lista con dos objetos. El primero nos muestra la descripción del perfil óptimo y el segundo la cuota de mercado que obtendría la competencia y el producto que maximiza la cuota de mercado.

First election or maximum utilitiy

En el caso de utilizar la regla de la máxima utilidad, el perfil que maximiza la cuota de mercado para toda la muestra es el que se muestra seguidamente, y la cuota de mercado del perfil óptimo sería el 100%.

osc.ms.op.1choice<-optim.ms.first.choice(osc$ratings, osc$bundles, osc$market.profiles, osc$design, hpb=1)
knitr::kable(osc.ms.op.1choice, digits=2, caption = 'Regla máxima utilidad')
Regla máxima utilidad
Location OfficeSupplies Furniture Computers
37 Less2Miles VLAssortment Yes SoftwareAndComputers
OfficeEquipment 0
DepartmentStore 0
Optim 100

Regla de la cuota de utilidad

En este caso el consumidor compra con frecuencia y por ello varía su elección en algunas ocasiones de compra.

osc.ms.op.us<-optim.ms.utility.share(osc$ratings, osc$bundles, osc$market.profiles, osc$design, hpb=1)
knitr::kable(osc.ms.op.us, digits=2, caption = 'Regla de la cuota de preferencia')
Regla de la cuota de preferencia
Location OfficeSupplies Furniture Computers
37 Less2Miles VLAssortment Yes SoftwareAndComputers
OfficeEquipment 36.1
DepartmentStore 22.4
Optim 41.5

Regla logit

La regla es similar a la cuota de preferencia pero favorece la predicción de cuota de los perfiles más preferidos.

osc.ms.op.logit<-optim.ms.logit(osc$ratings, osc$bundles, osc$market.profiles, osc$design, hpb=1)
knitr::kable(osc.ms.op.logit, digits=2, caption = 'Regla de la cuota de preferencia')
Regla de la cuota de preferencia
Location OfficeSupplies Furniture Computers
37 Less2Miles VLAssortment Yes SoftwareAndComputers
OfficeEquipment 0.33
DepartmentStore 0.00
Optim 99.67

Conclusiones

Esta primera versión del paquete MDSConjoint nos proporciona herramientas para facilitar la toma de decisiones, la simulación de la cuota de mercado de cualquier perfil de producto, dada la competencia, la obtención del perfil que maximiza la cuota de mercado y en un futuro próximo el perfil que maximiza el margen de contribución (Green, Carroll, & Goldberg, 1981; Green & Krieger, 1995).

Futuras versiones introducirán algoritmos basados en programación dinámica con el objeto de utilizar algoritmos más eficientes en la obtención del producto óptimo (Kohli & Krishnamurthy 1989). Otras mejoras estarán orientadas a incrementar la funcionalidad del paquete en la toma de decisiones comerciales, como la configuración de una línea de productos (Green & Krieger 1992; Michalek, Ebbes, Adigüzel, Feinberg, and Papalambros 2011), la simulación de cuotas de mercado para niveles de producto, como el precio, que no se han considerado expresamente en el diseño del análisis conjunto (Pekelman & Sen 1979), segmentación de mercados según las utilidades parciales (Green & Krieger 1991), o las decisiones de precios (Kohli & Mahajan 1991), etc.

Referencias

Aizaki, H. (2012). Basic Functions for Supporting an Implementation of Choice Experiments in R | Aizaki | J. Journal of Statistical Software, 50. Retrieved from https://www.jstatsoft.org/article/view/v050c02

Green, P. E., & Krieger, A. M. (1995). Attribute importance weights modification in assessing a Brand’s competitive potential. Marketing Science, 14(3), 253–270.

Green, P. E., & Krieger, A. M. (1988). Choice Rules and Sensitivity Analysis in Conjoint Simulators. Journal of the Academy of Marketing Science, 16(1), 114–127. http://doi.org/10.1177/009207038801600110

Green, P. E., & Srinivasan, V. (1978). Conjoint Analysis in Consumer Research: Issues and Outlook. Journal of Consumer Research, 5(September), 103–123.

Green, P. E., Carroll, J. D., & Goldberg, S. M. (1981). A General Approach to Porducht Design Optimization via Conjoint Analysis. Journal of Marketing, 45(Summer), 17–37.

Green, P. E., & Krieger, A. M. (1992). An application of a product positioning model to pharmaceutical products. MARKETING SCIENCE, 11(2).

Green, P. E., Krieger, A. M., & Wind, Y. (2001). Thirty Years of Conjoint Analysis: Reflections and Prospects. Interfaces, 31(3_supplement), S56–S73. http://doi.org/10.1287/inte.31.3s.56.9676

Green, P. E., & Krieger, A. M. (1991). Segmenting Markets with Conjoint Analysis. Journal of Marketing, 55(4), 20–31. http://doi.org/10.2307/1251954

Kohli, R., & Mahajan, V. (1991). A Reservation-Price Model for Optimal Pricing of Multiattribute Products in Conjoint Analysis. Journal of Marketing Research, 28(August), 347–354.

Kohli, R., & Krishnamurthy, R. (1989). Optimal product design using conjoint analysis: Computational complexity and algorithms. European Journal of Operational Research, 40, 186–195.

Lilien, Gary L., and Arvind Rangaswamy. Marketing Engineering: Computer-Assisted Marketing Analysis and Planning by Gary L. Lilien. Pearson, 2003.

Michalek, Jeremy J., Peter Ebbes, Feray Adigüzel, Fred M. Feinberg, and Panos Y. Papalambros. “Enhancing Marketing with Engineering: Optimal Product Line Design for Heterogeneous Markets☆☆☆.” International Journal of Research in Marketing, January 2011. doi:10.1016/j.ijresmar.2010.08.001.

Pekelman, D., & Sen, S. K. (1979). Improving Prediction in Conjoint Measurement. Journal of Marketing Research, 16(2), 211–220. http://doi.org/10.2307/3150685

SAS Institute Inc.(1993), SAS TeclmicalReport R-109, Conjoint Analysis Examples, Gary, NC: SAS Institute Inc.,85 pp.

Anexo: leer los datos de una fuente externa

El paquete XLConnect y junto con el complemento XLConnectJars nos proporcionan la posibilidad de leer libros de hojas electrónicas en formato xlsx.

require(XLConnect)
## Loading required package: XLConnect
## Loading required package: XLConnectJars
## XLConnect 0.2-12 by Mirai Solutions GmbH [aut],
##   Martin Studer [cre],
##   The Apache Software Foundation [ctb, cph] (Apache POI, Apache Commons
##     Codec),
##   Stephen Colebourne [ctb, cph] (Joda-Time Java library),
##   Graph Builder [ctb, cph] (Curvesapi Java library)
## http://www.mirai-solutions.com ,
## http://miraisolutions.wordpress.com
require(XLConnectJars)

La función readWorksheet() nos permite leer un libro de hojas electrónicas, sas-conjoint.xlsx por ejemplo, y guardarlo en el objeto tireData, por ejemplo.

tireData <- XLConnect::loadWorkbook("sas-conjoint.xlsx", create = T) #loading the spreadshit book

Después podemos ir leyendo las diferentes hojas que nos interesan: ratings, design y bundles. La primera contiene las evaluación de los individuos, la segunda los atributos y niveles utilizados en el estudio, y la última, bundles, contiene la descripción de los perfiles de producto.

# -- Read in conjoint rating data 
tiresBundles <- readWorksheet(tireData, rownames=1, sheet = "bundles", header = TRUE) #load the set of bundles rated by informants
tiresDesign <- readWorksheet(tireData, sheet = "design", header = TRUE) #read conjoint desing
tiresDesign.l <- df2list(tiresDesign)
tiresDesign.l
## $X.Brand
## [1] "Goodstone" "Pirogi"    "Machismo" 
## 
## $Price
## [1] "$69,99" "$74,99" "$79,99"
## 
## $Life
## [1] "50000km" "60000km" "70000km"
## 
## $Hazard
## [1] "Yes" "No"
tiresRatings <- readWorksheet(tireData, rownames=1, sheet = "ratings", header = TRUE)

También podemos leer los datos en formato csv.

tirebundles <- read.table("tirebundles.csv", row.names=1, header=T, dec = ".", sep=";")
tiredesign <- read.table("tiredesign.csv", header=T,  sep=";")
tiresDesign.l <- df2list(tiresDesign)
tiresDesign.l
## $X.Brand
## [1] "Goodstone" "Pirogi"    "Machismo" 
## 
## $Price
## [1] "$69,99" "$74,99" "$79,99"
## 
## $Life
## [1] "50000km" "60000km" "70000km"
## 
## $Hazard
## [1] "Yes" "No"
tireratings <- read.table("tireratings.csv", row.names=1, header=T, sep=";")

Un vez leidos los datos podemos comprobar si la lectura de datos ha sido correcta. Para ello utilizamos las funciones head(), tail()y dim().

tiredesign #conjoint design
##       Brand  Price    Life Hazard
## 1 Goodstone $69,99 50000km    Yes
## 2    Pirogi $74,99 60000km     No
## 3  Machismo $79,99 70000km
dim(tirebundles) #check the dimensions of bundles data frame
## [1] 18  4
class(tirebundles) #ckeck the data class
## [1] "data.frame"
head(tirebundles)
##             Brand   Price    Life Hazard
## bundle1 Machismo  $74.99  50000km    No 
## bundle2 Goodstone $69.99  50000km    No 
## bundle3   Pirogi  $74.99  70000km    No 
## bundle4 Goodstone $79.99  70000km    Yes
## bundle5   Pirogi  $74.99  70000km    No 
## bundle6 Machismo  $69.99  70000km    Yes
tail(tirebundles)
##              Brand   Price    Life Hazard
## bundle13 Goodstone $74.99  60000km    Yes
## bundle14   Pirogi  $69.99  60000km    Yes
## bundle15 Goodstone $74.99  60000km    Yes
## bundle16 Machismo  $79.99  60000km    No 
## bundle17 Machismo  $79.99  60000km    No 
## bundle18 Machismo  $69.99  70000km    Yes
head(tireratings) #checking data
##       bundle1 bundle2 bundle3 bundle4 bundle5 bundle6 bundle7 bundle8
## Subj1       3       6       6       4       7       9       3       3
## Subj2       5       8       3       4       4       8       1       5
## Subj3       1       2       7       7       9       9       3       1
## Subj4       3       3       5       8       6       9       1       4
## Subj5       3       6       6       3       7       9       2       3
##       bundle9 bundle10 bundle11 bundle12 bundle13 bundle14 bundle15
## Subj1       8        2        3        6        6        9        5
## Subj2       7        1        4        9        6        6        6
## Subj3       6        2        6        4        5        7        5
## Subj4       5        1        9        3        5        6        7
## Subj5       8        2        4        6        6        8        5
##       bundle16 bundle17 bundle18
## Subj1        2        2        8
## Subj2        2        2        8
## Subj3        4        4        8
## Subj4        4        4        9
## Subj5        2        2        9
tail(tireratings)
##       bundle1 bundle2 bundle3 bundle4 bundle5 bundle6 bundle7 bundle8
## Subj1       3       6       6       4       7       9       3       3
## Subj2       5       8       3       4       4       8       1       5
## Subj3       1       2       7       7       9       9       3       1
## Subj4       3       3       5       8       6       9       1       4
## Subj5       3       6       6       3       7       9       2       3
##       bundle9 bundle10 bundle11 bundle12 bundle13 bundle14 bundle15
## Subj1       8        2        3        6        6        9        5
## Subj2       7        1        4        9        6        6        6
## Subj3       6        2        6        4        5        7        5
## Subj4       5        1        9        3        5        6        7
## Subj5       8        2        4        6        6        8        5
##       bundle16 bundle17 bundle18
## Subj1        2        2        8
## Subj2        2        2        8
## Subj3        4        4        8
## Subj4        4        4        9
## Subj5        2        2        9
dim(tireratings)
## [1]  5 18