Skip to content

Commit

Permalink
add performance_score()
Browse files Browse the repository at this point in the history
  • Loading branch information
strengejacke committed May 22, 2019
1 parent dc0ba47 commit 0516124
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 11 deletions.
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ S3method(print,performance_accuracy)
S3method(print,performance_hosmer)
S3method(print,performance_pcp)
S3method(print,performance_roc)
S3method(print,performance_score)
S3method(print,r2_bayes)
S3method(print,r2_generic)
S3method(print,r2_nakagawa)
Expand Down Expand Up @@ -155,6 +156,7 @@ export(performance_pcp)
export(performance_rmse)
export(performance_roc)
export(performance_rse)
export(performance_score)
export(principal_components)
export(r2)
export(r2_bayes)
Expand Down Expand Up @@ -195,7 +197,9 @@ importFrom(stats,coef)
importFrom(stats,complete.cases)
importFrom(stats,cor)
importFrom(stats,cov2cor)
importFrom(stats,dbinom)
importFrom(stats,df.residual)
importFrom(stats,dnbinom)
importFrom(stats,dpois)
importFrom(stats,family)
importFrom(stats,fitted)
Expand All @@ -211,6 +215,8 @@ importFrom(stats,na.omit)
importFrom(stats,nobs)
importFrom(stats,pchisq)
importFrom(stats,pf)
importFrom(stats,pnbinom)
importFrom(stats,ppois)
importFrom(stats,prcomp)
importFrom(stats,predict)
importFrom(stats,predict.glm)
Expand Down
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

* `r2()` now works for more regression models.
* `r2_bayes()` now works for multivariate response models.
* `model_performance()` now works for more regression models, and also includes the log-loss and percentage of correct predictions as new metric for models with binary outcome.
* `model_performance()` now works for more regression models, and also includes the log-loss, proper scoring rules and percentage of correct predictions as new metric for models with binary outcome.

## New performance-functions

* `performance_accuracy()`, which calculates the predictive accuracy of linear or logistic regression models.
* `performance_logloss()` to compute the log-loss of models with binary outcome. The log-loss is a proper scoring function comparable to the `rmse()`.
* `performance_score()` to compute the logarithmic, quadratic and spherical proper scoring rules.
* `performance_pcp()` to calculate the percentage of correct predictions for models with binary outcome.
* `performance_roc()`, to calculate ROC-curves.

