setClass("mle", representation(call = "language", coef = "numeric", fullcoef = "numeric", vcov = "matrix", min = "numeric", details = "list", minuslogl = "function", method = "character")) setClass("summary.mle", representation(call = "language", coef = "matrix", m2logL = "numeric")) setClass("profile.mle", representation(profile="list", summary="summary.mle")) mle <- function(minuslogl, start=formals(minuslogl), method="BFGS", fixed=list(), ...) { # Insert sanity checks here... call <- match.call() n <- names(fixed) fullcoef <- formals(minuslogl) if(any(! n %in% names(fullcoef))) stop("some named arguments in 'fixed' are not arguments to the supplied log-likelihood") fullcoef[n] <- fixed if(!missing(start) && (!is.list(start) || is.null(names(start)))) stop("'start' must be a named list") start[n] <- NULL start <- sapply(start, eval.parent) # expressions are allowed nm <- names(start) oo <- match(nm, names(fullcoef)) if (any(is.na(oo))) stop("some named arguments in 'start' are not arguments to the supplied log-likelihood") start <- start[order(oo)] f <- function(p){ l <- as.list(p) names(l) <- nm l[n] <- fixed do.call("minuslogl", l) } oout <- optim(start, f, method=method, hessian=TRUE, ...) coef <- oout$par vcov <- if(length(coef)) solve(oout$hessian) else matrix(numeric(0),0,0) min <- oout$value fullcoef[nm] <- coef new("mle", call=call, coef=coef, fullcoef=unlist(fullcoef), vcov=vcov, min=min, details=oout, minuslogl=minuslogl, method=method) } setMethod("coef", "mle", function(object) object@fullcoef ) setMethod("coef", "summary.mle", function(object) object@coef ) setMethod("show", "mle", function(object){ cat("\nCall:\n") print(object@call) cat("\nCoefficients:\n") print(coef(object)) }) setMethod("show", "summary.mle", function(object){ cat("Maximum likelihood estimation\n\nCall:\n") print(object@call) cat("\nCoefficients:\n") print(coef(object)) cat("\n-2 log L:", object@m2logL, "\n") }) setMethod("summary", "mle", function(object, ...){ cmat <- cbind(Estimate = object@coef, `Std. Error` = sqrt(diag(object@vcov))) m2logL <- 2*object@min new("summary.mle", call=object@call, coef=cmat, m2logL= m2logL) }) setMethod("profile", "mle", function (fitted, which = 1:p, maxsteps = 100, alpha = 0.01, zmax = sqrt(qchisq(1 - alpha/2, p)), del = zmax/5, trace = FALSE, ...) { onestep <- function(step) { bi <- B0[i] + sgn * step * del * std.err[i] fix <- list(bi) names(fix) <- pi call$fixed <- fix pfit <- try(eval.parent(call, 2), silent=TRUE) if(inherits(pfit, "try-error")) return(NA) else { zz <- 2*(pfit@min - fitted@min) ri <- pv0 ri[, names(pfit@coef)] <- pfit@coef ri[, pi] <- bi if (zz > -0.001) zz <- max(zz, 0) else stop("profiling has found a better solution, so original fit had not converged") z <- sgn * sqrt(zz) pvi <<- rbind(pvi, ri) zi <<- c(zi, z) } if (trace) cat(bi, z, "\n") z } ## Profile the likelihood around its maximum ## Based on profile.glm in MASS summ <- summary(fitted) std.err <- summ@coef[, "Std. Error"] Pnames <- names(B0 <- fitted@coef) pv0 <- t(as.matrix(B0)) p <- length(Pnames) prof <- vector("list", length = length(which)) names(prof) <- Pnames[which] call <- fitted@call call$minuslogl <- fitted@minuslogl ndeps <- eval.parent(call$control$ndeps) parscale <- eval.parent(call$control$parscale) for (i in which) { zi <- 0 pvi <- pv0 pi <- Pnames[i] if (!is.null(ndeps)) call$control$ndeps <- ndeps[-i] if (!is.null(parscale)) call$control$parscale <- parscale[-i] for (sgn in c(-1, 1)) { if (trace) cat("\nParameter:", pi, c("down", "up")[(sgn + 1)/2 + 1], "\n") step <- 0 z <- 0 ## This logic was a bit frail in some cases with ## high parameter curvature. We should probably at least ## do something about cases where the mle call fails ## because the parameter gets stepped outside the domain. ## (We now have.) call$start <- as.list(B0) lastz <- 0 while ((step <- step + 1) < maxsteps && abs(z) < zmax) { z <- onestep(step) if(is.na(z)) break lastz <- z } if(abs(lastz) < zmax) { ## now let's try a bit harder if we came up short for(dstep in c(0.2, 0.4, 0.6, 0.8, 0.9)) { z <- onestep(step - 1 + dstep) if(is.na(z) || abs(z) > zmax) break } } else if(length(zi) < 5) { # try smaller steps mxstep <- step - 1 step <- 0.5 while ((step <- step + 1) < mxstep) onestep(step) } } si <- order(pvi[, i]) prof[[pi]] <- data.frame(z = zi[si]) prof[[pi]]$par.vals <- pvi[si,, drop=FALSE] } new("profile.mle", profile = prof, summary = summ) }) setMethod("plot", signature(x="profile.mle", y="missing"), function (x, levels, conf = c(99, 95, 90, 80, 50)/100, nseg = 50, absVal = TRUE, ...) { ## Plot profiled likelihood ## Based on profile.nls (package stats) obj <- x@profile confstr <- NULL if (missing(levels)) { levels <- sqrt(qchisq(pmax(0, pmin(1, conf)), 1)) confstr <- paste(format(100 * conf), "%", sep = "") } if (any(levels <= 0)) { levels <- levels[levels > 0] warning("levels truncated to positive values only") } if (is.null(confstr)) { confstr <- paste(format(100 * pchisq(levels^2, 1)), "%", sep = "") } mlev <- max(levels) * 1.05 nm <- names(obj) opar <- par(mar = c(5, 4, 1, 1) + 0.1) if (absVal) { for (i in seq(along = nm)) { ## This does not need to be monotonic sp <- splines::interpSpline(obj[[i]]$par.vals[, i], obj[[i]]$z, na.action=na.omit) bsp <-splines:: backSpline(sp) ## xlim <- predict(bsp, c(-mlev, mlev))$y if (is.na(xlim[1])) xlim[1] <- min(obj[[i]]$par.vals[, i]) if (is.na(xlim[2])) xlim[2] <- max(obj[[i]]$par.vals[, i]) plot(abs(z) ~ par.vals[, i], data = obj[[i]], xlab = nm[i], ylim = c(0, mlev), xlim = xlim, ylab = expression(abs(z)), type = "n") avals <- rbind(as.data.frame(predict(sp)), data.frame(x = obj[[i]]$par.vals[, i], y = obj[[i]]$z)) avals$y <- abs(avals$y) lines(avals[order(avals$x), ], col = 4) abline(v = predict(bsp, 0)$y, h=0, col = 3, lty = 2) for (lev in levels) { ## Note: predict may return NA if we didn't profile ## far enough in either direction. That's OK for the ## "h" part of the plot, but the horizontal line made ## with "l" disappears. pred <- predict(bsp, c(-lev, lev))$y lines(pred, rep(lev, 2), type = "h", col = 6, lty = 2) pred <- ifelse(is.na(pred), xlim, pred) lines(pred, rep(lev, 2), type = "l", col = 6, lty = 2) } } } else { for (i in seq(along = nm)) { ## This does not need to be monotonic sp <- splines::interpSpline(obj[[i]]$par.vals[, i], obj[[i]]$z, na.action=na.omit) bsp <- splines::backSpline(sp) ## xlim <- predict(bsp, c(-mlev, mlev))$y x0 <- predict(bsp, 0)$y if (is.na(xlim[1])) xlim[1] <- min(obj[[i]]$par.vals[, i]) if (is.na(xlim[2])) xlim[2] <- max(obj[[i]]$par.vals[, i]) plot(z ~ par.vals[, i], data = obj[[i]], xlab = nm[i], ylim = c(-mlev, mlev), xlim = xlim, ylab = expression(z), type = "n") lines(predict(sp), col = 4) abline(h = 0, v=x0, col = 3, lty = 2) for (lev in levels) { pred <- predict(bsp, c(-lev, lev))$y lines(pred, c(-lev, lev), type = "h", col = 6, lty = 2) pred <- ifelse(is.na(pred), xlim, pred) lines(c(x0,pred[2]), rep(lev, 2), type = "l", col = 6, lty = 2) lines(c(pred[1],x0), rep(-lev, 2), type = "l", col = 6, lty = 2) } } } par(opar) }) setMethod("confint", "profile.mle", function (object, parm, level = 0.95, ...) { ## Calculate confidence intervals based on likelihood ## profiles of <- object@summary pnames <- rownames(of@coef) if (missing(parm)) parm <- seq(along=pnames) if (is.character(parm)) parm <- match(parm, pnames, nomatch = 0) a <- (1 - level)/2 a <- c(a, 1 - a) pct <- paste(round(100 * a, 1), "%") ci <- array(NA, dim = c(length(parm), 2), dimnames = list(pnames[parm], pct)) cutoff <- qnorm(a) for (pm in parm) { pro <- object@profile[[pnames[pm]]] sp <- if (length(pnames) > 1) spline(x = pro[, "par.vals"][, pm], y = pro[, 1]) else spline(x = pro[, "par.vals"], y = pro[, 1]) ci[pnames[pm], ] <- approx(sp$y, sp$x, xout = cutoff)$y } drop(ci) }) setMethod("confint", "mle", function (object, parm, level = 0.95, ...) { cat("Profiling...\n") confint(profile(object), parm, level, ...) }) setMethod("logLik", "mle", function (object, ...) { if(length(list(...))) warning("extra arguments discarded") val <- -object@min attr(val, "df") <- length(object@coef) class(val) <- "logLik" val }) setMethod("vcov", "mle", function (object, ...) object@vcov) setMethod("update", "mle", function (object, ..., evaluate = TRUE) { call <- object@call extras <- match.call(expand.dots = FALSE)$... if (length(extras) > 0) { existing <- !is.na(match(names(extras), names(call))) for (a in names(extras)[existing]) call[[a]] <- extras[[a]] if (any(!existing)) { call <- c(as.list(call), extras[!existing]) call <- as.call(call) } } if (evaluate) eval(call, parent.frame()) else call })