I think I found the solution. My earlier attempts with posterior_linpred were thwarted by an unrelated error. Correlations are not broken by obtaining each linpred separately. Using the “transformed parameters” approach helped me double check results, but I do not recommend it in this specific case because it creates unnecessary large objects.
Comments are encouraged.
# Solution ========
## New fit with same data but explicitly getting draws of nonlinear
## parameters plus residual variance.
stan2get_la_scaf <- "
vector[N] nlp_Araw;
vector[N] nlp_Graw;
vector[N] nlp_Craw;
vector[N] nlp_lnBraw;
vector[N] nlp_Sraw;
vector[N] nlp_Mraw;
vector[N] nlp_c0;
// (you already have s_*/r_* computed above)
for (n in 1:N) {
// fixed–effects + group–effects for Araw
nlp_Araw[n] = X_Araw[n] * b_Araw
+ r_1_Araw_1[J_1[n]] * Z_1_Araw_1[n];
// Graw: population + spline + group
nlp_Graw[n] = X_Graw[n] * b_Graw
+ Xs_Graw[n] * bs_Graw
+ Zs_Graw_1_1[n] * s_Graw_1_1
+ r_2_Graw_1[J_2[n]] * Z_2_Graw_1[n];
// Craw
nlp_Craw[n] = X_Craw[n] * b_Craw
+ Xs_Craw[n] * bs_Craw
+ Zs_Craw_1_1[n] * s_Craw_1_1
+ r_3_Craw_1[J_3[n]] * Z_3_Craw_1[n];
// lnBraw
nlp_lnBraw[n] = X_lnBraw[n] * b_lnBraw
+ Xs_lnBraw[n] * bs_lnBraw
+ Zs_lnBraw_1_1[n] * s_lnBraw_1_1
+ r_4_lnBraw_1[J_4[n]] * Z_4_lnBraw_1[n];
// Sraw
nlp_Sraw[n] = X_Sraw[n] * b_Sraw
+ Xs_Sraw[n] * bs_Sraw
+ Zs_Sraw_1_1[n] * s_Sraw_1_1
+ r_5_Sraw_1[J_5[n]] * Z_5_Sraw_1[n];
// Mraw
nlp_Mraw[n] = X_Mraw[n] * b_Mraw
+ Xs_Mraw[n] * bs_Mraw
+ Zs_Mraw_1_1[n] * s_Mraw_1_1
+ r_6_Mraw_1[J_6[n]] * Z_6_Mraw_1[n];
// c0
nlp_c0[n] = X_c0[n] * b_c0;
}
"
bf_lgt_ppari_f <- bf(
lgt_ppari ~ logit(
inv_logit(Araw) * inv_logit(Graw) * inv_logit(Craw) *
exp(-exp(lnBraw) * exp(-c0 * inv_logit(Mraw) * inv_logit(Sraw) * adj_age))
),
Araw ~ 1 + (1|srv),
Graw ~ 1 + s(gdd, k = 5) + (1|srv),
Craw ~ 1 + s(chill28Feb, k = 5) + (1|srv),
lnBraw ~ 1 + s(m2_tree, k = 5) + (1|srv),
Sraw ~ 1 + s(shape, k = 5) + (1|srv),
Mraw ~ 1 + s(m2_tree, k = 5) + (1|srv),
c0 ~ 1,
nl = TRUE
)
priors_lgt_ppari_f <- c(
prior(normal(logit(0.99), 1), nlpar = "Araw"),
prior(normal(logit(0.98), 1), nlpar = "Graw"),
prior(normal(logit(0.98), 1), nlpar = "Craw"),
prior(normal(log(54), 1), nlpar = "lnBraw"),
prior(normal(logit(0.5), 1), nlpar = "Sraw"),
prior(normal(logit(0.5), 1), nlpar = "Mraw"),
prior(normal(1.35, 0.001), nlpar = "c0")
)
fit_lgt_ppari_f <- brm(
formula = bf_lgt_ppari_f,
data = ppari_data,
stanvars = stanvar(
scode = stan2get_la_scaf,
block = "tparameters",
position = "end"
),
family = gaussian(),
prior = priors_lgt_ppari_f,
init = "random",
control = list(adapt_delta = 0.99,
max_treedepth = 15),
chains = 4,
cores = 4,
iter = 4000,
warmup = 500,
backend = "cmdstanr",
threads = threading(5),
file = paste0(
proj_path,
"/models/fit_lgt_ppari_f.rds"
)
)
## Get posterior_predict ======
post_pred_lgt_ppari <- posterior_predict(
fit_lgt_ppari_f
) |>
unname()
## Reconstruct draws using posterior_linpred =====
## Note that each is obtained "independently" in different
## calls to posterior_linpred()
A_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "Araw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
G_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "Graw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
C_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "Craw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
B_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "lnBraw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
S_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "Sraw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
M_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "Mraw",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
c0_lin <- posterior_linpred(fit_lgt_ppari_f,
nlpar = "c0",
newdata = ppari_data,
re_formula = NULL,
transform = FALSE)
A_draws <- plogis(A_lin)
G_draws <- plogis(G_lin)
C_draws <- plogis(C_lin)
b_draws <- exp(B_lin)
S_draws <- plogis(S_lin)
M_draws <- plogis(M_lin)
c0_draws <- c0_lin
adj_age_mat <- matrix(
rep(ppari_data$adj_age, nrow(A_draws)),
nrow = nrow(A_draws),
byrow = TRUE
)
## A. Reconstruct draws of response using _lin and covariate from data
reconst_lgt_ppari_A <- qlogis(
A_draws *
G_draws *
C_draws *
exp(
-b_draws *
exp(-c0_draws * M_draws * S_draws * adj_age_mat)
)
)
## Summarize response draws for each observation
mu_lin_mean <- colMeans(reconst_lgt_ppari_A)
## Reconstruct draws using transformed parameter draws =====
## Note that these were obtained as vectors, explicitly
## preserving their joint distribution.
Araw <- fit_lgt_ppari_f |>
spread_draws(nlp_Araw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_Araw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
Craw <- fit_lgt_ppari_f |>
spread_draws(nlp_Craw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_Craw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
Graw <- fit_lgt_ppari_f |>
spread_draws(nlp_Graw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_Graw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
lnBraw <- fit_lgt_ppari_f |>
spread_draws(nlp_lnBraw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_lnBraw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
Sraw <- fit_lgt_ppari_f |>
spread_draws(nlp_Sraw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_Sraw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
Mraw <- fit_lgt_ppari_f |>
spread_draws(nlp_Mraw[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_Mraw
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
c0 <- fit_lgt_ppari_f |>
spread_draws(nlp_c0[obs]) |>
pivot_wider(
names_from = obs,
names_prefix = "obs",
values_from = nlp_c0
) |>
dplyr::select(starts_with("obs")) |>
as.matrix() |>
unname()
## B. Reconstruct draws of response using transformed parameter draws
## and covariate from data
reconst_lgt_ppari_B <- qlogis(
plogis(Araw) *
plogis(Graw) *
plogis(Craw) *
exp(
-exp(lnBraw) *
exp(
-c0 *
plogis(Mraw) *
plogis(Sraw) *
adj_age_mat
)
)
)
## Compare results =====
hist(post_pred_lgt_ppari - reconst_lgt_ppari_A, breaks = 200)
hist(post_pred_lgt_ppari - reconst_lgt_ppari_B, breaks = 200)
hist(reconst_lgt_ppari_A - reconst_lgt_ppari_B, breaks = 200)
# Reconstructions A and B seem to be the same down to "rounding error."
# Reconstructions differ from posterior predict due to normal residual with sigma = 0.39
# By using posterior_linpred for any of the parameters with the
# appropriate newdata I can get posterior_draws for any values of
# gdd, chill, m2tree and shape desired.
Reconstructions A and B areseem to be the same down to “rounding error.”
Reconstructions differ from posterior predict due to normal residual with sigma = 0.39
By using posterior_linpred for any of the parameters with the appropriate newdata I can get posterior_draws for any values of gdd, chill, m2tree and shape desired.