Expand Down
12 changes: 10 additions & 2 deletions R/model_performance.bayesian.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
#' Compute indices of model performance for (general) linear models.
#'
#' @param model Object of class \code{stanreg} or \code{brmsfit}.
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("LOOIC", "WAIC", "R2", "R2_adj", "RMSE", "LOGLOSS")}).
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("LOOIC", "WAIC", "R2", "R2_adj", "RMSE", "LOGLOSS", "SCORE")}).
#' @param ... Arguments passed to or from other methods.
#'
#' @return A data frame (with one row) and one column per "index" (see \code{metrics}).
#'
#' @details See 'Details' in \code{\link{model_performance.lm}} for more
#' details on returned indices.
#'
#' @examples
#' library(rstanarm)
#'
Expand All @@ -31,7 +34,7 @@
#' @export
model_performance.stanreg <- function(model, metrics = "all", ...) {
if (all(metrics == "all")) {
metrics <- c("LOOIC", "WAIC", "R2", "R2_adjusted", "RMSE", "LOGLOSS")
metrics <- c("LOOIC", "WAIC", "R2", "R2_adjusted", "RMSE", "LOGLOSS", "SCORE")
}

algorithm <- insight::find_algorithm(model)
Expand Down Expand Up @@ -71,6 +74,11 @@ model_performance.stanreg <- function(model, metrics = "all", ...) {
if (("LOGLOSS" %in% metrics) && mi$is_binomial) {
out$LOGLOSS <- performance_logloss(model)
}
if (("SCORE" %in% metrics) && (mi$is_binomial || mi$is_count)) {
.scoring_rules <- performance_score(model)
if (!is.na(.scoring_rules$logarithmic)) out$SCORE_LOG <- .scoring_rules$logarithmic
if (!is.na(.scoring_rules$spherical)) out$SCORE_SPHERICAL <- .scoring_rules$spherical
}

# TODO: What with sigma and deviance?

Expand Down
11 changes: 9 additions & 2 deletions R/model_performance.lm.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#' Compute indices of model performance for regression models.
#'
#' @param model A model.
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("AIC", "BIC", "R2", "RMSE", "LOGLOSS", "PCP")}).
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("AIC", "BIC", "R2", "RMSE", "LOGLOSS", "PCP", "SCORE")}).
#' @param ... Arguments passed to or from other methods.
#'
#' @return A data frame (with one row) and one column per "index" (see \code{metrics}).
Expand All @@ -16,6 +16,8 @@
#' \item{\strong{R2_adj}} {adjusted r-squared, see \code{\link{r2}}}
#' \item{\strong{RMSE}} {root mean squared error, see \code{\link{performance_rmse}}}
#' \item{\strong{LOGLOSS}} {Log-loss, see \code{\link{performance_logloss}}}
#' \item{\strong{SCORE_LOG}} {score of logarithmic proper scoring rule, see \code{\link{performance_score}}}
#' \item{\strong{SCORE_SPHERICAL}} {score of spherical proper scoring rule, see \code{\link{performance_score}}}
#' \item{\strong{PCP}} {percentage of correct predictions, see \code{\link{performance_pcp}}}
#' }
#'
Expand All @@ -31,7 +33,7 @@
#' @export
model_performance.lm <- function(model, metrics = "all", ...) {
if (all(metrics == "all")) {
metrics <- c("AIC", "BIC", "R2", "R2_adj", "RMSE", "LOGLOSS", "PCP")
metrics <- c("AIC", "BIC", "R2", "R2_adj", "RMSE", "LOGLOSS", "PCP", "SCORE")
}

mi <- insight::model_info(model)
Expand All @@ -53,6 +55,11 @@ model_performance.lm <- function(model, metrics = "all", ...) {
.logloss <- performance_logloss(model)
if (!is.na(.logloss)) out$LOGLOSS <- .logloss
}
if (("SCORE" %in% metrics) && (mi$is_binomial || mi$is_count)) {
.scoring_rules <- performance_score(model)
if (!is.na(.scoring_rules$logarithmic)) out$SCORE_LOG <- .scoring_rules$logarithmic
if (!is.na(.scoring_rules$spherical)) out$SCORE_SPHERICAL <- .scoring_rules$spherical
}

if (("PCP" %in% metrics) && mi$is_binomial && !mi$is_ordinal) {
out$PCP <- performance_pcp(model)$pcp_model
Expand Down
12 changes: 10 additions & 2 deletions R/model_performance.mixed.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
#' Compute indices of model performance for mixed models.
#'
#' @param model Object of class \code{merMod}, \code{glmmTMB}, \code{lme} or \code{MixMod}.
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("AIC", "BIC", "R2", "ICC", "RMSE", "LOGLOSS")}).
#' @param metrics Can be \code{"all"} or a character vector of metrics to be computed (some of \code{c("AIC", "BIC", "R2", "ICC", "RMSE", "LOGLOSS", "SCORE")}).
#' @param ... Arguments passed to or from other methods.
#'
#' @return A data frame (with one row) and one column per "index" (see \code{metrics}).
#'
#' @details This method returns the \emph{adjusted ICC} only, as this is typically
#' of interest when judging the variance attributed to the random effects part
#' of the model (see also \code{\link{icc}}).
#' \cr \cr
#' Furthermore, see 'Details' in \code{\link{model_performance.lm}} for
#' more details on returned indices.
#'
#' @examples
#' library(lme4)
Expand All @@ -22,7 +25,7 @@
#' @export
model_performance.merMod <- function(model, metrics = "all", ...) {
if (all(metrics == "all")) {
metrics <- c("AIC", "BIC", "R2", "ICC", "RMSE", "LOGLOSS")
metrics <- c("AIC", "BIC", "R2", "ICC", "RMSE", "LOGLOSS", "SCORE")
}

mi <- insight::model_info(model)
Expand All @@ -48,6 +51,11 @@ model_performance.merMod <- function(model, metrics = "all", ...) {
if (("LOGLOSS" %in% metrics) && mi$is_binomial) {
out$LOGLOSS <- performance_logloss(model)
}
if (("SCORE" %in% metrics) && (mi$is_binomial || mi$is_count)) {
.scoring_rules <- performance_score(model)
if (!is.na(.scoring_rules$logarithmic)) out$SCORE_LOG <- .scoring_rules$logarithmic
if (!is.na(.scoring_rules$spherical)) out$SCORE_SPHERICAL <- .scoring_rules$spherical
}

# TODO: What with sigma and deviance?

Expand Down
2 changes: 2 additions & 0 deletions R/performance_logloss.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#' bad predictions, while low values indicate good predictions. The lower
#' the log-loss, the better the model predicts the outcome.
#'
#' @seealso \code{\link[=performance_score]{performance_score()}}
#'
#' @examples
#' data(mtcars)
#' m <- glm(formula = vs ~ hp + wt, family = binomial, data = mtcars)
Expand Down
154 changes: 154 additions & 0 deletions R/performance_score.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#' @title Proper Scoring Rules
#' @name performance_score
#'
#' @description Calculates the logarithmic, quadratic/Brier and spherical score
#' from a model with binary or count outcome.
#'
#' @param model Model with binary or count outcome.
#' @param ... Currently not used.
#'
#' @return A list with three elements, the logarithmic, quadratic/Brier and spherical score.
#'
#' @details Proper scoring rules can be used to evaluate the quality of model
#' predictions and model fit. \code{performance_score()} calculates the logarithmic,
#' quadratic/Brier and spherical scoring rules. The spherical rule takes values
#' in the interval \code{[0, 1]}, with values closer to 1 indicating a more
#' accurate model, and the logarithmic rule in the interval \code{[-Inf, 0]},
#' with values closer to 0 indicating a more accurate model.
#'
#' @references Carvalho, A. (2016). An overview of applications of proper scoring rules. Decision Analysis 13, 223–242. \doi{10.1287/deca.2016.0337}
#'
#' @note Code is partially based on \href{https://drizopoulos.github.io/GLMMadaptive/reference/scoring_rules.html}{GLMMadaptive::scoring_rules()}.
#'
#' @seealso \code{\link[=performance_logloss]{performance_logloss()}}
#'
#' @examples
#' ## Dobson (1990) Page 93: Randomized Controlled Trial :
#' counts <- c(18, 17, 15, 20, 10, 20, 25, 13, 12)
#' outcome <- gl(3, 1, 9)
#' treatment <- gl(3, 3)
#' model <- glm(counts ~ outcome + treatment, family = poisson())
#'
#' performance_score(model)
#'
#' \dontrun{
#' library(glmmTMB)
#' data(Salamanders)
#' model <- glmmTMB(
#' count ~ spp + mined + (1 | site),
#' zi = ~ spp + mined,
#' family = nbinom2(),
#' data = Salamanders
#' )
#'
#' performance_score(model)
#' }
#'
#' @importFrom insight get_response model_info
#' @importFrom stats dbinom dpois dnbinom ppois pnbinom
#' @export
performance_score <- function(model) {
minfo <- insight::model_info(model)

prob_fun <- if (minfo$is_binomial) {
function(x, mean, pis, n) stats::dbinom(x, size = n, prob = mean)
} else if (minfo$is_poisson && !minfo$is_zero_inflated) {
function(x, mean, pis, n) stats::dpois(x, lambda = mean)
} else if (minfo$is_negbin && !minfo$is_zero_inflated) {
function(x, mean, pis, n) dnbinom(x, mu = mean, size = exp(.dispersion_parameter(model, minfo)))
} else if (minfo$is_poisson && minfo$is_zero_inflated && !minfo$is_hurdle) {
function(x, mean, pis, n) {
ind0 <- x == 0
out <- (1 - pis) * stats::dpois(x, lambda = mean / (1 - pis))
out[ind0] <- pis[ind0] + out[ind0]
out
}
} else if (minfo$is_zero_inflated && minfo$is_negbin && !minfo$is_hurdle) {
function(x, mean, pis, n) {
ind0 <- x == 0
out <- (1 - pis) * stats::dnbinom(x, mu = mean / (1 - pis), size = exp(.dispersion_parameter(model, minfo)))
out[ind0] <- pis[ind0] + out[ind0]
out
}
} else if (minfo$hurdle && minfo$is_poisson) {
function(x, mean, pis, n) {
ind0 <- x == 0
trunc_zero <- stats::dpois(x, lambda = mean) / stats::ppois(0, lambda = mean, lower.tail = FALSE)
out <- (1 - pis) * trunc_zero
out[ind0] <- pis[ind0]
out
}
} else if (minfo$hurdle && minfo$is_negbin) {
function(x, mean, pis, n) {
ind0 <- x == 0
trunc_zero <- stats::dnbinom(x, mu = mean, size = exp(.dispersion_parameter(model, minfo))) /
stats::pnbinom(0, mu = mean, size = exp(.dispersion_parameter(model, minfo)), lower.tail = FALSE)
out <- (1 - pis) * trunc_zero
out[ind0] <- pis[ind0]
out
}
}

pr <- .predict_score_y(model)
resp <- insight::get_response(model)
p_y <- prob_fun(resp, mean = pr$pred, pis = pr$pred_zi, sum(resp))

quadrat_p <- sum(p_y^2)

structure(
class = "performance_score",
list(
logarithmic = mean(log(p_y)),
quadratic = mean(2 * p_y + quadrat_p),
spherical = mean(p_y / sqrt(quadrat_p))
)
)
}



#' @importFrom stats residuals df.residual
.dispersion_parameter <- function(model, minfo) {
if (inherits(model, "MixMod")) {
model$phis
} else if (inherits(model, "glmmTMB")) {
if (minfo$is_zero_inflated) {
if (!requireNamespace("glmmTMB")) {
stop("Package 'glmmTMB' required for this function work. Please install it.")
}
glmmTMB::getME(model, "theta")
} else {
sum(stats::residuals(model , type = "pearson")^2) / stats::df.residual(model)
}
} else {
tryCatch({
sum(stats::residuals(model , type = "pearson")^2) / stats::df.residual(model)
},
error = function(e) {
0
})
}
}



#' @importFrom stats predict
.predict_score_y <- function(model) {
pred <- NULL
pred_zi <- NULL

if (inherits(model, "MixMod")) {
pred <- stats::predict(model, type = "subject_specific")
pred_zi <- if (!is.null(model$gammas)) attr(pred, "zi_probs")
} else if (inherits(model, "glmmTMB")) {
pred <- stats::predict(model, type = "response")
pred_zi <- stats::predict(model, type = "zprob")
} else if (inherits(model, c("hurdle", "zeroinfl"))) {
pred <- stats::predict(model, type = "response")
pred_zi <- stats::predict(model, type = "zero")
} else {
pred <- stats::predict(model, type = "response")
}

list(pred = pred, pred_zi = pred_zi)
}
21 changes: 20 additions & 1 deletion R/print-methods.R
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ print.performance_hosmer <- function(x, ...) {


#' @export
print.performance_accuracy <- function(x, ...) {
print.performance_accuracy <- function(x, ...) {
# headline
insight::print_color("# Accuracy of Model Predictions\n\n", "blue")

Expand All @@ -367,6 +367,25 @@ print.performance_accuracy <- function(x, ...) {
}


#' @export
print.performance_score <- function(x, ...) {
# headline
insight::print_color("# Proper Scoring Rules\n\n", "blue")

results <- format(
c(
sprintf("%.4f", x$logarithmic),
sprintf("%.4f", x$quadratic),
sprintf("%.4f", x$spherical)
),
justify = "right")

cat(sprintf("logarithmic: %s\n", results[1]))
cat(sprintf(" quadratic: %s\n", results[2]))
cat(sprintf(" spherical: %s\n", results[3]))
}


#' @export
print.check_collinearity <- function(x, ...) {
insight::print_color("# Check for Multicollinearity\n", "blue")
Expand Down
4 changes: 3 additions & 1 deletion man/model_performance.lm.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0516124

Please sign in to comment.