Page 1 of 1

Negative scale coefficient in MaxDiff / Case 2 BWS LC model

Posted: 08 Jul 2026, 11:43
by dominik_uibk
Dear Apollo team,

I am analysing results from a Best-Worst Scaling using a Latent Class Model with two classes (for now). We estimate separate attribute, attribute level parameters for both classes as well as separat scale paramaters (for the scale of the error variance of the worst vote relative to the best vote). We found a negative scale paramater for one of the classes which we are unsure how to proceed with.

The model builds on the simultanous or MaxDiff model (using the code of the apollo online example). We've estimated a Mixed Logit before and found large between-respondent heterogeneity for some of the coefficients (and then hoped to be able to reveal more information on voting patterns through the LCM). The scale parameter in the MXL was mu_worst=0.67. In the 2-class LCM, we got a similar value for one of the classes, but a significant negative scale paramter of mu_worst=-0.10 for the other (see code and results below). I would not have been to surprised about a class/group with a zero scale paramter, which I would have been interpret as a random worst choice or some kind of worst-non attendance. But I am unsure how to interpret with the the negative value.
Since the model is unconstrained, is it effectively dragging the coefficient to zero? Should we constrain the paramter ("mu_worst_1>0, mu_worst_2>0")?

Thank you and best regards

Dominik


Code:

Code: Select all

# ################################################################# #
#### DEFINE MODEL PARAMETERS                                     ####
# ################################################################# #

apollo_beta = c(

  # --- Class 1 (RUM) ---
  # attribute constants (asc_busfreq_1 fixed as reference)
  asc_busfreq_1    = 0,
  asc_mobhub_1     = -0.1,
  asc_shareserv_1  = -0.1,
  asc_facil_1      = -0.1,
  asc_parkpol_1    = -0.1,
  asc_caraccess_1  = -0.1,
  # busfreq (base level = 2: "3 times/h")
  b_busfreq_15min_1 = -0.1,
  b_busfreq_30min_1 = -0.1,
  b_busfreq_60min_1 = -0.1,
  # mobhub (base level = 5: "only stop")
  b_mobhub_lockers_1  = -0.1,
  b_mobhub_charging_1 = -0.1,
  b_mobhub_rental_1   = -0.1,
  # shareserv (base level = 11: "pooling")
  b_shareserv_shuttle_1 = -0.1,
  b_shareserv_hailing_1 = -0.1,
  b_shareserv_sharing_1 = -0.1,
  # facil (base level = 13: "EV charge price")
  b_facil_EVchargefree_1  = -0.1,
  b_facil_EVhotelprice_1  = -0.1,
  b_facil_EVhotelfree_1   = -0.1,
  # parkpol (base level = 17: "1.5 EUR/h")
  b_parkpol_3_1  = -0.1,
  b_parkpol_6_1  = -0.1,
  b_parkpol_12_1 = -0.1,
  # caraccess (base level = 21: "restricted")
  b_caraccess_nocar_park0_1    = -0.1,
  b_caraccess_nocar_park12.5_1 = -0.1,
  b_caraccess_nocar_park25_1   = -0.1,
  
  # --- Class 2 (RUM) ---
  asc_busfreq_2    = 0,
  asc_mobhub_2     = 0.2,
  asc_shareserv_2  = 0.2,
  asc_facil_2      = 0.2,
  asc_parkpol_2    = 0.2,
  asc_caraccess_2  = 0.2,
  b_busfreq_15min_2 = 0.2,
  b_busfreq_30min_2 = 0.2,
  b_busfreq_60min_2 = 0.2,
  b_mobhub_lockers_2  = 0.2,
  b_mobhub_charging_2 = 0.2,
  b_mobhub_rental_2   = 0.2,
  b_shareserv_shuttle_2 = 0.2,
  b_shareserv_hailing_2 = 0.2,
  b_shareserv_sharing_2 = 0.2,
  b_facil_EVchargefree_2  = 0.2,
  b_facil_EVhotelprice_2  = 0.2,
  b_facil_EVhotelfree_2   = 0.2,
  b_parkpol_3_2  = 0.2,
  b_parkpol_6_2  = 0.2,
  b_parkpol_12_2 = 0.2,
  b_caraccess_nocar_park0_2    = 0.2,
  b_caraccess_nocar_park12.5_2 = 0.2,
  b_caraccess_nocar_park25_2   = 0.2,

  # --- Separate scale parameter ---
  mu_worst_1 = 1,  
  mu_worst_2 = 1,

  # --- Class membership (logit) ---
  delta_1 = 0,
  delta_2 = 0
)

