Mixed Logit

The following examples provide step-by-step instructions to estimate mixed logit models using the xlogit package. You can interactively execute the code in this guide by opening it Google Colab using the following link:

Open In Colab

Install and import xlogit package

Install xlogit using pip as shown below. In addition, import the package and check if GPU processing is available.

[1]:
!pip install xlogit
from xlogit import MixedLogit
MixedLogit.check_if_gpu_available()
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting xlogit
  Downloading xlogit-0.2.0-py3-none-any.whl (35 kB)
Requirement already satisfied: numpy>=1.13.1 in /usr/local/lib/python3.7/dist-packages (from xlogit) (1.21.6)
Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.7/dist-packages (from xlogit) (1.4.1)
Installing collected packages: xlogit
Successfully installed xlogit-0.2.0
1 GPU device(s) available. xlogit will use GPU processing
[1]:
True

Swissmetro Dataset

The swissmetro dataset contains stated-preferences for three alternative transportation modes that include car, train and a newly introduced mode: the swissmetro. This dataset is commonly used for estimation examples with the Biogeme and PyLogit packages. The dataset is available at http://transp-or.epfl.ch/data/swissmetro.dat and Bierlaire et. al., (2001) provides a detailed discussion of the data as wells as its context and collection process. The explanatory variables in this example include the travel time (TT) and cost CO for each of the three alternative modes.

Read data

The dataset is imported to the Python environment using pandas. Then, two types of samples, ones with a trip purpose different to commute or business and ones with an unknown choice, are filtered out. The original dataset contains 10,729 records, but after filtering, 6,768 records remain for following analysis. Finally, a new column that uniquely identifies each sample is added to the dataframe and the CHOICE column, which originally contains a numerical coding of the choices, is mapped to a description that is consistent with the alternatives in the column names.

[2]:
import pandas as pd
import numpy as np

df_wide = pd.read_table("http://transp-or.epfl.ch/data/swissmetro.dat", sep='\t')

# Keep only observations for commute and business purposes that contain known choices
df_wide = df_wide[(df_wide['PURPOSE'].isin([1, 3]) & (df_wide['CHOICE'] != 0))]

df_wide['custom_id'] = np.arange(len(df_wide))  # Add unique identifier
df_wide['CHOICE'] = df_wide['CHOICE'].map({1: 'TRAIN', 2:'SM', 3: 'CAR'})
df_wide
[2]:
GROUP SURVEY SP ID PURPOSE FIRST TICKET WHO LUGGAGE AGE ... TRAIN_CO TRAIN_HE SM_TT SM_CO SM_HE SM_SEATS CAR_TT CAR_CO CHOICE custom_id
0 2 0 1 1 1 0 1 1 0 3 ... 48 120 63 52 20 0 117 65 SM 0
1 2 0 1 1 1 0 1 1 0 3 ... 48 30 60 49 10 0 117 84 SM 1
2 2 0 1 1 1 0 1 1 0 3 ... 48 60 67 58 30 0 117 52 SM 2
3 2 0 1 1 1 0 1 1 0 3 ... 40 30 63 52 20 0 72 52 SM 3
4 2 0 1 1 1 0 1 1 0 3 ... 36 60 63 42 20 0 90 84 SM 4
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
8446 3 1 1 939 3 1 7 3 1 5 ... 13 30 50 17 30 0 130 64 TRAIN 6763
8447 3 1 1 939 3 1 7 3 1 5 ... 12 30 53 16 10 0 80 80 TRAIN 6764
8448 3 1 1 939 3 1 7 3 1 5 ... 16 60 50 16 20 0 80 64 TRAIN 6765
8449 3 1 1 939 3 1 7 3 1 5 ... 16 30 53 17 30 0 80 104 TRAIN 6766
8450 3 1 1 939 3 1 7 3 1 5 ... 13 60 53 21 30 0 100 80 TRAIN 6767

6768 rows × 29 columns

Reshape data

The imported dataframe is in wide format, and it needs to be reshaped to long format for processing by xlogit, which offers the convenient wide_to_long utility for this reshaping process. The user needs to specify the column that uniquely identifies each sample, the names of the alternatives, the columns that vary across alternatives, and whether the alternative names are a prefix or suffix of the column names. Additionally, the user can specify a value (empty_val) to be used by default when an alternative is not available for a certain variable. Additional usage examples for the wide_to_long function are available in xlogit’s documentation at https://xlogit.readthedocs.io/en/latest/notebooks/convert_data_wide_to_long.html. Also, details about the function parameters are available at the API reference.

[3]:
from xlogit.utils import wide_to_long

