Skip to content

Commit 9e8094a

Browse files
authored
Merge pull request #164 from danforthcenter/geoms-and-stats
Geoms and stats
2 parents bfbb2b6 + 46d62cc commit 9e8094a

21 files changed

Lines changed: 983 additions & 7 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ vignettes/*.html
1313
vignettes/articles/*.html
1414
docs
1515
tools/lib
16-
tools/*.html
16+
tools/*.html
17+
\#\*R:scripts\*\#

DESCRIPTION

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: pcvr
22
Type: Package
33
Title: Plant Phenotyping and Bayesian Statistics
4-
Version: 1.3.1
4+
Version: 1.4.0
55
Authors@R:
66
c(person("Josh", "Sumner", email = "jsumner@danforthcenter.org",
77
role = c("aut", "cre"), comment = c(ORCID = "0000-0002-3399-9063")),
@@ -48,7 +48,7 @@ Imports:
4848
scales,
4949
survival,
5050
car
51-
Suggests:
51+
Suggests:
5252
knitr,
5353
rmarkdown,
5454
brms,
@@ -57,6 +57,8 @@ Suggests:
5757
cmdstanr,
5858
rstan,
5959
caret,
60+
vctrs,
61+
plyr,
6062
testthat (>= 3.0.0)
6163
VignetteBuilder: knitr
6264
Config/testthat/edition: 3

NAMESPACE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export(read.pcv)
6363
export(read.pcv.3)
6464
export(relativeTolerance)
6565
export(rqPlot)
66+
export(stat_brms_model)
67+
export(stat_growthss)
68+
export(stat_lme_model)
69+
export(stat_nlme_model)
70+
export(stat_nlrq_model)
71+
export(stat_nls_model)
6672
export(survregPlot)
6773
export(testGrowth)
6874
import(FactoMineR)

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# pcvr 1.4.0
2+
3+
Added `stat_growthss` to make ggplot layers of models from `growthSS`.
4+
15
# pcvr 1.3.1
26

37
Cran release

R/mvSS.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#' \donttest{
3838
#' mod1 <- fitGrowth(ss1, backend = "cmdstanr", iter = 1000, chains = 1, cores = 1)
3939
#' growthPlot(mod1, ss1$pcvrForm, df = ss1$df)
40+
#' library(ggplot2)
41+
#' ggplot() + stat_brms_model(fit = mod1, ss = ss1)
4042
#' }
4143
#'
4244
#' # when the model is longitudinal the same model is possible with growthSS

R/nlmePlot.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ nlmePlot <- function(fit, form, df = NULL, groups = NULL, timeRange = NULL, face
142142
#' @noRd
143143
.add_sigma_bounds <- function(preds, fit, x, group) {
144144
res <- do.call(rbind, lapply(unique(preds[[group]]), function(grp) {
145-
varCoef <- as.numeric(fit$modelStruct$varStruct[grp])
145+
i <- which(unique(preds[[group]]) == grp)
146+
varCoef <- as.numeric(fit$modelStruct$varStruct[i])
146147

147148
sub <- preds[preds[[group]] == grp, ]
148149
exes <- sub[[x]]
@@ -156,7 +157,7 @@ nlmePlot <- function(fit, form, df = NULL, groups = NULL, timeRange = NULL, face
156157
varSummary <- summary(fit$modelStruct$varStruct)
157158
coefs <- data.frame(
158159
x = 1 / unique(attr(varSummary, "weight")),
159-
g = unique(attr(varSummary, "groups"))
160+
g = unique(attr(varSummary, "groups")) %||% grp
160161
)
161162
out <- baseSigma * coefs[coefs$g == grp, "x"]
162163
}

R/pcvrss.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,6 @@ print.pcvrsssummary <- function(x, ...) {
108108
cat("\nData:\n")
109109
print(x$df[1:3, !grepl("dummyIndividual|dummyGroup", colnames(x$df))])
110110
cat(paste0("...\n"))
111-
cat(paste0("(", nrow(x$df), " rows)"))
111+
cat(paste0("(", nrow(x$df), " rows)\n"))
112112
return(invisible(x))
113113
}

R/stat_brms_model.R

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
#' Show brms model in ggplot layer
2+
#'
3+
#' @description
4+
#' Add posterior predictive distribution from a brms model fit with \link{growthSS} and \link{fitGrowth}
5+
#' to a ggplot object.
6+
#'
7+
#' @param mapping Set of aesthetic mappings created by \code{ggplot2::aes()}. If specified
8+
#' and ‘inherit.aes = TRUE’ (the default), it is combined with the default mapping at the
9+
#' top level of the plot. If there is no mapping then it is filled in by default using
10+
#' the \code{ss} object.
11+
#' @param data The data to be displayed in this layer. This behaves per normal ggplot2
12+
#' expectations except that if data is missing (ie, not inherited or specified) then the
13+
#' data from \code{ss} is used.
14+
#' @param fit A brmsfit object, typically returned from \code{fitGrowth}.
15+
#' @param ss A \code{pcvrss} object. Only the "pcvrForm" and "df" elements are used.
16+
#' @param CI A vector of Credible Intervals to plot, defaults to 0.95.
17+
#' @param inherit.aes Logical, should aesthetics be inherited from top level? Defaults to TRUE.
18+
#' @param ... Additional arguments passed to ggplot2::layer.
19+
#'
20+
#' @import ggplot2
21+
#' @rdname stat_growthss
22+
#' @keywords ggplot
23+
#' @export
24+
25+
stat_brms_model <- function(mapping = NULL, data = NULL,
26+
fit = NULL, ss = NULL, CI = 0.95,
27+
inherit.aes = TRUE, ...) {
28+
# These would normally be arguments to a stat layer but they should not be changed
29+
geom <- "ribbon"
30+
position <- "identity"
31+
na.rm <- FALSE
32+
show.legend <- c("fill" = TRUE, "alpha" = FALSE)
33+
parsed_form <- .parsePcvrForm(ss$pcvrForm, ss$df)
34+
stat <- statBrmsMod
35+
# pick stat based on whether or not the model is longitudinal
36+
if (!is.numeric(parsed_form$data[, parsed_form$x]) && !parsed_form$USEG && !parsed_form$USEID) {
37+
stat <- statBrmsStaticMod
38+
geom <- "rect"
39+
}
40+
# get elements to replace NULL defaults in case they are missing
41+
if (is.null(data) || is.null(mapping)) {
42+
data <- data %||% parsed_form$data
43+
mapping <- mapping %||% ggplot2::aes(x = .data[[parsed_form$x]])
44+
}
45+
# format credible intervals into a list of c(min, max) probs for predictions
46+
formatted_prob_list <- lapply(rev(sort(CI)), function(i) {
47+
return(
48+
c(((1 - i) / 2), (i + (1 - i) / 2))
49+
)
50+
})
51+
# make layer for each of the intervals
52+
layers <- lapply(formatted_prob_list, function(prob_pair) {
53+
lyr <- ggplot2::layer(
54+
stat = stat, data = data, mapping = mapping, geom = geom,
55+
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
56+
params = list(na.rm = na.rm, fit = fit, parsed_form = parsed_form, probs = prob_pair, ...)
57+
)
58+
return(lyr)
59+
})
60+
return(layers)
61+
}
62+
63+
64+
"%||%" <- function(a, b) {
65+
if (!is.null(a)) {
66+
return(a)
67+
} else {
68+
return(b)
69+
}
70+
}
71+
72+
statBrmsMod <- ggplot2::ggproto("StatBrm", Stat,
73+
# `specify that there will be extra params`
74+
extra_params = c("na.rm", "fit", "parsed_form", "probs"),
75+
# `data setup function`
76+
setup_data = function(data, params) {
77+
#' possible that ss is not a pcvrss object for compatibility with other brms models
78+
#' if "df" is part of it then work with that otherwise use general data.
79+
if ("data" %in% names(params$parsed_form)) {
80+
parsed_form <- params$parsed_form
81+
mod_data <- parsed_form$data
82+
mod_data <- mod_data[, unlist(parsed_form[c("x", "group")])]
83+
colnames(mod_data) <- c("x", "MOD_GROUP")
84+
mod_data <- mod_data[!duplicated(mod_data), ]
85+
data <- plyr::join(mod_data, data, type = "left", match = "all", by = "x")
86+
if (length(unique(data$PANEL)) > 1) {
87+
data <- data[data$PANEL == as.numeric(as.factor(data$MOD_GROUP)), ]
88+
}
89+
}
90+
return(data)
91+
},
92+
#' NOTE ggplot2:::Stat$compute_layer can use the default from ggproto
93+
#' `make plot within a given panel of the ggplot (a facet)`
94+
#' this is mostly the same as the default ggproto compute_panel function,
95+
#' but it takes more named args and passes them to compute_group and
96+
#' avoids warning about individual/time columns.
97+
compute_panel = function(self, data, scales, fit, parsed_form, probs, ...) {
98+
if (ggplot2:::empty(data)) return(ggplot2:::data_frame0())
99+
groups <- split(data, data[["MOD_GROUP"]])
100+
stats <- lapply(groups, function(groupdf) {
101+
d <- self$compute_group(
102+
data = groupdf, scales = scales,
103+
fit = fit, parsed_form = parsed_form, probs = probs,
104+
...
105+
)
106+
return(d)
107+
})
108+
non_constant_columns <- character(0)
109+
stats <- mapply(function(new, old) {
110+
if (ggplot2:::empty(new)) {
111+
return(ggplot2:::data_frame0())
112+
}
113+
old <- old[, !(names(old) %in% names(new)), drop = FALSE]
114+
non_constant <- vapply(old, vctrs::vec_unique_count, integer(1)) > 1L
115+
non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
116+
vc <- vctrs:::vec_cbind(
117+
new,
118+
old[rep(1, nrow(new)), , drop = FALSE]
119+
)
120+
return(vc)
121+
}, stats, groups, SIMPLIFY = FALSE)
122+
123+
non_constant_columns <- ggplot2:::unique0(non_constant_columns)
124+
dropped <- non_constant_columns[!non_constant_columns %in% c(
125+
self$dropped_aes, unlist(parsed_form[c("individual", "x")])
126+
)]
127+
128+
if (length(dropped) > 0) {
129+
warning(paste0(
130+
"The ", paste(dropped, collapse = ", "), " aesthetics were dropped,\n",
131+
" did you forget to specify a group aesthetic or convert a numerical variable into a factor?"
132+
))
133+
}
134+
data_new <- ggplot2:::vec_rbind0(!!!stats)
135+
return(
136+
data_new[, !names(data_new) %in% non_constant_columns, drop = FALSE]
137+
)
138+
},
139+
#' `make data out of model per a given aes-group, should only be 1 per panel`
140+
#' this is the heavily customized component which makes data for ribbons from
141+
#' the model and ss objects.
142+
compute_group = function(data, scales,
143+
fit = NULL, parsed_form = NULL, probs = NULL,
144+
...) {
145+
yvar <- parsed_form$y
146+
xvar <- parsed_form$x
147+
group <- parsed_form$group
148+
# make data to use drawing posterior predictions
149+
nd <- data[, c("x", "MOD_GROUP", "PANEL")]
150+
nd <- nd[!duplicated(nd), ]
151+
colnames(nd) <- c(xvar, group, "PANEL")
152+
# make predictions
153+
mod_data <- cbind(nd, predict(fit, newdata = nd, probs = probs))
154+
# lengthen predictions as in brmPlot
155+
longPreds <- do.call(rbind, lapply(seq_len(nrow(mod_data)), function(r) {
156+
sub <- mod_data[r, ]
157+
lp <- do.call(rbind, lapply(
158+
head(probs, floor(length(probs) / 2)) * 100,
159+
function(i) {
160+
min <- paste0("Q", i)
161+
max <- paste0("Q", 100 - i)
162+
iter <- sub[, c(xvar, group, "Estimate", "PANEL")]
163+
iter$q <- abs(((100 - i) - i))
164+
iter$min <- sub[[min]]
165+
iter$max <- sub[[max]]
166+
return(iter)
167+
}
168+
))
169+
return(lp)
170+
}))
171+
# select columns and rename
172+
grpdf <- longPreds[, c(xvar, group, "Estimate", "PANEL", "q", "min", "max")]
173+
colnames(grpdf) <- c("x", "MOD_GROUP", "y", "PANEL", "Cred.Int", "ymin", "ymax")
174+
return(grpdf)
175+
},
176+
# set defaults for several aesthetics, all have to be after stat is calculated
177+
default_aes = aes(
178+
ymin = after_stat(ymin),
179+
ymax = after_stat(ymax),
180+
fill = after_stat(Cred.Int),
181+
alpha = after_stat(0.5),
182+
group = after_stat(MOD_GROUP),
183+
x = after_stat(x)
184+
)
185+
)
186+
187+
statBrmsStaticMod <- ggplot2::ggproto("StatStaticBrm", Stat,
188+
# `specify that there will be extra params`
189+
extra_params = c("na.rm", "fit", "parsed_form", "probs"),
190+
# `data setup function`
191+
setup_data = function(data, params) {
192+
if ("data" %in% names(params$parsed_form)) {
193+
parsed_form <- params$parsed_form
194+
mod_data <- parsed_form$data
195+
mod_data <- mod_data[, unlist(parsed_form[c("x", "group")])]
196+
colnames(mod_data) <- c("x", "MOD_GROUP")
197+
mod_data <- mod_data[!duplicated(mod_data), ]
198+
data <- plyr::join(mod_data, data, type = "left", match = "all", by = "x")
199+
if (length(unique(data$PANEL)) > 1) {
200+
data <- data[data$PANEL == as.numeric(as.factor(data$MOD_GROUP)), ]
201+
}
202+
}
203+
return(data)
204+
},
205+
compute_panel = function(self, data, scales, fit, parsed_form, probs, ...) {
206+
if (ggplot2:::empty(data)) return(ggplot2:::data_frame0())
207+
groups <- split(data, data[["MOD_GROUP"]])
208+
stats <- lapply(groups, function(groupdf) {
209+
d <- self$compute_group(
210+
data = groupdf, scales = scales,
211+
fit = fit, parsed_form = parsed_form, probs = probs,
212+
...
213+
)
214+
return(d)
215+
})
216+
non_constant_columns <- character(0)
217+
stats <- mapply(function(new, old) {
218+
if (ggplot2:::empty(new)) {
219+
return(ggplot2:::data_frame0())
220+
}
221+
old <- old[, !(names(old) %in% names(new)), drop = FALSE]
222+
non_constant <- vapply(old, vctrs::vec_unique_count, integer(1)) > 1L
223+
non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
224+
vc <- vctrs:::vec_cbind(
225+
new,
226+
old[rep(1, nrow(new)), , drop = FALSE]
227+
)
228+
return(vc)
229+
}, stats, groups, SIMPLIFY = FALSE)
230+
231+
non_constant_columns <- ggplot2:::unique0(non_constant_columns)
232+
dropped <- non_constant_columns[!non_constant_columns %in% c(
233+
self$dropped_aes, unlist(parsed_form[c("individual", "x")])
234+
)]
235+
if (length(dropped) > 0) {
236+
warning(paste0(
237+
"The ", paste(dropped, collapse = ", "), " aesthetics were dropped,\n",
238+
" did you forget to specify a group aesthetic or convert a numerical variable into a factor?"
239+
))
240+
}
241+
data_new <- ggplot2:::vec_rbind0(!!!stats)
242+
return(
243+
data_new[, !names(data_new) %in% non_constant_columns, drop = FALSE]
244+
)
245+
},
246+
compute_group = function(data, scales,
247+
fit = NULL, parsed_form = NULL, probs = NULL,
248+
...) {
249+
yvar <- parsed_form$y
250+
xvar <- parsed_form$x
251+
group <- parsed_form$group
252+
nd <- data[, c("x", "MOD_GROUP", "PANEL")]
253+
nd <- nd[!duplicated(nd), ]
254+
colnames(nd) <- c(xvar, group, "PANEL")
255+
# make predictions
256+
mod_data <- cbind(nd, predict(fit, newdata = nd, probs = probs))
257+
# lengthen predictions as in brmPlot
258+
longPreds <- do.call(rbind, lapply(seq_len(nrow(mod_data)), function(r) {
259+
sub <- mod_data[r, ]
260+
lp <- do.call(rbind, lapply(
261+
head(probs, floor(length(probs) / 2)) * 100,
262+
function(i) {
263+
min <- paste0("Q", i)
264+
max <- paste0("Q", 100 - i)
265+
iter <- sub[, c(xvar, group, "Estimate", "PANEL")]
266+
iter$q <- abs(((100 - i) - i))
267+
iter$ymin <- sub[[min]]
268+
iter$ymax <- sub[[max]]
269+
return(iter)
270+
}
271+
))
272+
return(lp)
273+
}))
274+
longPreds$numericGroup <- as.numeric(as.factor(longPreds[[xvar]]))
275+
longPreds$xmin <- longPreds$numericGroup - c(0.45 * (1 - longPreds$q))
276+
longPreds$xmax <- longPreds$numericGroup + c(0.45 * (1 - longPreds$q))
277+
278+
# select columns and rename
279+
grpdf <- longPreds[, c(group, "Estimate", "PANEL", "q", "ymin", "ymax", "xmin", "xmax")]
280+
colnames(grpdf) <- c("MOD_GROUP", "y", "PANEL", "Cred.Int", "ymin", "ymax", "xmin", "xmax")
281+
return(grpdf)
282+
},
283+
# set defaults for several aesthetics, all have to be after stat is calculated
284+
default_aes = aes(
285+
ymin = after_stat(ymin),
286+
ymax = after_stat(ymax),
287+
xmin = after_stat(xmin),
288+
xmax = after_stat(xmax),
289+
fill = after_stat(Cred.Int),
290+
alpha = after_stat(0.5),
291+
group = after_stat(x)
292+
)
293+
)

0 commit comments

Comments
 (0)