File: postResample.R

package info (click to toggle)
r-cran-caret 7.0-1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,036 kB
  • sloc: ansic: 210; sh: 10; makefile: 2
file content (283 lines) | stat: -rw-r--r-- 11,506 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#' Calculates performance across resamples
#'
#' Given two numeric vectors of data, the mean squared error and
#'  R-squared are calculated. For two factors, the overall agreement
#'  rate and Kappa are determined.
#'
#' \code{postResample} is meant to be used with \code{apply}
#'  across a matrix. For numeric data the code checks to see if the
#'  standard deviation of either vector is zero. If so, the
#'  correlation between those samples is assigned a value of zero.
#'  \code{NA} values are ignored everywhere.
#'
#' Note that many models have more predictors (or parameters) than
#'  data points, so the typical mean squared error denominator (n -
#'  p) does not apply. Root mean squared error is calculated using
#'  \code{sqrt(mean((pred - obs)^2}. Also, \eqn{R^2} is calculated
#'  wither using as the square of the correlation between the
#'  observed and predicted outcomes when \code{form = "corr"}. when
#'  \code{form = "traditional"}, \deqn{ R^2 = 1-\frac{\sum (y_i -
#'  \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} }. Mean absolute error
#'  is calculated using \code{mean(abs(pred-obs))}.
#'
#' \code{defaultSummary} is the default function to compute
#'  performance metrics in \code{\link{train}}. It is a wrapper
#'  around \code{postResample}. The first argument is \code{data},
#'  which is \code{data.frame} with columns named \code{obs} and
#'  \code{pred} for the observed and predicted outcome values
#'  (either numeric data for regression or character values for
#'  classification). The second argument is \code{lev}, a character
#'  string that has the outcome factor levels or NULL for a
#'  regression model. The third parameter is \code{model}, which can
#'  be used if a summary metric is specific to a model function. If
#'  other columns from the data are required to compute the summary
#'  statistics, but should not be used in the model, the
#'  \code{recipe} method for \code{\link{train}} can be used.
#'
#' \code{twoClassSummary} computes sensitivity, specificity and
#'  the area under the ROC curve. \code{mnLogLoss} computes the
#'  minus log-likelihood of the multinomial distribution (without
#'  the constant term): \deqn{ -logLoss = \frac{-1}{n}\sum_{i=1}^n
#'  \sum_{j=1}^C y_{ij} \log(p_{ij}) } where the \code{y} values are
#'  binary indicators for the classes and \code{p} are the predicted
#'  class probabilities.
#'
#' \code{prSummary} (for precision and recall) computes values for
#'  the default 0.50 probability cutoff as well as the area under
#'  the precision-recall curve across all cutoffs and is labelled as
#'  \code{"AUC"} in the output. If assumes that the first level of
#'  the factor variables corresponds to a relevant result but the
#'  \code{lev} argument can be used to change this.
#'
#' \code{multiClassSummary} computes some overall measures of for
#'  performance (e.g. overall accuracy and the Kappa statistic) and
#'  several averages of statistics calculated from "one-versus-all"
#'  configurations. For example, if there are three classes, three
#'  sets of sensitivity values are determined and the average is
#'  reported with the name ("Mean_Sensitivity"). The same is true
#'  for a number of statistics generated by
#'  \code{\link{confusionMatrix}}. With two classes, the basic
#'  sensitivity is reported with the name "Sensitivity".
#'
#' To use \code{twoClassSummary} and/or \code{mnLogLoss}, the
#'  \code{classProbs} argument of \code{\link{trainControl}} should
#'  be \code{TRUE}. \code{multiClassSummary} can be used without
#'  class probabilities but some statistics (e.g. overall log loss
#'  and the average of per-class area under the ROC curves) will not
#'  be in the result set.
#'
#' Other functions can be used via the \code{summaryFunction}
#'  argument of \code{\link{trainControl}}. Custom functions must
#'  have the same arguments as\code{defaultSummary}.
#'
#' The function \code{getTrainPerf} returns a one row data frame
#'  with the resampling results for the chosen model. The statistics
#'  will have the prefix "\code{Train}" (i.e. "\code{TrainROC}").
#'  There is also a column called "\code{method}" that echoes the
#'  argument of the call to \code{\link{trainControl}} of the same
#'  name.
#'
#' @aliases postResample defaultSummary twoClassSummary prSummary
#'  getTrainPerf mnLogLoss R2 RMSE multiClassSummary MAE
#' @param pred A vector of numeric data (could be a factor)
#' @param obs A vector of numeric data (could be a factor)
#' @param data a data frame with columns \code{obs} and
#'  \code{pred} for the observed and predicted outcomes. For metrics
#'  that rely on class probabilities, such as
#'  \code{twoClassSummary}, columns should also include predicted
#'  probabilities for each class. See the \code{classProbs} argument
#'  to \code{\link{trainControl}}.
#' @param lev a character vector of factors levels for the
#'  response. In regression cases, this would be \code{NULL}.
#' @param model a character string for the model name (as taken
#'  from the \code{method} argument of \code{\link{train}}.
#' @return A vector of performance estimates.
#' @author Max Kuhn, Zachary Mayer
#' @seealso \code{\link{trainControl}}
#' @references Kvalseth. Cautionary note about \eqn{R^2}. American Statistician
#' (1985) vol. 39 (4) pp. 279-285
#' @keywords utilities
#' @examples
#'
#' predicted <-  matrix(rnorm(50), ncol = 5)
#' observed <- rnorm(10)
#' apply(predicted, 2, postResample, obs = observed)
#'
#' classes <- c("class1", "class2")
#' set.seed(1)
#' dat <- data.frame(obs =  factor(sample(classes, 50, replace = TRUE)),
#'                   pred = factor(sample(classes, 50, replace = TRUE)),
#'                   class1 = runif(50))
#' dat$class2 <- 1 - dat$class1
#'
#' defaultSummary(dat, lev = classes)
#' twoClassSummary(dat, lev = classes)
#' prSummary(dat, lev = classes)
#' mnLogLoss(dat, lev = classes)
#'
#' @export postResample
postResample <- function(pred, obs)
{

  isNA <- is.na(pred)
  pred <- pred[!isNA]
  obs <- obs[!isNA]

  if (!is.factor(obs) && is.numeric(obs))
    {
      if(length(obs) + length(pred) == 0)
        {
          out <- rep(NA, 3)
        } else {
          if(length(unique(pred)) < 2 || length(unique(obs)) < 2)
            {
              resamplCor <- NA
            } else {
              resamplCor <- try(cor(pred, obs, use = "pairwise.complete.obs"), silent = TRUE)
              if (inherits(resamplCor, "try-error")) resamplCor <- NA
            }
          mse <- mean((pred - obs)^2)
          mae <- mean(abs(pred - obs))

          out <- c(sqrt(mse), resamplCor^2, mae)
        }
      names(out) <- c("RMSE", "Rsquared", "MAE")
    } else {
      if(length(obs) + length(pred) == 0)
        {
          out <- rep(NA, 2)
        } else {
          pred <- factor(pred, levels = levels(obs))
          requireNamespaceQuietStop("e1071")
          out <- unlist(e1071::classAgreement(table(obs, pred)))[c("diag", "kappa")]
        }
      names(out) <- c("Accuracy", "Kappa")
    }
  if(any(is.nan(out))) out[is.nan(out)] <- NA
  out
}


