glmer vs julia vs INLA

Hablábamos el otro día mi amigo Carlos y yo sobre los modelos mixtos y el uso de lme4, Stan o INLA. Total, que el problema es que queríamos un atajo que permitiera tener una estimación de los efectos aleatorios en un tiempo menor que lo que queda hasta el fin del universo.

Pues nada, investigando vi que existía una librería en Julia llamada MixedModels y que es del autor de lme4 así que me puse a probar a ver si es verdad el lema de Julia, “tan rápido como C, tan fácil como Python”.

Vamos a la prueba.

En primer lugar cargamos nuestros conocidos datos de la Encuesta de Población Activa, que suelo utilizar por ser muy sencilla de entender y por tener un volumen de datos considerable.

## Libreria
library(tidyverse)
## ── Attaching packages ──────────────────── tidyverse 1.2.1 ──
## ✔ ggplot2 3.1.0       ✔ purrr   0.3.1  
## ✔ tibble  2.0.1       ✔ dplyr   0.8.0.1
## ✔ tidyr   0.8.3       ✔ stringr 1.4.0  
## ✔ readr   1.3.1       ✔ forcats 0.4.0
## ── Conflicts ─────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
library(MicroDatosEs)
# install.packages("JuliaCall")
library(JuliaCall)

##  cargar datos

# fpath_dropbox

# enlace_dropbox <- "https://www.dropbox.com/s/h8am8g2yk3dq1y2/md_EPA_2018T4.txt?dl=0"


fpath <- "~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt"

epa <- epa2005(fpath)
## Warning: 607 parsing failures.
##  row     col           expected actual                                            file
## 1641 REPAIRE 1/0/T/F/TRUE/FALSE    115 '~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt'
## 1963 REGEST  1/0/T/F/TRUE/FALSE    115 '~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt'
## 1963 REPAIRE 1/0/T/F/TRUE/FALSE    115 '~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt'
## 2093 REPAIRE 1/0/T/F/TRUE/FALSE    115 '~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt'
## 2168 REGEST  1/0/T/F/TRUE/FALSE    310 '~/Dropbox/Public/datos_4t18/md_EPA_2018T4.txt'
## .... ....... .................. ...... ...............................................
## See problems(...) for more details.
names(epa) <- tolower(names(epa))
epa <- subset(epa, select = c(prov, edad, nforma, aoi))

recodificacion <- function(dat) {
  dat$aoi[grepl("^Inactivos", dat$aoi)] <- "i"
  dat$aoi[grepl("[O-o]cupados", dat$aoi)] <- "o"
  dat$aoi[grepl("^Parados", dat$aoi)] <- "p"
  
  dat$aoi <- factor(dat$aoi)
  dat$nforma3 <- dat$nforma
  dat$nforma3[dat$nforma == "Analfabetos" |
                dat$nforma == "Educación primaria" |
                dat$nforma == "Educación primaria incompleta"] <-
    "Est primarios o menos"
  dat$nforma3[dat$nforma == "Educación superior"] <-
    "Est. Universitarios"
  dat$nforma3[dat$nforma == "Primera etapa de educación secundaria" |
                dat$nforma == "Segunda etapa de educación secundaria, orientación general" |
                dat$nforma == "Segunda etapa de educación secundaria, orientación profesional"] <-
    "Est. Secundarios"
  
  dat$nforma3 <- factor(dat$nforma3)
  
  dat$gedad <- dat$edad
  dat$gedad[dat$edad == "de 0 A 4 años" |
              dat$edad == "de 5 A 9 años" |
              dat$edad == "de 10 A 15 años"] <- "15 años o menos "
  dat$gedad[dat$edad == "de 16 A 19 años" |
              dat$edad == "de 20 A 24 años" |
              dat$edad == "de 25 A 29 años" |
              dat$edad == "de 30 A 34 años"] <- "De 16 a 34"
  
  dat$gedad[dat$edad == "de 35 A 39 años" |
              dat$edad == "de 40 A 44 años" |
              dat$edad == "de 45 A 49 años" |
              dat$edad == "de 50 A 54 años"] <- "De 35 a 54"
  
  dat$gedad[dat$edad == "de 55 A 59 años" |
              dat$edad == "de 60 A 64 años" |
              dat$edad == "65 o más años"] <- "55 o más"
  
  
  dat$gedad <-
    factor(dat$gedad,
           levels = c("15 años o menos ", "De 16 a 34", "De 35 a 54", "55 o más")
    )
  
  dat
}

epa <- recodificacion(epa)

# eliminar menores de 16 años  
epa <- epa[epa$gedad != "15 años o menos ", ]

# eliminar inactivos
epa <- epa[epa$aoi != "i", ]

epa$paro <- ifelse(epa$aoi=="p", 1,0)

No voy a contar lo que es un modelo mixto, ni coger datos de train y test, tan sólo ejecutar modelos simples y ver qué tarda más, y menos.

Modelo con glmer

Uso nAGQ=0 y optimizer = "nloptwrap" porque he leído por ahí que es lo más rápido.

Pego el cacho dónde lo he leído

" the option “nAGQ=0” tells glmer to ignore estimating those integrals. For some models this has minimal impact on parameter estimates, and this NCAA hockey model is one of those. The second option tells glmer to fit using the “nloptwrap” optimizer (there are several other optimizers available, too), which tends to be faster than the default optimization method."

library(tictoc)
library(lme4)
## Loading required package: Matrix
## 
## Attaching package: 'Matrix'
## The following object is masked from 'package:tidyr':
## 
##     expand
tic()
  fit_mixto <- glmer(paro ~ (1 | prov) + (1 | gedad) + (1 | nforma3),
    family = binomial,
    data = epa,
    nAGQ = 0L,
    control = glmerControl(optimizer = "nloptwrap")
  )
