Diff of /R/postestimation.R [000000] .. [ede2d4]

Switch to unified view

a b/R/postestimation.R
1
##=============================================================================
2
##
3
## Copyright (c) 2019 Marco Colombo
4
##
5
## Parts of the code are based on https://avehtari.github.io/bayes_R2/bayes_R2.html
6
## Portions copyright (c) 2019 Aki Vehtari, Andrew Gelman, Ben Goodrich, Jonah Gabry
7
##
8
## Parts of the code are based on https://github.com/stan-dev/rstanarm
9
## Portions copyright (c) 2015, 2016, 2017 Trustees of Columbia University
10
##
11
## This program is free software: you can redistribute it and/or modify
12
## it under the terms of the GNU General Public License as published by
13
## the Free Software Foundation, either version 3 of the License, or
14
## (at your option) any later version.
15
##
16
## This program is distributed in the hope that it will be useful,
17
## but WITHOUT ANY WARRANTY; without even the implied warranty of
18
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
## GNU General Public License for more details.
20
##
21
## You should have received a copy of the GNU General Public License
22
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
##
24
##=============================================================================
25
26
27
#' Pointwise log-likelihood
28
#'
29
#' Compute the pointwise log-likelihood.
30
#'
31
#' @param object An object of class `hsstan`.
32
#' @param newdata Optional data frame to use to evaluate the log-likelihood.
33
#'        If `NULL` (default), the model matrix is used.
34
#' @param ... Currently ignored.
35
#'
36
#' @return
37
#' A matrix of size `S` by `N`, where `S` is number of draws from the posterior
38
#' distribution, and `N` is the number of data points.
39
#'
40
#' @examples
41
#' \dontshow{utils::example("hsstan", echo=FALSE)}
42
#' # continued from ?hsstan
43
#' log_lik(hs.biom)
44
#'
45
#' @importFrom rstantools log_lik
46
#' @importFrom stats rbinom rnorm
47
#' @method log_lik hsstan
48
#' @aliases log_lik
49
#' @export log_lik
50
#' @export
51
log_lik.hsstan <- function(object, newdata=NULL, ...) {
52
    if (is.null(newdata))
53
        newdata <- object$data
54
    y <- validate.outcome(newdata[[object$model.terms$outcome]])
55
    mu <- posterior_linpred(object, newdata, transform=TRUE)
56
    if (!is.logistic(object))
57
        sigma <- as.matrix(object$stanfit, pars="sigma")
58
    llkfun <- ifelse(is.logistic(object),
59
                     function(x) stats::dbinom(y[x], 1, mu[, x], log=TRUE),
60
                     function(x) stats::dnorm(y[x], mu[, x], sigma, log=TRUE))
61
    llk <- cbind(sapply(1:ncol(mu), llkfun))
62
    return(llk)
63
}
64
65
#' Posterior uncertainty intervals
66
#'
67
#' Compute posterior uncertainty intervals for `hsstan` objects.
68
#'
69
#' @param object An object of class `hsstan`.
70
#' @param pars Names of parameters for which posterior intervals should be
71
#'        returned, which can be specified as regular expressions. If `NULL`
72
#'        (default) then this refers to the set of predictors used in the model.
73
#' @param prob A value between 0 and 1 indicating the desired probability
74
#'        to be covered by the uncertainty intervals (0.95, by default).
75
#' @param ... Currently ignored.
76
#'
77
#' @return
78
#' A matrix with lower and upper interval bounds as columns and as many rows
79
#' as selected parameters.
80
#'
81
#' @examples
82
#' \dontshow{utils::example("hsstan", echo=FALSE)}
83
#' # continued from ?hsstan
84
#' posterior_interval(hs.biom)
85
#'
86
#' @importFrom rstantools posterior_interval
87
#' @method posterior_interval hsstan
88
#' @aliases posterior_interval
89
#' @export posterior_interval
90
#' @export
91
posterior_interval.hsstan <- function(object, pars=NULL, prob=0.95, ...) {
92
    validate.samples(object)
93
    validate.probability(prob)
94
    pars <- get.pars(object, pars)
95
    post.matrix <- as.matrix(object$stanfit, pars=pars)
96
    rstantools::posterior_interval(post.matrix, prob=prob)
97
}
98
99
#' Posterior distribution of the linear predictor
100
#'
101
#' Extract the posterior draws of the linear predictor, possibly transformed
102
#' by the inverse-link function.
103
#'
104
#' @param object An object of class `hsstan`.
105
#' @param transform Whether the linear predictor should be transformed using
106
#'        the inverse-link function (`FALSE` by default).
107
#' @param newdata Optional data frame containing the variables to use to
108
#'        predict. If `NULL` (default), the model matrix is used. If
109
#'        specified, its continuous variables should be standardized, since
110
#'        the model coefficients are learnt on standardized data.
111
#' @param ... Currently ignored.
112
#'
113
#' @return
114
#' A matrix of size `S` by `N`, where `S` is the number of draws from the
115
#' posterior distribution of the (transformed) linear predictor, and `N` is
116
#' the number of data points.
117
#'
118
#' @examples
119
#' \dontshow{utils::example("hsstan", echo=FALSE)}
120
#' # continued from ?hsstan
121
#' posterior_linpred(hs.biom)
122
#'
123
#' @importFrom rstantools posterior_linpred
124
#' @method posterior_linpred hsstan
125
#' @aliases posterior_linpred
126
#' @export posterior_linpred
127
#' @export
128
posterior_linpred.hsstan <- function(object, transform=FALSE,
129
                                     newdata=NULL, ...) {
130
    validate.samples(object)
131
    newdata <- validate.newdata(object, newdata)
132
    pars <- grep("^beta_", object$stanfit@model_pars, value=TRUE)
133
    post.matrix <- as.matrix(object$stanfit, pars=pars)
134
    linear.predictor <- multiplyABt(post.matrix, newdata)
135
    if (!transform)
136
        return(linear.predictor)
137
    return(object$family$linkinv(linear.predictor))
138
}
139
140
#' Posterior predictive distribution
141
#'
142
#' Draw from the posterior predictive distribution of the outcome.
143
#'
144
#' @param object An object of class `hsstan`.
145
#' @param newdata Optional data frame containing the variables to use to
146
#'        predict. If `NULL` (default), the model matrix is used. If
147
#'        specified, its continuous variables should be standardized, since
148
#'        the model coefficients are learnt on standardized data.
149
#' @param nsamples A positive integer indicating the number of posterior samples
150
#'        to use. If `NULL` (default) all samples are used.
151
#' @param seed Optional integer defining the seed for the pseudo-random number
152
#'        generator.
153
#' @param ... Currently ignored.
154
#'
155
#' @return
156
#' A matrix of size `S` by `N`, where `S` is the number of simulations from
157
#' the posterior predictive distribution, and `N` is the number of data points.
158
#'
159
#' @examples
160
#' \dontshow{utils::example("hsstan", echo=FALSE)}
161
#' # continued from ?hsstan
162
#' posterior_predict(hs.biom)
163
#'
164
#' @importFrom rstantools posterior_predict
165
#' @importFrom stats rbinom rnorm
166
#' @method posterior_predict hsstan
167
#' @aliases posterior_predict
168
#' @export posterior_predict
169
#' @export
170
posterior_predict.hsstan <- function(object, newdata=NULL, nsamples=NULL,
171
                                     seed=NULL, ...) {
172
    validate.samples(object)
173
    if (!is.null(nsamples))
174
        validate.positive.scalar(nsamples, "nsamples", int=TRUE)
175
176
    ## extract a random subset of the posterior samples
177
    if (!is.null(seed))
178
        set.seed(seed)
179
    num.samples <- nsamples(object)
180
    samp <- sample(num.samples, min(num.samples, nsamples))
181
    nsamples <- length(samp)
182
183
    ## generate the posterior predictions
184
    mu <- posterior_linpred(object, newdata, transform=TRUE)[samp, , drop=FALSE]
185
    nobs <- ncol(mu)
186
    if (is.logistic(object))
187
        pp <- t(sapply(1:nsamples, function(z) rbinom(nobs, 1, mu[z, ])))
188
    else {
189
        sigma <- as.matrix(object$stanfit, pars="sigma")[samp, , drop=FALSE]
190
        pp <- t(sapply(1:nsamples, function(z) rnorm(nobs, mu[z, ], sigma[z])))
191
    }
192
    return(pp)
193
}
194
195
#' Posterior measures of performance
196
#'
197
#' Compute the log-likelihood and a relevant measure of performance (R-squared
198
#' or AUC) from the posterior samples.
199
#'
200
#' @param obj An object of class `hsstan` or `kfold`.
201
#' @param prob Width of the posterior interval (0.95, by default). It is
202
#'        ignored if `summary=FALSE`.
203
#' @param sub.idx Vector of indices of observations in the dataset to be used
204
#'        in computing the performance measures. If `NULL` (default), all
205
#'        observations in the dataset are used.
206
#' @param summary Whether a summary of the distribution of the performance
207
#'        measure should be returned rather than the pointwise values
208
#'        (`TRUE` by default).
209
#' @param cores Number of cores to use for parallelization (the value of
210
#'        `options("mc.cores")` by default).
211
#'
212
#' @return
213
#' The mean, standard deviation and posterior interval of the performance
214
#' measure (R-squared or AUC) if `summary=TRUE`, or a vector of values
215
#' of the performance measure with length equal to the size of the posterior
216
#' sample if `summary=FALSE`. Attribute `type` reports whether the performance
217
#' measures are cross-validated or not. If `sub.idx` is not `NULL`, attribute
218
#' `subset` reports the index of observations used in the computations.
219
#'
220
#' @examples
221
#' \donttest{
222
#' \dontshow{utils::example("hsstan", echo=FALSE)}
223
#' # continued from ?hsstan
224
#' posterior_performance(hs.biom, cores=1)
225
#' }
226
#'
227
#' @export
228
posterior_performance <- function(obj, prob=0.95, sub.idx=NULL, summary=TRUE,
229
                                  cores=getOption("mc.cores", 1)) {
230
    if (inherits(obj, "hsstan")) {
231
        obj <- list(fits=array(list(fit=obj, test.idx=1:nrow(obj$data)), c(1, 2)),
232
                    data=obj$data)
233
        colnames(obj$fits) <- c("fit", "test.idx")
234
    } else if (inherits(obj, c("kfold", "loo"))) {
235
        if (is.null(obj[["fits"]]))
236
            stop("No fitted models found, run 'kfold' with store.fits=TRUE.")
237
    } else
238
        stop("Not an 'hsstan' or 'kfold' object.")
239
240
    if (is.null(sub.idx)) {
241
        sub.idx <- 1:nrow(obj$data)
242
        used.subset <- FALSE
243
    } else {
244
        validate.indices(sub.idx, nrow(obj$data), "sub.idx")
245
        sub.idx <- sort(sub.idx)
246
        used.subset <- length(sub.idx) < nrow(obj$data)
247
    }
248
249
    validate.samples(obj$fits[[1]])
250
    validate.probability(prob)
251
    logistic <- is.logistic(obj$fits[[1]])
252
    num.folds <- nrow(obj$fits)
253
254
    ## loop over the folds
255
    y <- mu <- llk <- NULL
256
    for (fold in 1:num.folds) {
257
        hs <- obj$fits[[fold]]
258
        test.idx <- intersect(obj$fits[, "test.idx"][[fold]], sub.idx)
259
        if (length(test.idx) == 0)
260
            next
261
        newdata <- obj$data[test.idx, ]
262
        y <- c(y, obj$data[test.idx, hs$model.terms$outcome])
263
        mu <- cbind(mu, posterior_linpred(hs, newdata=newdata, transform=TRUE))
264
        llk <- cbind(llk, log_lik(hs, newdata=newdata))
265
    }
266
267
    if (used.subset && logistic && length(unique(y)) != 2)
268
        stop("'sub.idx' must contain both outcome classes.")
269
270
    if (logistic) {
271
        par.auc <- function(i)
272
            as.numeric(pROC::roc(y, mu[i, ], direction="<", quiet=TRUE)$auc)
273
        if (.Platform$OS.type != "windows") {
274
            out <- parallel::mclapply(X=1:nrow(mu), mc.cores=cores,
275
                                      mc.preschedule=TRUE, FUN=par.auc)
276
        } else { # windows
277
            cl <- parallel::makePSOCKcluster(cores)
278
            on.exit(parallel::stopCluster(cl))
279
            out <- parallel::parLapply(X=1:nrow(mu), cl=cl, fun=par.auc)
280
        }
281
    } else {
282
        out <- pmax(fastCor(y, mu), 0)^2
283
    }
284
285
    out <- cbind(perf=unlist(out), llk=rowSums(llk))
286
    colnames(out)[1] <- ifelse(logistic, "auc", "r2")
287
    if (summary)
288
        out <- posterior_summary(out, prob)
289
    attr(out, "type") <- paste0(if(num.folds == 1) "non ", "cross-validated")
290
    if (used.subset)
291
        attr(out, "subset") <- sub.idx
292
    return(out)
293
}
294
295
#' Predictive information criteria for Bayesian models
296
#'
297
#' Compute an efficient approximate leave-one-out cross-validation
298
#' using Pareto smoothed importance sampling (PSIS-LOO), or the widely
299
#' applicable information criterion (WAIC), also known as the Watanabe-Akaike
300
#' information criterion.
301
#'
302
#' @param x An object of class `hsstan`.
303
#' @param cores Number of cores used for parallelisation (the value of
304
#'        `options("mc.cores")` by default).
305
#' @param ... Currently ignored.
306
#'
307
#' @return
308
#' A `loo` object.
309
#'
310
#' @examples
311
#' \dontshow{utils::example("hsstan", echo=FALSE)}
312
#' \dontshow{oldopts <- options(mc.cores=2)}
313
#' # continued from ?hsstan
314
#' loo(hs.biom)
315
#' waic(hs.biom)
316
#' \dontshow{options(oldopts)}
317
#'
318
#' @importFrom loo loo
319
#' @method loo hsstan
320
#' @aliases loo
321
#' @export loo
322
#' @export
323
loo.hsstan <- function(x, cores=getOption("mc.cores"), ...) {
324
    validate.samples(x)
325
    llk <- log_lik(x)
326
    chain.id <- rep(1:ncol(x$stanfit), each=nrow(x$stanfit))
327
    r.eff <- loo::relative_eff(exp(llk), chain_id=chain.id, cores=cores)
328
    loo <- suppressWarnings(loo::loo(llk, r_eff=r.eff, cores=cores))
329
    return(loo)
330
}
331
332
#' @rdname loo.hsstan
333
#' @importFrom loo waic
334
#' @method waic hsstan
335
#' @aliases waic
336
#' @export waic
337
#' @export
338
waic.hsstan <- function(x, cores=getOption("mc.cores"), ...) {
339
    validate.samples(x)
340
    llk <- log_lik(x)
341
    waic <- suppressWarnings(loo::waic(llk, cores=cores))
342
    return(waic)
343
}
344
345
#' Bayesian and LOO-adjusted R-squared
346
#'
347
#' Compute the Bayesian and the LOO-adjusted R-squared from the posterior
348
#' samples. For Bayesian R-squared it uses the modelled residual variance
349
#' (rather than the variance of the posterior distribution of the residuals).
350
#' The LOO-adjusted R-squared uses Pareto smoothed importance sampling LOO
351
#' residuals and Bayesian bootstrap.
352
#'
353
#' @param object An object of class `hsstan`.
354
#' @param prob Width of the posterior interval (0.95, by default). It is
355
#'        ignored if `summary=FALSE`.
356
#' @param summary Whether a summary of the distribution of the R-squared
357
#'        should be returned rather than the pointwise values (`TRUE` by
358
#'        default).
359
#' @param ... Currently ignored.
360
#'
361
#' @return
362
#' The mean, standard deviation and posterior interval of R-squared if
363
#' `summary=TRUE`, or a vector of R-squared values with length equal to
364
#' the size of the posterior sample if `summary=FALSE`.
365
#'
366
#' @references
367
#' Andrew Gelman, Ben Goodrich, Jonah Gabry and Aki Vehtari (2019),
368
#' R-squared for Bayesian regression models,
369
#' _The American Statistician_, 73 (3), 307-309.
370
#' \doi{10.1080/00031305.2018.1549100}
371
#'
372
#' Aki Vehtari, Andrew Gelman, Ben Goodrich and Jonah Gabry (2019),
373
#' Bayesian R2 and LOO-R2.
374
#' \url{https://avehtari.github.io/bayes_R2/bayes_R2.html}
375
#'
376
#' @examples
377
#' \dontshow{utils::example("hsstan", echo=FALSE)}
378
#' \dontshow{oldopts <- options(mc.cores=2)}
379
#' # continued from ?hsstan
380
#' bayes_R2(hs.biom)
381
#' loo_R2(hs.biom)
382
#' \dontshow{options(oldopts)}
383
#'
384
#' @importFrom rstantools bayes_R2
385
#' @method bayes_R2 hsstan
386
#' @aliases bayes_R2
387
#' @export bayes_R2
388
#' @export
389
bayes_R2.hsstan <- function(object, prob=0.95, summary=TRUE, ...) {
390
    validate.samples(object)
391
    validate.probability(prob)
392
    mu <- posterior_linpred(object, transform=TRUE)
393
    var.mu <- apply(mu, 1, stats::var)
394
    if (is.logistic(object))
395
        sigma2 <- rowMeans(mu * (1 - mu))
396
    else
397
        sigma2 <- drop(as.matrix(object$stanfit, pars="sigma"))^2
398
    R2 <- var.mu / (var.mu + sigma2)
399
    if (summary)
400
        R2 <- vector.summary(R2, prob)
401
    return(R2)
402
}
403
404
#' @rdname bayes_R2.hsstan
405
#' @importFrom rstantools loo_R2
406
#' @method loo_R2 hsstan
407
#' @aliases loo_R2
408
#' @export loo_R2
409
#' @export
410
loo_R2.hsstan <- function(object, prob=0.95, summary=TRUE, ...) {
411
    validate.samples(object)
412
    validate.probability(prob)
413
    y <- object$data[[object$model.terms$outcome]]
414
    mu <- posterior_linpred(object, transform=TRUE)
415
    ll <- log_lik(object)
416
    S <- nrow(mu)
417
    N <- ncol(mu)
418
    chains <- object$stanfit@sim$chains
419
    r.eff <- loo::relative_eff(exp(ll), chain_id=rep(1:chains, each=S / chains))
420
    psis <- suppressWarnings(loo::psis(-ll, r_eff=r.eff))
421
    mu.loo <- loo::E_loo(mu, psis, log_ratios=-ll)$value
422
    err.loo <- mu.loo - y
423
424
    ## set the random seed as the seed used in the first chain and ensure
425
    ## the old RNG state is restored on exit
426
    rng.state.old <- .Random.seed
427
    on.exit(assign(".Random.seed", rng.state.old, envir=.GlobalEnv))
428
    set.seed(object$stanfit@stan_args[[1]]$seed)
429
430
    ## dirichlet weights for bayesian bootstrap
431
    exp.draws <- matrix(stats::rexp(S * N, rate=1), nrow=S, ncol=N)
432
    dw <- exp.draws / rowSums(exp.draws)
433
434
    var.y <- (rowSums(sweep(dw, 2, y^2, FUN = "*")) -
435
              rowSums(sweep(dw, 2, y, FUN = "*"))^2) * (N / (N - 1))
436
    var.err.loo <- (rowSums(sweep(dw, 2, err.loo^2, FUN = "*")) -
437
                    rowSums(sweep(dw, 2, err.loo, FUN = "*")^2)) * (N / (N - 1))
438
439
    R2 <- 1 - var.err.loo / var.y
440
    R2[R2 < -1] <- -1
441
    R2[R2 > 1] <- 1
442
443
    if (summary)
444
        R2 <- vector.summary(R2, prob)
445
    return(R2)
446
}