#' @rdname postResample
#' @importFrom pROC roc
#' @export
twoClassSummary <- function (data, lev = NULL, model = NULL) {
  if(length(lev) > 2) {
    stop(paste("Your outcome has", length(lev),
               "levels. The twoClassSummary() function isn't appropriate."))
  }
  requireNamespaceQuietStop('pROC')
  if (!all(levels(data[, "pred"]) == lev)) {
    stop("levels of observed and predicted data do not match")
  }
  rocObject <- try(pROC::roc(data$obs, data[, lev[1]], direction = ">", quiet = TRUE), silent = TRUE)
  rocAUC <- if(inherits(rocObject, "try-error")) NA else rocObject$auc
  out <- c(rocAUC,
           sensitivity(data[, "pred"], data[, "obs"], lev[1]),
           specificity(data[, "pred"], data[, "obs"], lev[2]))
  names(out) <- c("ROC", "Sens", "Spec")
  out
}

#' @rdname postResample
#' @importFrom stats complete.cases
#' @export
mnLogLoss <- function(data, lev = NULL, model = NULL){
  if(is.null(lev)) stop("'lev' cannot be NULL")
  if(!all(lev %in% colnames(data)))
    stop("'data' should have columns consistent with 'lev'")
  if(!all(sort(lev) %in% sort(levels(data$obs))))
    stop("'data$obs' should have levels consistent with 'lev'")

  dataComplete <- data[complete.cases(data),]
  probs <- as.matrix(dataComplete[, lev, drop = FALSE])

  logLoss <- ModelMetrics::mlogLoss(dataComplete$obs, probs)
  c(logLoss = logLoss)
}