toc()
## 10.903 sec elapsed

Summary del modelo

summary(fit_mixto)
## Generalized linear mixed model fit by maximum likelihood (Adaptive
##   Gauss-Hermite Quadrature, nAGQ = 0) [glmerMod]
##  Family: binomial  ( logit )
## Formula: paro ~ (1 | prov) + (1 | gedad) + (1 | nforma3)
##    Data: epa
## Control: glmerControl(optimizer = "nloptwrap")
## 
##      AIC      BIC   logLik deviance df.resid 
##  57620.1  57657.0 -28806.1  57612.1    74564 
## 
## Scaled residuals: 
##     Min      1Q  Median      3Q     Max 
## -1.2062 -0.4286 -0.3446 -0.2478  5.7528 
## 
## Random effects:
##  Groups  Name        Variance Std.Dev.
##  prov    (Intercept) 0.1324   0.3638  
##  gedad   (Intercept) 0.1879   0.4334  
##  nforma3 (Intercept) 0.3759   0.6131  
## Number of obs: 74568, groups:  prov, 52; gedad, 3; nforma3, 3
## 
## Fixed effects:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  -1.6461     0.4367  -3.769 0.000164 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Modelo con MixedModels en julia

Fijándome en este post del creador de la librería y en la docu de la librería, me instalé la librería JuliaCall que permite usar julia dentro de R

library(JuliaCall)
julia_setup()
## Julia version 1.0.3 at location /media/hd2/Descargas/julia/julia-1.0.3/bin will be used.
## Loading setup script for JuliaCall...
## Finish loading setup script for JuliaCall.
julia_library("MixedModels")
julia_assign("form", formula(fit_mixto))
julia_assign("epa", epa)

Ponemos opción fast=true en GeneralizedLinearMixedModels porque la documentación dice que es lo más rápido. Veamos el tiempo que tarda

julia_eval("@elapsed gm1 = fit!(GeneralizedLinearMixedModel(form, epa, Bernoulli()), fast=true)")
## [1] 38.94062

¡Ojo!, utilizando modelos más complejos, glmer pudo hacerlos pero MixedModels en julia, no. La primera vez que julia ejecuta el modelo tarda como unos 40 segundos, porque tiene que compilar no sé qué historia, las sucesivas veces tarda entorno a 9.5 segundos. No está mal, unos 11 segundos de glmer vs 9.5 de MixedModels

Summary del modelo

julia_eval("gm1")
## Julia Object of type GeneralizedLinearMixedModel{Float64}.
## Generalized Linear Mixed Model fit by maximum likelihood (nAGQ = 1)
##   Formula: paro ~ (1 | prov) + (1 | gedad) + (1 | nforma3)
##   Distribution: Bernoulli{Float64}
##   Link: LogitLink()
## 
##   Deviance: 57612.1234
## 
## Variance components:
##              Column    Variance   Std.Dev.  
##  prov    (Intercept)  0.13269447 0.36427252
##  gedad   (Intercept)  0.19182336 0.43797643
##  nforma3 (Intercept)  0.38060583 0.61693260
## 
##  Number of obs: 74568; levels of grouping factors: 52, 4, 3
## 
## Fixed-effects parameters:
##              Estimate Std.Error  z value P(>|z|)
## (Intercept)  -1.64609  0.440015 -3.74097  0.0002

Modelo con INLA

Con INLA se pueden especificar muchas cosas, voy a poner un modelo sencillo, seguro que se puede optimizar un montón, pero no me apetece ahora mismo.

library(INLA)
## Loading required package: sp
## Loading required package: splines
family1 <- "binomial"

formula1 <- paro ~ f(prov, model = "iid") +
  f(gedad, model = "iid") + f(nforma3, model = "iid")

tic()

m_inla <- inla(formula1,
  family = family1,
  data = epa,
  control.compute = list(dic = FALSE, cpo = FALSE),
  control.inla = list(int.strategy = "eb")
)

toc()
## 41.776 sec elapsed

Ojo, que a veces el modelo de INLA da un crash.

Resumen del modelo

summary(m_inla)
## 
## Call:
## c("inla(formula = formula1, family = family1, data = epa, control.compute = list(dic = FALSE, ",  "    cpo = FALSE), control.inla = list(int.strategy = \"eb\"))" )
## 
## Time used:
##  Pre-processing    Running inla Post-processing           Total 
##          1.2621         40.1902          0.3095         41.7618 
## 
## Fixed effects:
##                mean     sd 0.025quant 0.5quant 0.975quant    mode kld
## (Intercept) -1.6466 0.3516    -2.3369  -1.6466    -0.9569 -1.6466   0
## 
## Random effects:
## Name   Model
##  prov   IID model 
## gedad   IID model 
## nforma3   IID model 
## 
## Model hyperparameters:
##                       mean    sd      0.025quant 0.5quant 0.975quant
## Precision for prov     8.0160  1.7225  5.1678     7.8371  11.8852   
## Precision for gedad   11.2787  9.1258  2.1820     8.7681  35.2338   
## Precision for nforma3  5.2391  4.3753  0.9665     4.0213  16.7312   
##                       mode   
## Precision for prov     7.4915
## Precision for gedad    5.3011
## Precision for nforma3  2.3706
## 
## Expected number of effective parameters(std dev): 52.89(0.00)
## Number of equivalent replicates : 1409.83 
## 
## Marginal Likelihood:  -28830.08

Pues al menos con este conjunto de datos y este modelo me quedaría con glmer en vez de la implementación con julia o INLA

 
comments powered by Disqus