File: plot_glmnet.R

package info (click to toggle)
r-cran-plotmo 3.7.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,400 kB
  • sloc: sh: 13; makefile: 2
file content (304 lines) | stat: -rw-r--r-- 12,250 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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# plot_glmnet.R:
#
# This code is based on code in glmnet version 2.0-5 (march 2016).

plot_glmnet <- function(x=stop("no 'x' argument"),
                        xvar=c("rlambda", "lambda", "norm", "dev"),
                        label=10, nresponse=NA, grid.col=NA, s=NA, ...)
{
    check.classname(x, "x", c("glmnet", "multnet"))
    obj <- x
    beta <- get.beta(obj$beta, nresponse)
    ibeta <- nonzeroCoef(beta) # ibeta is a vector of coefficient indices
    if(length(ibeta) == 0) {
        plot(0:1, 0:1, col=0) # dummy plot
        legend("topleft", legend="all glmnet coefficients are zero", bty="n")
        return(invisible(NULL))
    }
    # following was in original plot.glmnet code but seems unnecessary
    # if(length(ibeta) == 1) {
    #     warning("1 or less nonzero coefficients; glmnet plot is not meaningful")
    #     plot(0:1, 0:1, col=0)
    #     legend("topleft", legend="only one coefficient is nonzero", bty="n")
    #     return()
    # }
    beta <- as.matrix(beta[ibeta, , drop=FALSE])
    xlim <- dota("xlim", ...) # get xlim from dots, NA if not in dots
    xvar <- match.arg1(xvar)
    switch(xvar,
    "norm"= {
        if(inherits(obj, "multnet") || inherits(obj, "mrelnet")) {
            # we don't (yet) precalc norm or support type.coef, so have to stop here
            stop0("xvar=\"norm\" is not supported by plot_gbm for ",
                  "multiple responses (use plot.glmnet instead)")
        }
        x <- apply(abs(beta), 2, sum)
        if(!is.specified(xlim))
            xlim <- c(min(x), max(x))
        xlab <- "L1 Norm"
        approx.f <- 1
    },
    "lambda"= {
        x <- log(obj$lambda)
        if(!is.specified(xlim))
            xlim <- c(min(x), max(x))
        xlab <- "Log Lambda"
        approx.f <- 0
    },
    "rlambda"= {
        x <- log(obj$lambda)
        if(!is.specified(xlim))
            xlim <- c(max(x), min(x)) # backwards
        xlab <- "Log Lambda"
        approx.f <- 0
    },
    "dev"= {
        x <- obj$dev.ratio
        if(!is.specified(xlim))
            xlim <- c(min(x), max(x))
        xlab <- "Fraction Deviance Explained"
        approx.f <- 1
    })
    xlim <- fix.lim(xlim)
    if(xvar != "rlambda")
        stopifnot(xlim[1] < xlim[2])
    else if(xlim[2] >= xlim[1]) # backwards
         stop0("xlim[1] must be bigger than xlim[2] for xvar=\"rlambda\"")

    iname <- get.iname(beta, ibeta, label)  # index of varnames on rhs of plot

    old.par <- par("mar", "mgp", "cex.axis", "cex.lab")
    on.exit(par(mar=old.par$mar, mgp=old.par$mgp, cex.axis=old.par$cex.axis,
                cex.lab=old.par$cex.lab))
    mar4 <- old.par$mar[4]      # right hand margin
    if(length(iname)) {
        cex.names <- min(1, max(.5, 2.5 / sqrt(length(iname)))) # seems ok
        # ensure right margin is big enough for the varnames
        # can't use strwidth because no plot yet, so just estimate
        mar4 <- max(old.par$mar[4] + 1,
                    .75 * cex.names * par("cex") * max(nchar(names(iname))))
    }
    # set mar[3] with space for top axis and maybe main, and mar[4] for rhs labels
    main <- dota("main", ...)   # get main from dots, NA if not in dots
    nlines.needed.for.main <- if(is.specified(main)) nlines(main) + .5 else 0
    par(mar=c(old.par$mar[1],
              old.par$mar[2],
              max(old.par$mar[3], nlines.needed.for.main + 2.6),
              mar4))
    par(mgp=c(1.5, .4, 0))      # squash axis annotations
    par(cex.axis=.8)
    ylab <- "Coefficients"
    if(is.list(obj$beta))       # multiple response model?
        ylab <- paste0(ylab, ": Response ", rownames(obj$dfmat)[nresponse])
    coef.col <- get.coef.col(..., beta=beta)  # color of coef lines

    # discard lines with color NA or 0
    keep <- which((coef.col != "NA") & (coef.col != "0"))
    iname <- iname[iname %in% keep]
    beta[-keep,] <- NA

    # Call graphics::matplot but drop args in dots that aren't graphics args
    # or formal args of graphics::matplot.
    # If argname below is prefixed with force. then ignore any such arg in dots.
    # Any argname below prefixed with def. can be overridden by a user arg in dots.
    # force.main="" because we later manually add a top axis and possibly main.
    call.plot(graphics::matplot, force.x=x, force.y=t(beta),
        force.main="", force.col=coef.col,
        def.xlim=xlim, def.xlab=xlab, def.ylab=ylab,
        def.lty=1, def.lwd=1, def.type="l", ...)

    abline(h=0, col="gray", lty=3) # zero axis line
    maybe.grid(x=x, beta=beta, grid.col=grid.col, coef.col=coef.col, ...)
    if(xvar == "rlambda") {
        # args are named below to prevent potential clash with argnames in dots
        annotate.rlambda(lambda=obj$lambda, x=x, beta=beta, s=s,
                         grid.col=grid.col, coef.col=coef.col, ...)
        toplab <- "Lambda"
    } else {
        top.axis(obj, x, nresponse, approx.f)
        toplab <- "Degrees of Freedom"
    }
    mtext(toplab, side=3, line=1.5, cex=par("cex") * par("cex.lab"))
    if(is.specified(main))
        mtext(main, side=3, line=3, , cex=par("cex")) # above top axis
    if(length(iname))
        right.labs(beta, iname, cex.names, coef.col)
    invisible(NULL)
}
get.beta <- function(beta, nresponse)
{
    if(is.list(beta)) { # multiple response model?
        check.integer.scalar(nresponse, min=1, max=length(beta),
                             na.ok=TRUE, logical.ok=FALSE)
        if(is.na(nresponse))
            stop0(
"Use the nresponse argument to specify a response for this multiple response model.\n",
"       Example: nresponse=", length(beta))
        check.index(nresponse, "nresponse", beta)
        beta <- beta[[nresponse]]
    }
    beta
}
get.coef.col <- function(..., beta)
{
    # default colors are distinguishable yet harmonious (at least to my eye)
    # adjacent colors are as different as easily possible
    def.col <- c("black", "red", "gray50", "orangered3", "darkorange", "magenta2")

    col <- dota("col", DEF=def.col, ...) # get col from dots, def.col if not in dots

    # the colors must stay in the above order as we move down rhs of plot
    order <- order(beta[, ncol(beta)], decreasing=TRUE)
    coef.col <- vector(mode="character", nrow(beta))
    coef.col[order] <- rep_len(col, nrow(beta))
    coef.col
}
# named index of varnames to be printed on right of plot, NULL if none
get.iname <- function(beta, ibeta, label)
{
    iname <- NULL
    check.integer.scalar(label, min=0, logical.ok=TRUE, na.ok=TRUE)
    if(!is.na(label) && label) { # allow label=NA, treat as FALSE
        names <- if(is.null(rownames(beta))) paste(ibeta)
                 else                        rownames(beta)
        names[!nzchar(names)] <- paste(ibeta)[!nzchar(names)]
        iname <- order(abs(beta[, ncol(beta)]), decreasing=TRUE)
        if(is.logical(label)) # label=TRUE is special meaning all
            iname <- iname[1:length(iname)]
        else if(length(iname) > label)
            iname <- iname[1:label]
        names(iname) <- abbreviate(names[iname], minlength=8)
    }
    iname # named index of varnames to be printed, NULL if none
}
maybe.grid <- function(x, beta, grid.col, coef.col, ...)
{
    if(is.specified(grid.col[1])) {
        grid(col=grid.col[1], lty=1)
        # replot over the grid (using add=TRUE)
        call.plot(graphics::matplot, force.x=x, force.y=t(beta),
            force.add=TRUE, force.main="", force.col=coef.col,
            def.lty=1, def.lwd=1, def.type="l", ...)
    }
}
right.labs <- function(beta, iname, cex.names, coef.col) # varnames on right of plot
{
    usr <- par("usr")
    text(x=usr[2] + .01 * (usr[2] - usr[1]),
         y=spread.labs(beta[iname, ncol(beta)], mindiff=1.2 * cex.names * strheight("X")),
         labels=names(iname), cex=cex.names, col=coef.col[iname], adj=0, xpd=NA)
}
top.axis <- function(obj, x, nresponse, approx.f)
{
    at <- pretty(x)
    # use is.list(obj$beta) to determine if multiple response model
    df <- if(is.list(obj$beta)) obj$dfmat[nresponse,] else obj$df
    # compute df by interpolating to df at next smaller lambda
    # thanks to Yunyang Qian
    prettydf <- approx(x=x, y=df, xout=at,
                       rule=2, method="constant", f=approx.f)$y
    axis(3, at=at, labels=prettydf)
}
# Draw the top axis of an rlambda plot.
# Also draw a labeled vertical line at lambda=s, if s isn't NA.
# Dot arguments prefixed with "s". can be used to set the annotation
# attributes e.g. s.col=NA or s.col=0 for no vertical line.
# This is achieved with call.plot(text.on.white, PREFIX="s.", ...) below.
annotate.rlambda <- function(lambda, x, beta, s, grid.col, coef.col, ...)
{
    check.numeric.scalar(s, na.ok=TRUE, null.ok=TRUE, logical.ok=FALSE)
    s.col <- dota("s.col", DEF=1, ...) # get s.col from dots, 1 if not in dots
    add.s.line <- !is.null(s) && !is.na(s) && is.specified(s.col)

    # top axis
    at <- pretty(x)
    labs <- signif(exp(at), digits=2)
    # hack: delete confusing rightmost lab (if any) with a value greater
    # than s but drawn to the right of the vertical line at s
    if(add.s.line && s <= labs[1])
        labs[1] <- ""
    axis(3, at=at, labels=labs)

    if(add.s.line) # add vertical line showing s?
        add.s.line(lambda=lambda, x=x, beta=beta, s=s,
                   grid.col=grid.col, coef.col=coef.col, s.col=s.col, ...)
}
add.s.line <- function(lambda, x, beta, s, grid.col, coef.col, s.col, ...)
{
    line.col <- "gray"
    line.lty <- 1
    if(is.specified(grid.col)) {
            line.col <- 1
            line.lty <- 3
    }
    log.s <- log(max(lambda[length(lambda)], s))

    abline(v=log.s, col=line.col, lty=line.lty) # vertical line at s

    # replot over the vertical line (using add=TRUE)
    call.plot(graphics::matplot, force.x=x, force.y=t(beta),
              force.add=TRUE, force.main="", force.col=coef.col,
              def.lty=1, def.lwd=1, def.type="l", ...)

    # add s label on vertical line
    # to minimize overplotting, y coord of label is biggest gap between matplot lines
    usr <- par("usr") # xmin, xmax, ymin, ymax
    col.index <- which.min(abs(lambda-s)) # lambda column corresponding to s
    y <- sort(c(usr[3], beta[, col.index], usr[4])) # include plot edges, and sort
    which <- which.max(diff(y))
    # call graphics::matplot() but drop args in dots that aren't graphics args
    # or argnames prefixed with "s." or formal args of text.on.white
    call.plot(text.on.white, PREFIX="s.",
            force.x=log.s, force.y=(y[which]+y[which+1]) / 2,
            force.label= # gsub below drops leading and trailing zeros for compactness
                if(s == 0) "s=0"
                else        paste0("s=", gsub("^0|0$|\\.0*$", "", signif(s,2))),
            force.col=s.col, force.cex=.8, def.srt=90, def.xpd=NA, ...)
}
# Return NULL or an integer vector
# Reproduced here (from glmnet version 2.0-16, nov 2018)
# so don't have to import glmnet into plotmo.

nonzeroCoef = function (beta, bystep = FALSE)
{
### bystep = FALSE means which variables were ever nonzero
### bystep = TRUE means which variables are nonzero for each step
  nr=nrow(beta)
  if (nr == 1) {#degenerate case
    if (bystep)
      apply(beta, 2, function(x) if (abs(x) > 0)
            1
      else NULL)
    else {
      if (any(abs(beta) > 0))
        1
      else NULL
    }
  }
  else {
    beta=abs(beta)>0 # this is sparse
    which=seq(nr)
    ones=rep(1,ncol(beta))
    nz=as.vector((beta%*%ones)>0)
    which=which[nz]
    if (bystep) {
      if(length(which)>0){
        beta=as.matrix(beta[which,,drop=FALSE])
        nzel = function(x, which) if (any(x))
          which[x]
        else NULL
        which=apply(beta, 2, nzel, which)
        if(!is.list(which))which=data.frame(which)# apply can return a matrix!!
        which
      }
      else{
        dn=dimnames(beta)[[2]]
        which=vector("list",length(dn))
        names(which)=dn
        which
      }

    }
    else which
  }
}