Skip to content

Commit fda35a0

Browse files
authored
Merge pull request #8 from Bayer-Group/dev
Try to add pre-rendered images instead.
2 parents a1db377 + 56d9663 commit fda35a0

26 files changed

+1919
-49
lines changed

NAMESPACE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
S3method(calpha.test,fisher)
44
S3method(plot,StepDownRSCABS)
5+
S3method(plot,dunnett_test_result)
56
S3method(print,RSCABS)
67
S3method(print,StepDownRSCABS)
78
S3method(print,drcComp)
9+
S3method(print,dunnett_test_result)
810
S3method(print,stepDownTrendBinom)
911
S3method(print,tskresult)
1012
S3method(summary,StepDownRSCABS)
@@ -34,6 +36,7 @@ export(convert_fish_data)
3436
export(create_contingency_table)
3537
export(dose.p.glmmPQL)
3638
export(drcCompare)
39+
export(dunnett_test)
3740
export(expand_to_individual_simple)
3841
export(expand_to_individual_tidy)
3942
export(getEC50)
@@ -102,6 +105,9 @@ importFrom(drc,getMeanFunctions)
102105
importFrom(ggplot2,aes)
103106
importFrom(ggplot2,element_blank)
104107
importFrom(ggplot2,element_text)
108+
importFrom(ggplot2,geom_errorbar)
109+
importFrom(ggplot2,geom_hline)
110+
importFrom(ggplot2,geom_point)
105111
importFrom(ggplot2,geom_text)
106112
importFrom(ggplot2,geom_tile)
107113
importFrom(ggplot2,ggplot)
@@ -112,10 +118,14 @@ importFrom(ggplot2,theme)
112118
importFrom(ggplot2,theme_minimal)
113119
importFrom(graphics,text)
114120
importFrom(isotone,gpava)
121+
importFrom(lme4,lmer)
115122
importFrom(magrittr,"%>%")
116123
importFrom(metafor,rma.mh)
117124
importFrom(multcomp,glht)
118125
importFrom(multcomp,mcp)
126+
importFrom(nlme,gls)
127+
importFrom(nlme,lme)
128+
importFrom(nlme,varIdent)
119129
importFrom(purrr,map)
120130
importFrom(purrr,map2)
121131
importFrom(rlang,":=")