#' @rdname postResample
#' @export
multiClassSummary <- function(data, lev = NULL, model = NULL){
  #Check data
  if (!all(levels(data[, "pred"]) == levels(data[, "obs"])))
    stop("levels of observed and predicted data do not match")

  has_class_probs <- all(lev %in% colnames(data))

  if(has_class_probs) {
    ## Overall multinomial loss
    lloss <- mnLogLoss(data = data, lev = lev, model = model)

    requireNamespaceQuietStop("pROC")
    requireNamespaceQuietStop("MLmetrics")

    #Calculate custom one-vs-all ROC curves for each class
    prob_stats <-
      lapply(levels(data[, "pred"]),
             function(x){
               #Grab one-vs-all data for the class
               obs  <- ifelse(data[,"obs"] == x, 1, 0)
               prob <- data[,x]
               roc_auc <- try(pROC::roc(obs, data[,x], direction = "<", quiet = TRUE), silent = TRUE)
               roc_auc <- if (inherits(roc_auc, "try-error")) NA else roc_auc$auc
               pr_auc <- try(MLmetrics::PRAUC(y_pred = data[,x], y_true = obs),
                             silent = TRUE)
               if(inherits(pr_auc, "try-error"))
                 pr_auc <- NA
               res <- c(ROC = roc_auc, AUC = pr_auc)
               return(res)
               })
    prob_stats <- do.call("rbind", prob_stats)
    prob_stats <- colMeans(prob_stats, na.rm = TRUE)
  }

  #Calculate confusion matrix-based statistics
  CM <- confusionMatrix(data[, "pred"], data[, "obs"],
                        mode = "everything")

  #Aggregate and average class-wise stats
  #Todo: add weights
  # RES: support two classes here as well
  if (length(levels(data[, "pred"])) == 2) {
    class_stats <- CM$byClass
  } else {
    class_stats <- colMeans(CM$byClass)
    names(class_stats) <- paste("Mean", names(class_stats))
  }

  # Aggregate overall stats
  overall_stats <-
    if (has_class_probs)
      c(CM$overall,
        logLoss = as.numeric(lloss),
        AUC = unname(prob_stats["ROC"]),
        prAUC = unname(prob_stats["AUC"]))
  else
    CM$overall


  # Combine overall with class-wise stats and remove some stats we don't want
  stats <- c(overall_stats, class_stats)
  stats <- stats[! names(stats) %in% c('AccuracyNull', "AccuracyLower", "AccuracyUpper",
                                       "AccuracyPValue", "McnemarPValue",
                                       'Mean Prevalence', 'Mean Detection Prevalence')]

  # Clean names
  names(stats) <- gsub('[[:blank:]]+', '_', names(stats))

  # Change name ordering to place most useful first
  # May want to remove some of these eventually
  stat_list <- c("Accuracy", "Kappa", "Mean_F1",
                 "Mean_Sensitivity", "Mean_Specificity",
                 "Mean_Pos_Pred_Value", "Mean_Neg_Pred_Value",
                 "Mean_Precision", "Mean_Recall",
                 "Mean_Detection_Rate",
                 "Mean_Balanced_Accuracy")
  if(has_class_probs) stat_list <- c("logLoss", "AUC", "prAUC", stat_list)
  if (length(levels(data[, "pred"])) == 2) stat_list <- gsub("^Mean_", "", stat_list)

  stats <- stats[c(stat_list)]

  return(stats)
}