apollo_fixed = c("asc_busfreq_1", "asc_busfreq_2", "delta_2")


# ################################################################# #
#### DEFINE LATENT CLASS PARAMETERS                              ####
# ################################################################# #

apollo_lcPars = function(apollo_beta, apollo_inputs) {
  lcpars = list()

  # Map each attribute parameter to its per-class values (list of length = nClasses)
  lcpars[["asc_busfreq"]]   = list(asc_busfreq_1,   asc_busfreq_2)
  lcpars[["asc_mobhub"]]    = list(asc_mobhub_1,    asc_mobhub_2)
  lcpars[["asc_shareserv"]] = list(asc_shareserv_1, asc_shareserv_2)
  lcpars[["asc_facil"]]     = list(asc_facil_1,     asc_facil_2)
  lcpars[["asc_parkpol"]]   = list(asc_parkpol_1,   asc_parkpol_2)
  lcpars[["asc_caraccess"]] = list(asc_caraccess_1, asc_caraccess_2)

  lcpars[["b_busfreq_15min"]] = list(b_busfreq_15min_1, b_busfreq_15min_2)
  lcpars[["b_busfreq_30min"]] = list(b_busfreq_30min_1, b_busfreq_30min_2)
  lcpars[["b_busfreq_60min"]] = list(b_busfreq_60min_1, b_busfreq_60min_2)

  lcpars[["b_mobhub_lockers"]]  = list(b_mobhub_lockers_1,  b_mobhub_lockers_2)
  lcpars[["b_mobhub_charging"]] = list(b_mobhub_charging_1, b_mobhub_charging_2)
  lcpars[["b_mobhub_rental"]]   = list(b_mobhub_rental_1,   b_mobhub_rental_2)

  lcpars[["b_shareserv_shuttle"]] = list(b_shareserv_shuttle_1, b_shareserv_shuttle_2)
  lcpars[["b_shareserv_hailing"]] = list(b_shareserv_hailing_1, b_shareserv_hailing_2)
  lcpars[["b_shareserv_sharing"]] = list(b_shareserv_sharing_1, b_shareserv_sharing_2)

  lcpars[["b_facil_EVchargefree"]]  = list(b_facil_EVchargefree_1,  b_facil_EVchargefree_2)
  lcpars[["b_facil_EVhotelprice"]]  = list(b_facil_EVhotelprice_1,  b_facil_EVhotelprice_2)
  lcpars[["b_facil_EVhotelfree"]]   = list(b_facil_EVhotelfree_1,   b_facil_EVhotelfree_2)

  lcpars[["b_parkpol_3"]]  = list(b_parkpol_3_1,  b_parkpol_3_2)
  lcpars[["b_parkpol_6"]]  = list(b_parkpol_6_1,  b_parkpol_6_2)
  lcpars[["b_parkpol_12"]] = list(b_parkpol_12_1, b_parkpol_12_2)

  lcpars[["b_caraccess_nocar_park0"]]    = list(b_caraccess_nocar_park0_1,    b_caraccess_nocar_park0_2)
  lcpars[["b_caraccess_nocar_park12.5"]] = list(b_caraccess_nocar_park12.5_1, b_caraccess_nocar_park12.5_2)
  lcpars[["b_caraccess_nocar_park25"]]   = list(b_caraccess_nocar_park25_1,   b_caraccess_nocar_park25_2)
  
  lcpars[["mu_worst"]]   = list(mu_worst_1,   mu_worst_2)

  # Class membership (delta_2 = 0 fixed as reference)
  V = list()
  V[["class_1"]] = delta_1
  V[["class_2"]] = delta_2

  classAlloc_settings = list(
    classes   = c(class_1 = 1, class_2 = 2),
    avail     = 1,
    utilities = V
  )

  lcpars[["pi_values"]] = apollo_classAlloc(classAlloc_settings)

  return(lcpars)
}


# ################################################################# #
#### GROUP AND VALIDATE INPUTS                                   ####
# ################################################################# #

apollo_inputs = apollo_validateInputs()


# ################################################################# #
#### DEFINE MODEL AND LIKELIHOOD FUNCTION                        ####
# ################################################################# #