R/dunnett.R

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
#' Conduct Dunnett Test with Various Model Specifications
2+
#'
3+
#' This function performs Dunnett's test for comparing multiple treatment levels to a control
4+
#' using various model specifications, including options for random effects and variance structures.
5+
#'
6+
#' @param data A data frame containing the dose-response data
7+
#' Conduct Dunnett Test with Various Model Specifications
8+
#'
9+
#' This function performs Dunnett's test for comparing multiple treatment levels to a control
10+
#' using various model specifications, including options for random effects and variance structures.
11+
#'
12+
#' @param data A data frame containing the dose-response data
13+
#' @param response_var Name of the response variable column
14+
#' @param dose_var Name of the dose/treatment variable column
15+
#' @param block_var Name of the blocking/tank variable column (optional)
16+
#' @param control_level The level of dose_var to use as control (default is minimum dose)
17+
#' @param include_random_effect Logical, whether to include random effects for blocks/tanks
18+
#' @param variance_structure Character, specifying the variance structure:
19+
#' "homoscedastic" (default) or "heteroscedastic"
20+
#' @param alpha Significance level for determining NOEC (default = 0.05)
21+
#' @param conf_level Confidence level for intervals (default = 0.95)
22+
#' @param return_model Logical, whether to return the fitted model object (default = FALSE)
23+
#'
24+
#' @return A list containing the Dunnett test results, NOEC value, and optionally the model object
25+
#' @export
26+
#'
27+
#' @importFrom multcomp glht mcp
28+
#' @importFrom lme4 lmer
29+
#' @importFrom nlme gls lme varIdent
30+
#' @importFrom stats as.formula
31+
dunnett_test <- function(data,
32+
response_var = "Response",
33+
dose_var = "Dose",
34+
block_var = "Tank",
35+
control_level = NULL,
36+
include_random_effect = TRUE,
37+
variance_structure = c("homoscedastic", "heteroscedastic"),
38+
alpha = 0.05,
39+
conf_level = 0.95,
40+
return_model = FALSE) {
41+
42+
# Input validation
43+
if (!is.data.frame(data)) {
44+
stop("Data must be a data frame")
45+
}
46+
47+
if (!response_var %in% names(data)) {
48+
stop(paste("Response variable", response_var, "not found in data"))
49+
}
50+
51+
if (!dose_var %in% names(data)) {
52+
stop(paste("Dose/treatment variable", dose_var, "not found in data"))
53+
}
54+
55+
# Ensure dose variable is a factor
56+
if (!is.factor(data[[dose_var]])) {
57+
data[[dose_var]] <- factor(data[[dose_var]])
58+
}
59+
60+
# Set control level if not specified
61+
if (is.null(control_level)) {
62+
# Use the minimum dose level as control
63+
control_level <- levels(data[[dose_var]])[1]
64+
} else {
65+
# Ensure control_level is in the levels
66+
if (!as.character(control_level) %in% levels(data[[dose_var]])) {
67+
stop("Control level not found in dose variable levels")
68+
}
69+
}
70+
71+
# Match variance structure argument
72+
variance_structure <- match.arg(variance_structure)
73+
74+
# Check if block variable exists when random effects are requested
75+
if (include_random_effect && !block_var %in% names(data)) {
76+
stop(paste("Block/tank variable", block_var, "not found in data"))
77+
}
78+
79+
# Create formula strings
80+
fixed_formula_str <- paste(response_var, "~", dose_var)
81+
fixed_formula <- as.formula(fixed_formula_str)
82+
83+
# Fit the appropriate model based on specifications
84+
if (include_random_effect) {
85+
if (variance_structure == "homoscedastic") {
86+
# Mixed model with homoscedastic errors
87+
message("Fitting mixed model with homoscedastic errors")
88+
model <- lme4::lmer(
89+
as.formula(paste(fixed_formula_str, "+ (1|", block_var, ")")),
90+
data = data
91+
)
92+
} else {
93+
# Mixed model with heteroscedastic errors by dose level
94+
message("Fitting mixed model with heteroscedastic errors")
95+
model <- nlme::lme(
96+
fixed = fixed_formula,
97+
random = as.formula(paste("~ 1 |", block_var)),
98+
weights = nlme::varIdent(form = as.formula(paste("~ 1 |", dose_var))),
99+
data = data
100+
)
101+
}
102+
} else {
103+
if (variance_structure == "homoscedastic") {
104+
# Linear model with homoscedastic errors
105+
message("Fitting linear model with homoscedastic errors")
106+
model <- stats::lm(fixed_formula, data = data)
107+
} else {
108+
# GLS model with heteroscedastic errors by dose level
109+
message("Fitting GLS model with heteroscedastic errors")
110+
model <- nlme::gls(
111+
fixed_formula,
112+
weights = nlme::varIdent(form = as.formula(paste("~ 1 |", dose_var))),
113+
data = data
114+
)
115+
}
116+
}
117+
118+
# Create contrast for Dunnett test
119+
# This is the corrected part that properly handles variable names
120+
linfct <- NULL
121+
122+
if (inherits(model, "lmerMod") || inherits(model, "lm") || inherits(model, "lme")) {
123+
# For lmer,lme and lm models
124+
dunnett_args <- list(model)
125+
mc_formula <- paste(dose_var, "= 'Dunnett'")
126+
mc_call <- call("mcp")
127+
mc_call[[dose_var]] <- "Dunnett"
128+
129+
# Set control level if not the first level
130+
if (control_level != levels(data[[dose_var]])[1]) {
131+
mc_call$base <- which(levels(data[[dose_var]]) == as.character(control_level))
132+
}
133+
134+
dunnett_args$linfct <- mc_call
135+
dunnett_result <- do.call(multcomp::glht, dunnett_args)
136+
137+
} else if (inherits(model, "gls")) {
138+
# For nlme models (lme, gls)
139+
# Create a contrast matrix manually
140+
## browser()
141+
n_levels <- nlevels(data[[dose_var]])
142+
control_idx <- which(levels(data[[dose_var]]) == as.character(control_level))
143+
144+
# Create Dunnett contrast matrix
145+
K <- matrix(0, n_levels - 1, n_levels)
146+
row_idx <- 1
147+
for (i in 1:n_levels) {
148+
if (i != control_idx) {
149+
K[row_idx, i] <- 1 # Treatment level
150+
#K[row_idx, control_idx] <- -1 # Control level
151+
row_idx <- row_idx + 1
152+
}
153+
}
154+
155+
# Create row names for the contrast matrix
156+
level_names <- levels(data[[dose_var]])
157+
row_names <- character(n_levels - 1)
158+
row_idx <- 1
159+
for (i in 1:n_levels) {
160+
if (i != control_idx) {
161+
row_names[row_idx] <- paste(level_names[i], "-", level_names[control_idx])
162+
row_idx <- row_idx + 1
163+
}
164+
}
165+
rownames(K) <- row_names
166+
167+
# Create the contrast
168+
linfct <- multcomp::glht(model, linfct = K)
169+
dunnett_result <- linfct
170+
}
171+
172+
# Get test results
173+
dunnett_summary <- summary(dunnett_result, test = multcomp::adjusted("single-step"))
174+
dunnett_confint <- confint(dunnett_result, level = conf_level)
175+
176+
# Extract p-values and format comparison results
177+
p_values <- dunnett_summary$test$pvalues
178+
##browser()
179+
comparisons <- rownames(as.data.frame(dunnett_result$linfct))
180+
181+
# Create a data frame with results
182+
results_df <- data.frame(
183+
comparison = comparisons,
184+
estimate = dunnett_summary$test$coefficients,
185+
std.error = dunnett_summary$test$sigma,
186+
statistic = dunnett_summary$test$tstat,
187+
p.value = p_values,
188+
conf.low = dunnett_confint$confint[, "lwr"],
189+
conf.high = dunnett_confint$confint[, "upr"],
190+
significant = p_values < alpha
191+
)
192+
193+
# Determine NOEC (No Observed Effect Concentration)
194+
# Extract dose levels from comparison strings and convert to numeric
195+
dose_levels <- sapply(strsplit(comparisons, " - "), function(x) x[1])
196+
197+
# Convert to numeric if possible
198+
numeric_doses <- suppressWarnings(as.numeric(dose_levels))
199+
if (all(!is.na(numeric_doses))) {
200+
dose_levels <- numeric_doses
201+
}
202+
203+
# Find the highest dose with non-significant effect
204+
significant_effects <- p_values < alpha
205+
if (all(significant_effects)) {
206+
noec <- min(dose_levels) # All doses show effects, NOEC is below lowest dose
207+
noec_message <- "All tested doses show significant effects. NOEC is below the lowest tested dose."
208+
} else if (!any(significant_effects)) {
209+
noec <- max(dose_levels) # No doses show effects, NOEC is at or above highest dose
210+
noec_message <- "No significant effects detected at any dose. NOEC is at or above the highest tested dose."
211+
} else {
212+
# Find the highest non-significant dose
213+
non_sig_doses <- dose_levels[!significant_effects]
214+
sig_doses <- dose_levels[significant_effects]
215+
216+
# Ensure we're working with proper numeric values for comparison
217+
if (is.numeric(non_sig_doses) && is.numeric(sig_doses)) {
218+
noec <- max(non_sig_doses[non_sig_doses < max(sig_doses)])
219+
} else {
220+
# If doses aren't numeric, just return the highest non-significant level
221+
noec <- non_sig_doses[length(non_sig_doses)]
222+
}
223+
noec_message <- paste("NOEC determined as", noec)
224+
}
225+
226+
# Prepare return object
227+
result <- list(
228+
dunnett_test = dunnett_summary,
229+
results_table = results_df,
230+
noec = noec,
231+
noec_message = noec_message,
232+
model_type = paste0(
233+
ifelse(include_random_effect, "Mixed", "Fixed"),
234+
" model with ",
235+
variance_structure,
236+
" errors"
237+
),
238+
control_level = control_level,
239+
alpha = alpha
240+
)
241+
242+
if (return_model) {
243+
result$model <- model
244+
}
245+
246+
class(result) <- "dunnett_test_result"
247+
248+
return(result)
249+
}
250+
251+
#' Print method for dunnett_test_result objects
252+
#'
253+
#' @param x A dunnett_test_result object
254+
#' @param ... Additional arguments passed to print methods
255+
#'
256+
#' @export
257+
print.dunnett_test_result <- function(x, ...) {
258+
cat("Dunnett Test Results\n")
259+
cat("-------------------\n")
260+
cat("Model type:", x$model_type, "\n")
261+
cat("Control level:", x$control_level, "\n")
262+
cat("Alpha level:", x$alpha, "\n\n")
263+
264+
cat("Results Table:\n")
265+
print(x$results_table, row.names = FALSE)
266+
267+
cat("\nNOEC Determination:\n")
268+
cat(x$noec_message, "\n")
269+
}
270+
271+
#' Plot method for dunnett_test_result objects
272+
#'
273+
#' @param x A dunnett_test_result object
274+
#' @param ... Additional arguments passed to plot methods
275+
#'
276+
#' @importFrom ggplot2 ggplot aes geom_point geom_errorbar theme_minimal labs geom_hline
277+
#' @export
278+
plot.dunnett_test_result <- function(x, ...) {
279+
# Extract data for plotting
280+
plot_data <- x$results_table
281+
plot_data$comparison <- factor(plot_data$comparison,levels=plot_data$comparison)
282+
# Create the plot
283+
p <- ggplot2::ggplot(plot_data, ggplot2::aes(x = comparison, y = estimate, color = significant)) +
284+
ggplot2::geom_point(size = 3) +
285+
ggplot2::geom_errorbar(ggplot2::aes(ymin = conf.low, ymax = conf.high), width = 0.2) +
286+
ggplot2::geom_hline(yintercept = 0, linetype = "dashed") +
287+
ggplot2::theme_minimal() +
288+
ggplot2::labs(
289+
title = paste("Dunnett Test Results:", x$model_type),
290+
subtitle = paste("NOEC =", x$noec),
291+
x = "Comparison",
292+
y = "Difference from Control",
293+
color = "Significant"
294+
) +
295+
ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, hjust = 1))
296+
297+
return(p)
298+
}

README.Rmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,6 @@ knitr::knit("vignettes/drcHelper.Rmd.orig", output = "vignettes/drcHelper.Rmd",f
164164
```
165165
## Acknowledgements
166166

167-
The work is supported by Bayer Environment Effects team members, especially by Andreas Solga and Daniela Jans. The Mesocosm colleagues Sarah Baumert and Harald Schulz have supported the verification and validation with extensive examples and scripts and SAS / VB validated calculations.
167+
The work is supported by Bayer Environment Effects team members, especially by Andreas Solga and Daniela Jans. The Mesocosm colleagues Sarah Baumert and Harald Schulz have supported the verification and validation with extensive examples and scripts and SAS / VB validated calculations. Discussions with the Bayer RS-stats group, ecotox stats core group and members of the CLE stats group regarding current practices and statistical principles have been extremely helpful.
168168

169169

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,4 @@ The work is supported by Bayer Environment Effects team members,
248248
especially by Andreas Solga and Daniela Jans. The Mesocosm colleagues
249249
Sarah Baumert and Harald Schulz have supported the verification and
250250
validation with extensive examples and scripts and SAS / VB validated
251-
calculations.
251+
calculations.

0 commit comments

Comments
 (0)