I am trying to fit a conditional logit using mlogit::mlogit()
at the end leaves of the tree generated by the MOB algorithm partykit::mob()
. Apparently, it cannot be made directly using the partykit::mob()
function (below my attempts). However, I found the LORET algorithm, but I couldn't find any documentation with examples, so I tried guessing which function I need from the source code, but unfortunately, I couldn't make it work.
Do you know how (1) where I could find examples for the LORET library and (2) if it is possible to use the partykit:mob()
function to work together with mlogit::mlogit
? Thanks in advance.
Example data
For illustration, please gently consider the following data. It represents data from 5 individuals (id_ind
) that choose among 3 alternatives (altern
). Each of the five individuals chose three times; hence we have 15 choice situations (id_choice
). Each alternative is represented by two generic attributes (x1
and x2
), and the choices are registered in y
(1
if selected, 0
otherwise). Finally, z1
is a candidate partition variable.
df <- read.table(header = TRUE, text = "
id_ind id_choice altern x1 x2 y
1 1 1 1 1.586788801 0.11887832 1
2 1 1 2 -0.937965347 1.15742493 0
3 1 1 3 -0.511504401 -1.90667519 0
4 1 2 1 1.079365680 -0.37267925 0
5 1 2 2 -0.009203032 1.65150370 1
6 1 2 3 0.870474033 -0.82558651 0
7 1 3 1 -0.638604013 -0.09459502 0
8 1 3 2 -0.071679538 1.56879334 0
9 1 3 3 0.398263302 1.45735788 1
10 2 4 1 0.291413453 -0.09107974 0
11 2 4 2 1.632831160 0.92925495 0
12 2 4 3 -1.193272276 0.77092623 1
13 2 5 1 1.967624379 -0.16373709 1
14 2 5 2 -0.479859282 -0.67042130 0
15 2 5 3 1.109780885 0.60348187 0
16 2 6 1 -0.025834772 -0.44004183 0
17 2 6 2 -1.255129594 1.10928280 0
18 2 6 3 1.309493274 1.84247199 1
19 3 7 1 1.593558740 -0.08952151 0
20 3 7 2 1.778701074 1.44483791 1
21 3 7 3 0.643191170 -0.24761157 0
22 3 8 1 1.738820924 -0.96793288 0
23 3 8 2 -1.151429915 -0.08581901 0
24 3 8 3 0.606695064 1.06524268 1
25 3 9 1 0.673866953 -0.26136206 0
26 3 9 2 1.176959443 0.85005871 1
27 3 9 3 -1.568225496 -0.40002252 0
28 4 10 1 0.516456176 -1.02081089 1
29 4 10 2 -1.752854918 -1.71728381 0
30 4 10 3 -1.176101700 -1.60213536 0
31 4 11 1 -1.497779616 -1.66301234 0
32 4 11 2 -0.931117325 1.50128532 1
33 4 11 3 -0.455543630 -0.64370825 0
34 4 12 1 0.894843784 -0.69859139 0
35 4 12 2 -0.354902281 1.02834859 0
36 4 12 3 1.283785176 -1.18923098 1
37 5 13 1 -1.293772990 -0.73491317 0
38 5 13 2 0.748091387 0.07453705 1
39 5 13 3 -0.463585127 0.64802031 0
40 5 14 1 -1.946438667 1.35776140 0
41 5 14 2 -0.470448172 -0.61326604 1
42 5 14 3 1.478763383 -0.66490028 0
43 5 15 1 0.588240775 0.84448489 1
44 5 15 2 1.131731049 -1.51323232 0
45 5 15 3 0.212145247 -1.01804594 0
")
df$z1 <- rnorm(n= nrow(df),mean = 0,sd = 1)
mlogit
+ partykit::mob()
library(mlogit)
library(partykit)
mo <- mlogit(formula = y ~ x1 + x2 ,
data = df,
idx = c("id_choice", "altern"))
# Coefficients:
# (Intercept):2 (Intercept):3 x1 x2
# 0.036497 0.293254 0.821173 1.062794
mlogit_function <- function(y, x,
offset = NULL,
...){ mlogit(y ~ x ,
data = df)}
formula <- y ~ x1 + x2 | z1
mob(formula = formula,
data = df,
fit = mlogit_function,
control = mob_control(minsize = 10,
alpha = 0.01))
# Error in mob(formula = formula, data = df, fit = mlogit_function, control = mob_control(minsize = 10,
# no suitable fitting function specified
mlogit
+ loret::multinomtree()
This function runs the tree, but it is not what I want because there is a missing constant for alternative 2.
loret::multinomtree(formula = formula,
data = df)
# Model-based recursive partitioning (NULL)
# Model formula:
# y ~ x1 + x2 | z1
#
# Fitted party:
# [1] root: n = 45
# 1:(Intercept) 1:x1 1:x2
# -1.1046317 0.7663315 1.0418296
#
# Number of inner nodes: 0
# Number of terminal nodes: 1
# Number of parameters per node: 3
# Objective function: 22.62393
mlogit
+ loret::clmtree()
loret::clmtree(formula = formula,
data = df)
# Error in clm.fit.default(y = c(1L, 0L, 0L, 0L, 1L, 0L, 0L, 0L, 1L, 0L, :
# is.list(y) is not TRUE
I don't have a complete solution but hopefully enough feedback to get you started:
It is important to distinguish so-called "conditional logit" models with alternative-specific variables (which you are interested in) and classic "multinomial logit models" with only subject-specific (or individual-specific) variables.
mlogit::mlogit()
can fit both (and also mixed versions) whilennet::multinom()
only supports the latter.For fitting conditional logit models you could have the data in long vs. wide form. You specify your data in long form and also have a splitting variable
z1
that is alternative-specific. This means that data from the same individual could end up in different nodes of the tree which would be rather awkward.Instead it would be better to have the data in wide form so that each row corresponds to an individual and then you could only consider individual-specific variables for splitting. This would also match the view of the
$gradient
element of a fittedmlogit
object which provides the individual-specific gradient contributions. (This is whatsandwich::estfun()
extracts which in turns is the essential information forpartykit::mob()
.)It might also be possible to do sensible recursive partitioning based on alternative-specific variables but I find it hard to see what kind of models this would yield and what these would mean. In any case, you then would have to write your own code to extract the
estfun
from the fitted-model object that provides the fully disaggregated alternative-specific gradient contributions.The
loret
package is somewhat unfinished and hasn't been updated in quite a while. Hence, I would not recommend using it "in production" at the moment. Alsonnet::multinom()
(underlyingloret::multinomtree()
) does not fit the model you need (as mentioned above) andordinal::clm()
(underlyingloret::clmtree()
) is for a completely different model.One specific aspect that we wanted to build into
loret
but did not finish yet, is automatic detection of (quasi-)complete separation in logistic models and handling it appropriately in the tree.Your
mlogit
+partykit::mob()
approach does not work because the fitting function does not have the right interface (as you are correctly informed). Seevignette("mob", package = "partykit")
for the two supported interfaces.To write an appropriate interface you need to make sure that you have all the variables you need in each subset. Note that the response variable plus regressor matrix is not enough for this but you need the index variables as well! I would recommend to include these variables either through the
y
variables or thex
variables of the formula specified forpartykit::mob()
. Inmob_control()
you can then setytype = "data.frame"
andxtype = "data.frame"
. Then bothy
andx
are provided asdata.frame
objects and can be combined again prior to callingmlogit::mlogit()
. Theformula
andidx
arguments formlogit()
have to be provided in some way then. In the example below I have hard-coded them.Illustration based on your example: You can set up a model-fitting function
my_fit()
that expectsy
andx
to be data frames and then uses theformula = y ~ x1 + x2
andidx = c("id_choice", "altern")
to fit themlogit()
model.To specifify the tree you then need to pass all required variables for the
mlogit()
model through thex
variables of themob()
formula:Superficially, this works. At least it fits the desired model in the root node as you can see by checking
coef(tree)
. However, the only reason it works is that it does not even try to do any partitioning as the sample size is judged to be too small compared to the number of parameters.But when you additionally set
minsize = 10
inmob_control()
then the partitioning process will fail. The reason for this is that the splitting variable is of length 45 but the gradient/estfun frommlogit()
is only of length 15. This is the long alternative-specific format vs. the short individual-specific format.Thus, to make things work you have to use the data in wide form and then adapt the
mob()
formula and themlogit()
call insidemy_fit()
accordingly.