apollo_probabilities = function(apollo_beta, apollo_inputs, functionality = "estimate") {

  ### Attach inputs and detach after function exit
  apollo_attach(apollo_beta, apollo_inputs)
  on.exit(apollo_detach(apollo_beta, apollo_inputs))

  ### Create list of probabilities P
  P = list()

  ### Pre-build the 30 best-!=worst paired alternative codes (shared across classes)
  alts = list()
  for (best in 1:6) for (worst in 1:6) if (best != worst)
    alts[[paste0("alt_b", best, "_w", worst)]] = 10*best + worst

  choiceVar = 10 * best_attr + worst_attr

  ### Loop over latent classes
  for (s in 1:2) {

    ### Effects coding constraints using class-s parameters
    b_busfreq_3times      = -b_busfreq_15min[[s]] - b_busfreq_30min[[s]] - b_busfreq_60min[[s]]
    b_mobhub_onlystop     = -b_mobhub_lockers[[s]] - b_mobhub_charging[[s]] - b_mobhub_rental[[s]]
    b_shareserv_pooling   = -b_shareserv_shuttle[[s]] - b_shareserv_hailing[[s]] - b_shareserv_sharing[[s]]
    b_facil_EVchargeprice = -b_facil_EVchargefree[[s]] - b_facil_EVhotelprice[[s]] - b_facil_EVhotelfree[[s]]
    b_parkpol_1.5         = -b_parkpol_3[[s]] - b_parkpol_6[[s]] - b_parkpol_12[[s]]
    b_caraccess_restr     = -b_caraccess_nocar_park0[[s]] - b_caraccess_nocar_park12.5[[s]] - b_caraccess_nocar_park25[[s]]

    ### Attribute-level utilities for class s
    V = list()
    V[["alt1"]] = asc_busfreq[[s]]   + b_busfreq_15min[[s]]*(bus_freq==1)    + b_busfreq_3times*(bus_freq==2)     + b_busfreq_30min[[s]]*(bus_freq==3)    + b_busfreq_60min[[s]]*(bus_freq==4)
    V[["alt2"]] = asc_mobhub[[s]]    + b_mobhub_onlystop*(mob_hub==5)        + b_mobhub_lockers[[s]]*(mob_hub==6) + b_mobhub_charging[[s]]*(mob_hub==7)   + b_mobhub_rental[[s]]*(mob_hub==8)
    V[["alt3"]] = asc_shareserv[[s]] + b_shareserv_shuttle[[s]]*(share_serv==9) + b_shareserv_hailing[[s]]*(share_serv==10) + b_shareserv_pooling*(share_serv==11) + b_shareserv_sharing[[s]]*(share_serv==12)
    V[["alt4"]] = asc_facil[[s]]     + b_facil_EVchargeprice*(facil==13)     + b_facil_EVchargefree[[s]]*(facil==14) + b_facil_EVhotelprice[[s]]*(facil==15) + b_facil_EVhotelfree[[s]]*(facil==16)
    V[["alt5"]] = asc_parkpol[[s]]   + b_parkpol_1.5*(park_pol==17)          + b_parkpol_3[[s]]*(park_pol==18)    + b_parkpol_6[[s]]*(park_pol==19)       + b_parkpol_12[[s]]*(park_pol==20)
    V[["alt6"]] = asc_caraccess[[s]] + b_caraccess_restr*(car_access==21)    + b_caraccess_nocar_park0[[s]]*(car_access==22) + b_caraccess_nocar_park12.5[[s]]*(car_access==23) + b_caraccess_nocar_park25[[s]]*(car_access==24)

    ### Paired-alternative utilities for class s
    utils = list()
    for (b in 1:6) for (w in 1:6) if (b != w) {
      nm          = paste0("alt_b", b, "_w", w)
      utils[[nm]] = V[[paste0("alt", b)]] - mu_worst[[s]] * V[[paste0("alt", w)]]
    }

    ### MNL choice probability for this class
    mnl_settings = list(
      alternatives  = unlist(alts),
      choiceVar     = choiceVar,
      utilities     = utils,
      componentName = paste0("Class_", s)
    )

    P[[paste0("Class_", s)]] = apollo_mnl(mnl_settings, functionality)

    ### Panel product: multiply across tasks for the same respondent
    P[[paste0("Class_", s)]] = apollo_panelProd(P[[paste0("Class_", s)]], apollo_inputs, functionality)
  }

  ### Latent class combination (pi_values provided by apollo_lcPars via apollo_attach)
  lc_settings = list(inClassProb = P, classProb = pi_values)
  P[["model"]] = apollo_lc(lc_settings, apollo_inputs, functionality)

  ### Prepare and return outputs of function
  P = apollo_prepareProb(P, apollo_inputs, functionality)
  return(P)
}


# ################################################################# #
#### MODEL ESTIMATION                                            ####
# ################################################################# #

model = apollo_estimate(apollo_beta, apollo_fixed, apollo_probabilities, apollo_inputs)
Results:

Code: Select all