df = wide_to_long(df_wide, id_col='custom_id', alt_name='alt', sep='_',
                  alt_list=['TRAIN', 'SM', 'CAR'], empty_val=0,
                  varying=['TT', 'CO', 'HE', 'AV', 'SEATS'], alt_is_prefix=True)
df
[3]:
custom_id alt TT CO HE AV SEATS GROUP SURVEY SP ... TICKET WHO LUGGAGE AGE MALE INCOME GA ORIGIN DEST CHOICE
0 0 TRAIN 112 48 120 1 0 2 0 1 ... 1 1 0 3 0 2 0 2 1 SM
1 0 SM 63 52 20 1 0 2 0 1 ... 1 1 0 3 0 2 0 2 1 SM
2 0 CAR 117 65 0 1 0 2 0 1 ... 1 1 0 3 0 2 0 2 1 SM
3 1 TRAIN 103 48 30 1 0 2 0 1 ... 1 1 0 3 0 2 0 2 1 SM
4 1 SM 60 49 10 1 0 2 0 1 ... 1 1 0 3 0 2 0 2 1 SM
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
20299 6766 SM 53 17 30 1 0 3 1 1 ... 7 3 1 5 1 2 0 1 2 TRAIN
20300 6766 CAR 80 104 0 1 0 3 1 1 ... 7 3 1 5 1 2 0 1 2 TRAIN
20301 6767 TRAIN 108 13 60 1 0 3 1 1 ... 7 3 1 5 1 2 0 1 2 TRAIN
20302 6767 SM 53 21 30 1 0 3 1 1 ... 7 3 1 5 1 2 0 1 2 TRAIN
20303 6767 CAR 100 80 0 1 0 3 1 1 ... 7 3 1 5 1 2 0 1 2 TRAIN

20304 rows × 23 columns

Create model specification

Following the reshaping, users can create or update the dataset’s columns in order to accommodate their model specification needs, if necessary. The code below shows how the columns ASC_TRAIN and ASC_CAR were created to incorporate alternative-specific constants in the model. In addition, the example illustrates an effective way of establishing variable interactions (e.g., trip costs for commuters with an annual pass) by updating existing columns conditional on values of other columns. Although apparently simple, column operations provide users with an intuitive and highly-flexible mechanism to incorporate model specification aspects, such as variable transformations, interactions, and alternative specific coefficients and constants. By operating the dataframe columns, any utility specification can be accommodated in xlogit. As shown in this specification example, highly-flexible utility specifications can be modeled in xlogit by operating the dataframe columns.

[4]:
df['ASC_TRAIN'] = np.ones(len(df))*(df['alt'] == 'TRAIN')
df['ASC_CAR'] = np.ones(len(df))*(df['alt'] == 'CAR')
df['TT'], df['CO'] = df['TT']/100, df['CO']/100  # Scale variables
annual_pass = (df['GA'] == 1) & (df['alt'].isin(['TRAIN', 'SM']))
df.loc[annual_pass, 'CO'] = 0  # Cost zero for pass holders

Estimate model parameters

The fit method estimates the model by taking as input the data from the previous step along with additional specification criteria, such as the distribution of the random parameters (randvars), the number of random draws (n_draws), and the availability of alternatives for the choice situations (avail). We set the optimization method as L-BFGS-B as this is a robust routine that usually helps solve convergence issues. Once the estimation routine is completed, the summary method can be used to display the estimation results.

[5]:
from xlogit import MixedLogit
varnames=['ASC_CAR', 'ASC_TRAIN', 'CO', 'TT']
model = MixedLogit()
model.fit(X=df[varnames], y=df['CHOICE'], varnames=varnames,
          alts=df['alt'], ids=df['custom_id'], avail=df['AV'],
          panels=df["ID"], randvars={'TT': 'n'}, n_draws=1500,
          optim_method='L-BFGS-B')
model.summary()
GPU processing enabled.
Optimization terminated successfully.
    Message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH'
    Iterations: 14
    Function evaluations: 15
Estimation time= 17.0 seconds
---------------------------------------------------------------------------
Coefficient              Estimate      Std.Err.         z-val         P>|z|
---------------------------------------------------------------------------
ASC_CAR                 0.2831085     0.0560480     5.0511797      2.35e-06 ***
ASC_TRAIN              -0.5722790     0.0794780    -7.2004737      4.84e-12 ***
CO                     -1.6601703     0.0778870   -21.3151016      2.52e-96 ***
TT                     -3.2289850     0.1749807   -18.4533828      5.58e-73 ***
sd.TT                   3.6485337     0.1683459    21.6728359      1.88e-99 ***
---------------------------------------------------------------------------
Significance:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Log-Likelihood= -4359.218
AIC= 8728.436
BIC= 8762.536

