Skip to content

Commit 34b08d3

Browse files
authored
Merge pull request #68 from UCD-SERG/Kwan_2025
Modeling Individual-Level Antibody Response Curves Using Bayesian Inference and Assessing Model Fit via Residual Analysis
2 parents 19949ad + 874e094 commit 34b08d3

23 files changed

+5230
-316
lines changed

.Rbuildignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
^pkgdown$
1212
^\.github$
1313
^LICENSE\.md$
14+
^serodynamics\.Rcheck$
15+
^serodynamics.*\.tar\.gz$
16+
^serodynamics.*\.tgz$
1417
^inst/extdata/sees_case_data_long_11012023\.rds$
1518
^inst/extdata/elisa_clean_2023-11-01\.csv$
1619
^\.lintr$
@@ -22,3 +25,4 @@
2225
^_extensions$
2326
^vignettes/\.quarto$
2427
^vignettes/*_files$
28+

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ project*.tar.gz
1111
project*.tgz
1212
elisa_clean_2023-11-01.csv
1313
cholera_data_compiled_050324.csv
14+
inst/extdata/*.csv
15+
*.xlsx
16+
*.xls
17+
docs
18+
serodynamics.Rcheck/
19+
serodynamics*.tar.gz
20+
serodynamics*.tgz
21+
1422
inst/doc
1523
docs
1624
**/.quarto/
25+

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: serodynamics
22
Title: What the Package Does (One Line, Title Case)
3-
Version: 0.0.0.9034
3+
Version: 0.0.0.9035
44
Authors@R: c(
55
person("Peter", "Teunis", , "p.teunis@emory.edu", role = c("aut", "cph"),
66
comment = "Author of the method and original code."),
@@ -26,7 +26,7 @@ Imports:
2626
rlang,
2727
runjags,
2828
scales,
29-
serocalculator,
29+
serocalculator (>= 1.3.0.9037),
3030
stats,
3131
tibble,
3232
tidyr,
@@ -37,6 +37,7 @@ Suggests:
3737
readr,
3838
rlist,
3939
spelling,
40+
stringr,
4041
testthat (>= 3.0.0),
4142
tidyverse,
4243
withr,
@@ -45,7 +46,6 @@ Suggests:
4546
here,
4647
rjags,
4748
rmarkdown,
48-
stringr,
4949
magrittr,
5050
ssdtools,
5151
rex,

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export(plot_jags_Rhat)
1111
export(plot_jags_dens)
1212
export(plot_jags_effect)
1313
export(plot_jags_trace)
14+
export(plot_predicted_curve)
1415
export(post_summ)
1516
export(postprocess_jags_output)
1617
export(prep_data)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## New features
66

7+
* Added `plot_predicted_curve()` (#68)
78
* Replacing old data object with new run_mod output (#102)
89
* Adding class assignment to run_mod output (#76)
910
* Making prep_priors modifiable (#78)

R/plot_predicted_curve.R

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
#' @title Generate Predicted Antibody Response Curves (Median + 95% CI)
2+
#' @description
3+
#' Plots a median antibody response curve with a 95% credible interval
4+
#' ribbon, using MCMC samples from the posterior distribution.
5+
#' Optionally overlays observed data,
6+
#' applies logarithmic spacing on the y- and x-axes,
7+
#' and shows all individual
8+
#' sampled curves.
9+
#'
10+
#' @param sr_model An `sr_model` object (returned by [run_mod()]) containing
11+
#' samples from the posterior distribution of the model parameters.
12+
#' @param id The participant ID to plot; for example, "sees_npl_128".
13+
#' @param antigen_iso The antigen isotype to plot; for example, "HlyE_IgA" or
14+
#' "HlyE_IgG".
15+
#' @param dataset (Optional) A [dplyr::tbl_df] with observed antibody response
16+
#' data.
17+
#' Must contain:
18+
#' - `timeindays`
19+
#' - `value`
20+
#' - `id`
21+
#' - `antigen_iso`
22+
#' @param legend_obs Label for observed data in the legend.
23+
#' @param legend_median Label for the median prediction line.
24+
#' @param show_quantiles [logical]; if [TRUE] (default), plots the 2.5%, 50%,
25+
#' and 97.5% quantiles.
26+
#' @param log_y [logical]; if [TRUE], applies a [log10] transformation to
27+
#' the y-axis.
28+
#' @param log_x [logical]; if [TRUE], applies a [log10] transformation to the
29+
#' x-axis.
30+
#' @param show_all_curves [logical]; if [TRUE], overlays all
31+
#' individual sampled curves.
32+
#' @param alpha_samples Numeric; transparency level for individual
33+
#' curves (default = 0.3).
34+
#' @param xlim (Optional) A numeric vector of length 2 providing custom x-axis
35+
#' limits.
36+
#' @param ylab (Optional) A string for the y-axis label. If `NULL` (default),
37+
#' the label is automatically set to "ELISA units" or "ELISA units (log scale)"
38+
#' based on the `log_y` argument.
39+
#'
40+
#' @return A [ggplot2::ggplot] object displaying predicted antibody response
41+
#' curves with a median curve and a 95% credible interval band as default.
42+
#' @export
43+
#'
44+
#' @example inst/examples/examples-plot_predicted_curve.R
45+
plot_predicted_curve <- function(sr_model,
46+
id,
47+
antigen_iso,
48+
dataset = NULL,
49+
legend_obs = "Observed data",
50+
legend_median = "Median prediction",
51+
show_quantiles = TRUE,
52+
log_y = FALSE,
53+
log_x = FALSE,
54+
show_all_curves = FALSE,
55+
alpha_samples = 0.3,
56+
xlim = NULL,
57+
ylab = NULL) {
58+
59+
# --------------------------------------------------------------------------
60+
# 1) The 'sr_model' object is now the tibble itself
61+
df <- sr_model
62+
63+
64+
# --------------------------------------------------------------------------
65+
# 2) Filter to the subject & antigen of interest:
66+
df_sub <- df |>
67+
dplyr::filter(
68+
.data$Subject == id, # e.g. "sees_npl_128"
69+
.data$Iso_type == antigen_iso # e.g. "HlyE_IgA"
70+
)
71+
72+
# --------------------------------------------------------------------------
73+
# 3) Pivot to wide format: one row per iteration/chain
74+
param_medians_wide <- df_sub |>
75+
dplyr::select(
76+
all_of(c("Chain",
77+
"Iteration",
78+
"Iso_type",
79+
"Parameter",
80+
"value"))
81+
) |>
82+
tidyr::pivot_wider(
83+
names_from = c("Parameter"),
84+
values_from = c("value")
85+
) |>
86+
dplyr::arrange(.data$Chain, .data$Iteration) |>
87+
88+
dplyr::mutate(
89+
antigen_iso = factor(.data$Iso_type),
90+
r = .data$shape
91+
) |>
92+
dplyr::select(-c("Iso_type"))
93+
94+
# Add sample_id if not present (to identify individual samples)
95+
if (!"sample_id" %in% names(param_medians_wide)) {
96+
param_medians_wide <- param_medians_wide |>
97+
dplyr::mutate(sample_id = dplyr::row_number())
98+
}
99+
# Define time points for prediction
100+
tx2 <- seq(0, 1200, by = 5)
101+
102+
103+
## --- Prepare data for Model 1 ---
104+
dt1 <- data.frame(t = tx2) |>
105+
dplyr::mutate(id = dplyr::row_number()) |>
106+
tidyr::pivot_wider(names_from = "id",
107+
values_from = "t",
108+
names_prefix = "time") |>
109+
dplyr::slice(
110+
rep(seq_len(dplyr::n()), each = nrow(param_medians_wide))
111+
)
112+
113+
114+
serocourse_all1 <- cbind(param_medians_wide, dt1) |>
115+
tidyr::pivot_longer(cols = dplyr::starts_with("time"), values_to = "t") |>
116+
dplyr::select(-c("name")) |>
117+
dplyr::rowwise() |>
118+
dplyr::mutate(res = ab(.data$t,
119+
.data$y0,
120+
.data$y1,
121+
.data$t1,
122+
.data$alpha,
123+
.data$shape)) |>
124+
dplyr::ungroup()
125+
126+
# Determine Y-axis label
127+
if (is.null(ylab)) {
128+
if (log_y) {
129+
ylab <- "ELISA units (log scale)"
130+
} else {
131+
ylab <- "ELISA units"
132+
}
133+
}
134+
135+
# Base ggplot object with legend at the bottom.
136+
p <- ggplot2::ggplot() +
137+
ggplot2::theme_minimal() +
138+
ggplot2::labs(x = "Days since fever onset", y = ylab) +
139+
ggplot2::theme(legend.position = "bottom")
140+
141+
# If show_all_curves is TRUE, overlay all individual sampled curves.
142+
if (show_all_curves) {
143+
p <- p +
144+
ggplot2::geom_line(data = serocourse_all1,
145+
ggplot2::aes(x = .data$t,
146+
y = .data$res,
147+
group = .data$sample_id,
148+
color = "samples"),
149+
alpha = alpha_samples)
150+
}
151+
152+
# --- Summarize & Plot Model 1 (Median + 95% Ribbon) ---
153+
if (show_quantiles) {
154+
sum1 <- serocourse_all1 |>
155+
dplyr::group_by(t) |>
156+
dplyr::summarise(
157+
res.med = stats::quantile(.data$res, probs = 0.50, na.rm = TRUE),
158+
res.low = stats::quantile(.data$res, probs = 0.025, na.rm = TRUE),
159+
res.high = stats::quantile(.data$res, probs = 0.975, na.rm = TRUE),
160+
.groups = "drop"
161+
)
162+
163+
p <- p +
164+
ggplot2::geom_ribbon(data = sum1,
165+
ggplot2::aes(x = .data$t,
166+
ymin = .data$res.low,
167+
ymax = .data$res.high,
168+
fill = "ci"),
169+
alpha = 0.2, inherit.aes = FALSE) +
170+
ggplot2::geom_line(data = sum1,
171+
ggplot2::aes(x = .data$t,
172+
y = .data$res.med,
173+
color = "median"),
174+
linewidth = 1, inherit.aes = FALSE)
175+
}
176+
177+
# --- Overlay Observed Data (if provided) ---
178+
if (!is.null(dataset)) {
179+
observed_data <- dataset |>
180+
dplyr::rename(t = c("timeindays"),
181+
res = c("value")) |>
182+
dplyr::select(all_of(c("id",
183+
"t",
184+
"res",
185+
"antigen_iso"))) |>
186+
dplyr::mutate(id = as.factor(.data$id))
187+
188+
p <- p +
189+
ggplot2::geom_point(data = observed_data,
190+
ggplot2::aes(x = .data$t,
191+
y = .data$res,
192+
group = .data$id,
193+
color = "observed"),
194+
size = 2, show.legend = TRUE) +
195+
ggplot2::geom_line(data = observed_data,
196+
ggplot2::aes(x = .data$t,
197+
y = .data$res,
198+
group = .data$id,
199+
color = "observed"),
200+
linewidth = 1, show.legend = TRUE)
201+
}
202+
203+
# --- Construct Unified Legend ---
204+
color_vals <- c("median" = "red")
205+
color_labels <- c("median" = legend_median)
206+
fill_vals <- c("ci" = "red")
207+
fill_labels <- c("ci" = "95% credible interval")
208+
209+
if (show_all_curves) {
210+
color_vals["samples"] <- "gray"
211+
color_labels["samples"] <- "Posterior samples"
212+
}
213+
214+
if (!is.null(dataset)) {
215+
color_vals["observed"] <- "blue"
216+
color_labels["observed"] <- legend_obs
217+
}
218+
219+
p <- p +
220+
ggplot2::scale_color_manual(
221+
name = "Component",
222+
values = color_vals,
223+
labels = color_labels,
224+
guide = ggplot2::guide_legend(override.aes = list(shape = NA))
225+
) +
226+
ggplot2::scale_fill_manual(
227+
name = "Component",
228+
values = fill_vals,
229+
labels = fill_labels,
230+
guide = ggplot2::guide_legend(override.aes = list(color = NA))
231+
)
232+
233+
# --- Optionally add log10 scales for y and/or x ---
234+
if (log_y) {
235+
p <- p + ggplot2::scale_y_log10()
236+
}
237+
if (log_x) {
238+
p <- p +
239+
ggplot2::scale_x_continuous(
240+
trans = scales::pseudo_log_trans(sigma = 1, base = 10)
241+
)
242+
}
243+
244+
# --- Set custom x-axis limits if provided ---
245+
if (!is.null(xlim)) {
246+
p <- p + ggplot2::coord_cartesian(xlim = xlim)
247+
}
248+
249+
return(p)
250+
}

data-raw/nepal_sees_jags_output.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ nepal_sees_jags_output <- run_mod(
1414

1515
# Filtering to keep only 2 subjects + newperson
1616
nepal_sees_jags_output <- nepal_sees_jags_output |>
17-
filter(Subject %in% c("newperson", "sees_npl_1", "sees_npl_2"))
17+
filter(Subject %in% c("newperson", "sees_npl_1", "sees_npl_2", "sees_npl_128"))
1818

1919
usethis::use_data(nepal_sees_jags_output, overwrite = TRUE)

data-raw/v9na.resid.r

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
library(Hmisc);
2-
source("graph-func.r");
3-
source("minticks.r");
2+
source("data-raw/graph-func.r");
3+
source("data-raw/minticks.r");
44

55
file.pmc <- paste("./output/",ver,".mcmc",".rda",sep="");
66
file.pdf <- paste("./output/",ver,".graph",".pdf",sep="");

data/nepal_sees_jags_output.rda

32.7 KB
Binary file not shown.

inst/WORDLIST

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,11 @@ stratifications
3434
tbl
3535
unstratified
3636
wishdf
37+
dayssincefeveronset
38+
ggplot
39+
tibble
40+
HlyE
41+
IgA
42+
IgG
43+
newpage
44+
npl

0 commit comments

Comments
 (0)