Model name                                  : BWScase2_simultan_LC_baseline_2c_scaled
Model description                           : Best-Worst Scaling Case 2 latent class model:
                     to model heterogenous voting behavior
                     Both classes have class-specific utility parameters.
Model run at                                : 2026-06-18 22:55:31.72068
Estimation method                           : bgw
Estimation diagnosis                        : Relative function convergence
Optimisation diagnosis                      : Maximum found
     hessian properties                     : Negative definite
     maximum eigenvalue                     : -16.421932
     reciprocal of condition number         : 0.00713948
Number of individuals                       : 1199
Number of rows in database                  : 9592
Number of modelled outcomes                 : 9592

Number of cores used                        :  8 
Model without mixing

LL(start)                                   : -32725.26
LL (whole model) at equal shares, LL(0)     : -32624.29
LL (whole model) at observed shares, LL(C)  : -31264.99
LL(final, whole model)                      : -29721.55
Rho-squared vs equal shares                  :  0.089 
Adj.Rho-squared vs equal shares              :  0.0875 
Rho-squared vs observed shares               :  0.0494 
Adj.Rho-squared vs observed shares           :  0.0497 
AIC                                         :  59541.1 
BIC                                         :  59892.37 

LL(0,Class_1)                    : -32624.29
LL(final,Class_1)                : -34873.08
LL(0,Class_2)                    : -32624.29
LL(final,Class_2)                : -33162.64

Estimated parameters                        : 49
Time taken (hh:mm:ss)                       :  00:07:44.78 
     pre-estimation                         :  00:00:42.14 
     estimation                             :  00:01:43.13 
     post-estimation                        :  00:05:19.51 
Iterations                                  :  47  

Unconstrained optimisation.

Estimates:
                                Estimate        s.e.   t.rat.(0)  p(2-sided)    t.rat(1)  p(2-sided)
asc_busfreq_1                    0.00000          NA          NA          NA          NA          NA
asc_mobhub_1                    -0.72143     0.04837  -14.915126    0.000000     -35.589    0.000000
asc_shareserv_1                 -1.17491     0.07983  -14.717072    0.000000     -27.243    0.000000
asc_facil_1                     -1.33438     0.11558  -11.545291    0.000000     -20.197    0.000000
asc_parkpol_1                   -2.17150     0.12388  -17.529295    0.000000     -25.602    0.000000
asc_caraccess_1                 -1.67670     0.07877  -21.285293    0.000000     -33.980    0.000000
...
mu_worst_1                       0.80148     0.05737   13.970362    0.000000      -3.460  5.3950e-04
mu_worst_2                      -0.10034     0.04534   -2.212857    0.026907     -24.267    0.000000
delta_1                         -0.16442     0.09688   -1.697220    0.089655     -12.019    0.000000
delta_2                          0.00000          NA          NA          NA          NA          NA
                                Rob.s.e. Rob.t.rat.(0)  p(2-sided) Rob.t.rat.(1)  p(2-sided)
asc_busfreq_1                         NA            NA          NA            NA          NA
asc_mobhub_1                     0.22235     -3.244541    0.001176       -7.7419   9.770e-15
asc_shareserv_1                  0.46984     -2.500665    0.012396       -4.6291   3.673e-06
asc_facil_1                      0.76029     -1.755092    0.079244       -3.0704    0.002138
asc_parkpol_1                    0.64751     -3.353606  7.9766e-04       -4.8980   9.683e-07
asc_caraccess_1                  0.37112     -4.517978   6.243e-06       -7.2125   5.491e-13
...
mu_worst_1                       0.10106      7.930360   2.220e-15       -1.9643    0.049498
mu_worst_2                       0.27263     -0.368042    0.712842       -4.0360   5.436e-05
delta_1                          0.49737     -0.330586    0.740957       -2.3411    0.019225
delta_2                               NA            NA          NA            NA          NA


Summary of class allocation for model component :
         Mean prob.
Class_1      0.4590
Class_2      0.5410

Re: Negative scale coefficient in MaxDiff / Case 2 BWS LC model

Posted: 01 Aug 2026, 16:42
by dpalma
Hi Dominik,

While I am no expert in Best-Worst, my limited experience with it is that a wrong scale parameter simply indicates that the model formulation is not supported by the data. This would be similar to obtaining an out-of-range nest parameter in a nested logit, which simply means that the nesting structure is not supported by the data.

I would try:
  • Different starting values. You can try using apollo_searchStart for this.
  • Different utility functions formulation
  • Different parameters for the best and worst utilities. Sometimes preferences for the best and worst alternatives are different for some attributes.
I hope this helps.

Best wishes,
David