The negative signs for the cost and time coefficients suggest that decision makers experience a general disutility with alternatives that have higher waiting times and costs, which conforms to the underlying decision making theory. Note that these estimates are highly consistent with those returned by Biogeme (https://biogeme.epfl.ch/examples/swissmetro/05normalMixtureIntegral.html)

Electricity Dataset

The electricity dataset contains 4,308 choices among four electricity suppliers based on the attributes of the offered plans, which include prices(pf), contract lengths(cl), time of day rates (tod), seasonal rates(seas), as well as attributes of the suppliers, which include whether the supplier is local (loc) and well-known (wk). The data was collected through a survey where 12 different choice situations were presented to each participant. The multiple responses per participants were organized into panels. Given that some participants answered less than 12 of the choice situations, some panels are unbalanced, which xlogit is able to handle. Revelt and Train (1999) provide a detailed description of this dataset.

Read data

The dataset is already in long format so no reshaping is necessary, it can be used directly in xlogit.

[6]:
import pandas as pd

df = pd.read_csv("https://raw.github.com/arteagac/xlogit/master/examples/data/electricity_long.csv")
df
[6]:
choice id alt pf cl loc wk tod seas chid
0 0 1 1 7 5 0 1 0 0 1
1 0 1 2 9 1 1 0 0 0 1
2 0 1 3 0 0 0 0 0 1 1
3 1 1 4 0 5 0 1 1 0 1
4 0 1 1 7 0 0 1 0 0 2
... ... ... ... ... ... ... ... ... ... ...
17227 0 361 4 0 1 1 0 0 1 4307
17228 1 361 1 9 0 0 1 0 0 4308
17229 0 361 2 7 0 0 0 0 0 4308
17230 0 361 3 0 1 0 1 0 1 4308
17231 0 361 4 0 5 1 0 1 0 4308

17232 rows × 10 columns

Fit the model

Note that the parameter panels was included in the fit function in order to take into account panel structure of this dataset during estimation.

[7]:
from xlogit import MixedLogit

varnames = ['pf', 'cl', 'loc', 'wk', 'tod', 'seas']
model = MixedLogit()
model.fit(X=df[varnames],
          y=df['choice'],
          varnames=varnames,
          ids=df['chid'],
          panels=df['id'],
          alts=df['alt'],
          n_draws=600,
          randvars={'pf': 'n', 'cl': 'n', 'loc': 'n',
                    'wk': 'n', 'tod': 'n', 'seas': 'n'})
model.summary()
GPU processing enabled.
Optimization terminated successfully.
    Message: The gradients are close to zero
    Iterations: 25
    Function evaluations: 27
Estimation time= 3.5 seconds
---------------------------------------------------------------------------
Coefficient              Estimate      Std.Err.         z-val         P>|z|
---------------------------------------------------------------------------
pf                     -0.9972102     0.0361699   -27.5701452      7.2e-153 ***
cl                     -0.2196812     0.0143577   -15.3006166      2.44e-50 ***
loc                     2.2901807     0.0891457    25.6903002     3.37e-134 ***
wk                      1.6943247     0.0719012    23.5646304     2.87e-114 ***
tod                    -9.6752279     0.3117174   -31.0384572     1.15e-189 ***
seas                   -9.6961836     0.3111496   -31.1624480     4.94e-191 ***
sd.pf                   0.2207255     0.0126686    17.4230731      1.54e-64 ***
sd.cl                   0.4115547     0.0200678    20.5082053       5.5e-88 ***
sd.loc                  1.7840255     0.0974403    18.3089162      6.13e-71 ***
sd.wk                   1.2296227     0.0838150    14.6706716      1.93e-46 ***
sd.tod                  2.2757059     0.1257114    18.1026193      2.01e-69 ***
sd.seas                 1.4862206     0.1316147    11.2922079      4.06e-28 ***
---------------------------------------------------------------------------
Significance:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Log-Likelihood= -3888.465
AIC= 7800.930
BIC= 7877.349

The xlogit estimates are similar to those estimated using R’s mlogit package (https://cran.r-project.org/web/packages/mlogit/vignettes/e3mxlogit.html). With GPU-enabled estimations, xlogit estimates the model in less than 10 seconds, significantly faster than open-source pacakges such as mlogit and pylogit. This feature can be beneficial while fitting models for large datasets with multiple explanatory variables to be estimated with random coefficients.

Fishing Dataset

This example illustrates the estimation of a Mixed Logit model for choices of 1,182 individuals for sport fishing modes using xlogit. The goal is to analyze the market shares of four alternatives (i.e., beach, pier, boat, and charter) based on their cost and fish catch rate. Cameron (2005) provides additional details about this dataset. The following code illustrates how to use xlogit to estimate the model parameters.

Read data

The data to be analyzed can be imported to Python using any preferred method. In this example, the data in CSV format was imported using the popular pandas Python package. However, it is worth highlighting that xlogit does not depend on the pandas package, as xlogit can take any array-like structure as input. This represents an additional advantage because xlogit can be used with any preferred dataframe library, and not only with pandas.

[8]:
import pandas as pd
df = pd.read_csv("https://raw.github.com/arteagac/xlogit/master/examples/data/fishing_long.csv")
df
[8]:
id alt choice income price catch
0 1 beach 0 7083.33170 157.930 0.0678
1 1 boat 0 7083.33170 157.930 0.2601
2 1 charter 1 7083.33170 182.930 0.5391
3 1 pier 0 7083.33170 157.930 0.0503
4 2 beach 0 1249.99980 15.114 0.1049
... ... ... ... ... ... ...
4723 1181 pier 0 416.66668 36.636 0.4522
4724 1182 beach 0 6250.00130 339.890 0.2537
4725 1182 boat 1 6250.00130 235.436 0.6817
4726 1182 charter 0 6250.00130 260.436 2.3014
4727 1182 pier 0 6250.00130 339.890 0.1498

4728 rows × 6 columns

Fit model

Once the data is in the Python environment, xlogit can be used to fit the model, as shown below. The MultinomialLogit class is imported from xlogit, and its constructor is used to initialize a new model. The fit method estimates the model using the input data and estimation criteria provided as arguments to the method’s call. The arguments of the fit methods are described in `xlogit’s documentation <https://https://xlogit.readthedocs.io/en/latest/api/>`__.

[9]:
from xlogit import MixedLogit
varnames = ['price', 'catch']
model = MixedLogit()
model.fit(X=df[varnames],
          y=df['choice'],
          varnames=varnames,
          alts=df['alt'],
          ids=df['id'],
          n_draws=1000,
          randvars={'price': 'n', 'catch': 'n'})
model.summary()
GPU processing enabled.
Optimization terminated successfully.
    Message: The gradients are close to zero
    Iterations: 19
    Function evaluations: 31
Estimation time= 1.0 seconds
---------------------------------------------------------------------------
Coefficient              Estimate      Std.Err.         z-val         P>|z|
---------------------------------------------------------------------------
price                  -0.0272460     0.0024982   -10.9062938      1.86e-25 ***
catch                   1.3271150     0.1724869     7.6940046      2.23e-13 ***
sd.price                0.0102130     0.0022025     4.6370078      1.87e-05 ***
sd.catch               -1.5706825     0.5732798    -2.7398182        0.0189 *
---------------------------------------------------------------------------
Significance:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Log-Likelihood= -1300.511
AIC= 2609.023
BIC= 2629.323

Prediction

xlogit also provides a convenient set of post-estimation tools for prediction or forecasting. The predict function uses estimated parameters and a new or updated dataset to compute predicted choices. By including the return_proba and return_freq parameters in the function’s call, users can also obtain the predicted probabilities and frequency of the chosen alternatives. The following code illustrates the prediction functionality to forecast changes in market shares (choice frequency) for fishing modes caused by an increase in price for the “boat” mode. First, base market shares are computed by running predict on the original dataset. Then, an increase of 20% in the price for the “boat” alternative is applied to the dataset and the updated shares are predicted.

[10]:
choices, freq = model.predict(X=df[varnames], varnames=varnames, ids=df['id'],
                              alts=df['alt'], return_freq=True)
print(f"base: {freq}")

df.loc[df['alt']=='boat', 'price'] *= 1.2  # 20 percent price increase
choices, freq = model.predict(X=df[varnames], varnames=varnames, ids=df['id'],
                              alts=df['alt'], return_freq=True)
print(f"updated: {freq}")
GPU processing enabled.
base: {'beach': 0.223, 'boat': 0.461, 'charter': 0.228, 'pier': 0.089}
GPU processing enabled.
updated: {'beach': 0.238, 'boat': 0.379, 'charter': 0.278, 'pier': 0.105}

The output shows that the 20% price increase would result in a decrease of almost 10% in market share for the “boat” alternative.

Car Dataset

The fourth example uses a stated preference panel dataset for choice of car. Three alternatives are considered, with upto 6 choice situations per individual. This again is an unbalanced panel with responses of some individuals less than 6 situations. The dataset contains 8 explanaotry variables: price, operating cost, range, and binary indicators to indicate whether the car is electric, hybrid, and if performance is high or medium respectively. This dataset was taken from Kenneth Train’s MATLAB codes for estimation of Mixed Logit models as shown in this link: https://eml.berkeley.edu/Software/abstracts/train1006mxlmsl.html

Read data

[11]:
import pandas as pd
import numpy as np

df = pd.read_csv("https://raw.github.com/arteagac/xlogit/master/examples/data/car100_long.csv")

Since price and operating cost need to be estimated with negative coefficients, we reverse the variable signs in the dataframe.

[12]:
df['price'] = -df['price']/10000
df['opcost'] = -df['opcost']
df
[12]:
person_id choice_id alt choice price opcost range ev gas hybrid hiperf medhiperf
0 1 1 1 0 -4.6763 -47.43 0.0 0 0 1 0 0
1 1 1 2 1 -5.7209 -27.43 1.3 1 0 0 1 1
2 1 1 3 0 -8.7960 -32.41 1.2 1 0 0 0 1
3 1 2 1 1 -3.3768 -4.89 1.3 1 0 0 1 1
4 1 2 2 0 -9.0336 -30.19 0.0 0 0 1 0 1
... ... ... ... ... ... ... ... ... ... ... ... ...
4447 100 1483 2 0 -2.8036 -14.45 1.6 1 0 0 0 0
4448 100 1483 3 0 -1.9360 -54.76 0.0 0 1 0 1 1
4449 100 1484 1 1 -2.4054 -50.57 0.0 0 1 0 0 0
4450 100 1484 2 0 -5.2795 -21.25 0.0 0 0 1 0 1
4451 100 1484 3 0 -6.0705 -25.41 1.4 1 0 0 0 0

4452 rows × 12 columns

Fit the model

[13]:
from xlogit import MixedLogit

varnames = ['hiperf', 'medhiperf', 'price', 'opcost', 'range', 'ev', 'hybrid']
model = MixedLogit()
model.fit(X=df[varnames],
          y=df['choice'],
          varnames=varnames,
          alts=df['alt'],
          ids=df['choice_id'],
          panels=df['person_id'],
          randvars = {'price': 'ln', 'opcost': 'n',
                      'range': 'ln', 'ev':'n', 'hybrid': 'n'},
          n_draws = 100)
model.summary()
GPU processing enabled.
Optimization terminated successfully.
    Message: The gradients are close to zero
    Iterations: 31
    Function evaluations: 34
Estimation time= 1.4 seconds
---------------------------------------------------------------------------
Coefficient              Estimate      Std.Err.         z-val         P>|z|
---------------------------------------------------------------------------
hiperf                  0.1058410     0.0971974     1.0889290         0.441
medhiperf               0.5604997     0.0977352     5.7348796      6.81e-08 ***
price                  -0.7871346     0.1048151    -7.5097475      7.49e-13 ***
opcost                  0.0110846     0.0041762     2.6542027        0.0237 *
range                  -0.6857957     0.4362769    -1.5719276         0.232
ev                     -1.5574339     0.3250817    -4.7908996      8.97e-06 ***
hybrid                  0.6883966     0.1467451     4.6911049      1.43e-05 ***
sd.price                0.8666443     0.0926631     9.3526350      2.72e-19 ***
sd.opcost               0.0401550     0.0047037     8.5369142      2.77e-16 ***
sd.range               -0.5527011     0.2453684    -2.2525353        0.0633 .
sd.ev                   0.9139887     0.2038702     4.4831898      3.66e-05 ***
sd.hybrid               0.7099238     0.1513756     4.6898169      1.44e-05 ***
---------------------------------------------------------------------------
Significance:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Log-Likelihood= -1302.683
AIC= 2629.366
BIC= 2692.995

References

  • Bierlaire, M. (2018). PandasBiogeme: a short introduction. EPFL (Transport and Mobility Laboratory, ENAC).

  • Brathwaite, T., & Walker, J. L. (2018). Asymmetric, closed-form, finite-parameter models of multinomial choice. Journal of Choice Modelling, 29, 78–112.

  • Cameron, A. C., & Trivedi, P. K. (2005). Microeconometrics: methods and applications. Cambridge university press.

  • Croissant, Y. (2020). Estimation of Random Utility Models in R: The mlogit Package. Journal of Statistical Software, 95(1), 1-41.

  • Revelt, D., & Train, K. (1999). Customer-specific taste parameters and mixed logit. University of California, Berkeley.