rmachine-learningdecision-treeparty

partykit::mob() + mlogit: `Error no suitable fitting function specified`


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

Solution

  • I don't have a complete solution but hopefully enough feedback to get you started:

    Illustration based on your example: You can set up a model-fitting function my_fit() that expects y and x to be data frames and then uses the formula = y ~ x1 + x2 and idx = c("id_choice", "altern") to fit the mlogit() model.

    my_fit <- function(y, x = NULL, start = NULL, weights = NULL, offset = NULL, ...) {
      mlogit::mlogit(formula = y ~ x1 + x2, data = cbind(y, x), idx = c("id_choice", "altern"))
    }
    

    To specifify the tree you then need to pass all required variables for the mlogit() model through the x variables of the mob() formula:

    tr <- mob(y ~ x1 + x2 + id_choice + altern | z1, data = df, fit = my_fit,
      control = mob_control(ytype = "data.frame", xtype = "data.frame"))
    

    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 in mob_control() then the partitioning process will fail. The reason for this is that the splitting variable is of length 45 but the gradient/estfun from mlogit() 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 the mlogit() call inside my_fit() accordingly.