This analysis is part of the data processing pipeline for the Cylinder Test, a behavioral assessment used in the associated manuscript: Behavioural Differences Across Captivity Conditions in Triggerfish (Rhinecanthus aculeatus): Implications for Cognitive Testing.
The current notebook focuses on the following components of the behavioral dataset:
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.1
## ✔ purrr 1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
## Warning in checkDepPackageVersion(dep_pkg = "TMB"): Package version inconsistency detected.
## glmmTMB was built with TMB version 1.9.11
## Current TMB version is 1.9.14
## Please re-install glmmTMB from source or restore original 'TMB' package (see '?reinstalling' for more information)
## Loading required package: Matrix
##
## Attaching package: 'Matrix'
##
## The following objects are masked from 'package:tidyr':
##
## expand, pack, unpack
##
##
## Attaching package: 'lmerTest'
##
## The following object is masked from 'package:lme4':
##
## lmer
##
## The following object is masked from 'package:stats':
##
## step
##
## This is DHARMa 0.4.6. For overview type '?DHARMa'. For recent changes, type news(package = 'DHARMa')
##
## Attaching package: 'nlme'
##
## The following object is masked from 'package:lme4':
##
## lmList
##
## The following object is masked from 'package:dplyr':
##
## collapse
##
## Welcome to emmeans.
## Caution: You lose important information if you filter this package's results.
## See '? untidy'
# Load the Excel file
setwd("/Users/user/projects/CaptiveCognition/DetourTest")
data_P1 <- read_excel("processed_detourtest_data_annotated.xlsx", sheet = "phase_1")
data_T <- read_excel("processed_detourtest_data_annotated.xlsx", sheet = "test")
First we tested how many fish passed the first Phase, and ate within the 5 minute trial time. We found no statistical significant difference in the number of fish that finished.
Fish used in the experiment
fish_by_group <- data_P1 %>%
dplyr::select(fish_id, location) %>%
distinct() %>%
arrange(location, fish_id) %>%
group_by(location) %>%
summarise(Fish_IDs = paste(sort(unique(fish_id)), collapse = ", ")) %>%
pivot_wider(names_from = location, values_from = Fish_IDs)
print(fish_by_group)
## # A tibble: 1 × 2
## LIRS OXFORD
## <chr> <chr>
## 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 46, 49, 53, 54, 56, 59, 60, 61, 62, 63, 64, 65,…
Extract pass rate per individual.
# Count replicates per fish
replicate_counts <- data_P1 %>%
group_by(fish_id) %>%
summarise(n_replicates = n())
print(replicate_counts)
## # A tibble: 23 × 2
## fish_id n_replicates
## <dbl> <int>
## 1 1 6
## 2 2 6
## 3 3 6
## 4 4 6
## 5 5 6
## 6 6 5
## 7 7 6
## 8 8 6
## 9 9 6
## 10 10 6
## # ℹ 13 more rows
# Summarize pass/fail per fish across all trials
fish_pass_status <- data_P1 %>%
group_by(location, fish_id) %>%
summarise(passed = any(test_result == "pass"), .groups = "drop") %>%
mutate(pass_status = ifelse(passed, "Pass", "Fail"))
# Count number of pass/fail fish per location
fish_pass_summary <- fish_pass_status %>%
count(location, pass_status) %>%
pivot_wider(names_from = pass_status, values_from = n, values_fill = 0)
# Get fish IDs that failed
fish_pass_status %>%
filter(pass_status == "Fail")
## # A tibble: 4 × 4
## location fish_id passed pass_status
## <chr> <dbl> <lgl> <chr>
## 1 OXFORD 46 FALSE Fail
## 2 OXFORD 56 FALSE Fail
## 3 OXFORD 59 FALSE Fail
## 4 OXFORD 61 FALSE Fail
# View the result
fish_pass_summary
## # A tibble: 2 × 3
## location Pass Fail
## <chr> <int> <int>
## 1 LIRS 10 0
## 2 OXFORD 9 4
A Fisher’s Exact Test is used to test if the proportion of pass vs fail differs by location.
# Build 2x2 matrix for test
fisher_matrix <- fish_pass_summary %>%
column_to_rownames("location") %>%
as.matrix()
fisher.test(fisher_matrix)
##
## Fisher's Exact Test for Count Data
##
## data: fisher_matrix
## p-value = 0.1045
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.5550236 Inf
## sample estimates:
## odds ratio
## Inf
We found no statistical difference between the pass and failure rates by location (p = 0.10).
We first remove trials where fish did not eat the food within the trial time limit.
# Filter out rows where test_result == 'fail'
filtered_data <- data_P1 %>%
filter(test_result != 'fail')
filtered_data %>%
dplyr::group_by(location) %>%
dplyr::summarise(fish_ids = paste(sort(unique(fish_id)), collapse = ", "),
n_fish = n_distinct(fish_id))
## # A tibble: 2 × 3
## location fish_ids n_fish
## <chr> <chr> <int>
## 1 LIRS 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 10
## 2 OXFORD 49, 53, 54, 60, 62, 63, 64, 65, 66 9
This plot shows that, like a lot of time data, our results are skewed to the right.
ggplot(filtered_data, aes(x = trial_duration, fill = location)) +
geom_histogram(position = "stack", bins = 15, color = "black", alpha = 0.8) +
scale_fill_manual(values = c("OXFORD" = "#002147", "LIRS" = "#C44E52")) +
labs(
title = "Stacked Histogram of Trial Time by Fish Group",
x = "Trial time",
y = "Frequency",
fill = "Fish Group") +
theme_minimal(base_size = 14) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "top",
axis.text = element_text(size = 12))
# Violin plot by Fish ID (flipped)
p_fish <- ggplot(filtered_data, aes(x = factor(fish_id), y = trial_duration)) +
geom_violin(fill = "#69b3a2", alpha = 0.7, scale = "width", trim = FALSE) +
geom_jitter(width = 0.1, alpha = 0.5, color = "black", size = 1) +
coord_flip() +
labs(title = "Variance in Trial Duration by Fish",
x = "Fish ID",
y = "Trial Duration (s)") +
theme_minimal()
# Violin plot by Location (flipped)
p_location <- ggplot(filtered_data, aes(x = location, y = trial_duration)) +
geom_violin(fill = "#ffa07a", alpha = 0.7, scale = "width", trim = FALSE) +
geom_jitter(width = 0.1, alpha = 0.5, color = "black", size = 1) +
coord_flip() +
labs(title = "Variance in Trial Duration by Location",
x = "Location",
y = "Trial Duration (s)") +
theme_minimal()
# Combine plots in a vertical panel
p_fish | p_location
ggplot(filtered_data, aes(x = replicate_num, y = trial_duration, group = as.factor(fish_id), color = as.factor(fish_id))) +
geom_line(alpha = 0.6) +
geom_point() +
facet_wrap(~location) +
labs(x = "Replicate Number", y = "Trial Duration (s)",
color = "Fish ID",
title = "Trial Duration Across Replicates by Fish and Location") +
theme_minimal()
Skewed data can be difficult to fit with linear models. We will first try to transform the data using Log, Square Root, and Cube Root transformations.
# Apply transformations
log_offset<-0.0001
transformed_data <- filtered_data %>%
mutate(
trial_duration_log = log(trial_duration + log_offset), # log(x + 0.01) to avoid log(0)
trial_duration_sqrt = sqrt(trial_duration),
trial_duration_cube = trial_duration^(1/3))
# Plot original
p1 <- ggplot(transformed_data, aes(x = trial_duration)) +
geom_histogram(bins = 30, fill = "#69b3a2") +
labs(title = "Original", x = "Trial Duration", y = "Count") +
theme_minimal()
# Plot log-transformed
p2 <- ggplot(transformed_data, aes(x = trial_duration_log)) +
geom_histogram(bins = 30, fill = "#404080") +
labs(title = "Log(x + 0.001) Transformed", x = "Log Trial Duration", y = "Count") +
theme_minimal()
# Plot sqrt-transformed
p3 <- ggplot(transformed_data, aes(x = trial_duration_sqrt)) +
geom_histogram(bins = 30, fill = "#ffa07a") +
labs(title = "Sqrt(x) Transformed", x = "Sqrt Trial Duration", y = "Count") +
theme_minimal()
# Plot cube root-transformed
p4 <- ggplot(transformed_data, aes(x = trial_duration_cube)) +
geom_histogram(bins = 30, fill = "#8b0000") +
labs(title = "Cube Root Transformed", x = "Cube Root Trial Duration", y = "Count") +
theme_minimal()
# Combine plots
(p1 | p2) / (p3 | p4)
Although not perfect, the log transformation does the best job. We will
try to use this transformation when trying to fit a model to the
data.
We aim to determine whether trial duration differs between locations, while accounting for individual variation among fish. Since trial duration is continuous, positive, and slightly skewed, we tested multiple model types.
Although we explored alternative distributions (e.g., Inverse Gaussian, Poisson, Negative Binomial), these did not fit the data well. Below are the top-performing models based on theoretical justification and fit.
# Model with raw trial duration
# - Assumes normally distributed residuals
model_basic <- glmmTMB(trial_duration ~ location + (1 | fish_id), data = filtered_data)
# -- Gamma Distribution Models --
# Gamma is suited for positive, skewed data; log link ensures positive predictions
# Basic model with Gamma
model_gamma <- glmmTMB(trial_duration ~ location + (1 | fish_id),
family = Gamma(link = "log"), data = filtered_data)
# Gamma model with fish-specific dispersion (variance allowed to vary by individual)
model_var_disp_fish_gam <- glmmTMB(trial_duration ~ location + (1 | fish_id),
dispformula = ~ fish_id, # models heteroscedasticity
family = Gamma(link = "log"), data = filtered_data)
# Gamma model with location-specific dispersion (variance allowed to vary by location)
model_var_disp_location_gam <- glmmTMB(trial_duration ~ location + (1 | fish_id),
dispformula = ~ location, # models heteroscedasticity
family = Gamma(link = "log"), data = filtered_data)
# -- Log Transformed Models (log(x+0.01)) --
# Basic model with log-transformed data
model_log <- glmmTMB(trial_duration_log ~ location + (1 | fish_id), data = transformed_data)
# Fish-specific Intercepts and Location Slopes (Uncorrelated)
model_random_slopes_log <- glmmTMB(trial_duration_log ~ location + (1 + location || fish_id), data = transformed_data)
# Basic model with log-transformed data
model_replicates_log <- glmmTMB(trial_duration_log ~ location + (1 | fish_id) + (1|replicate_num),
data = transformed_data)
model_var_disp_replicates_log <- glmmTMB(trial_duration_log ~ location + (1 + location || fish_id),
dispformula = ~ replicate_num,
family = gaussian(),
data = transformed_data)
model_replicate_slopes_log <- glmmTMB(trial_duration_log ~ location * replicate_num +
(replicate_num | fish_id), # random intercepts + slopes per fish
dispformula = ~ replicate_num,
data = transformed_data, family = gaussian())
# Model with fish-specific dispersion
model_var_disp_fish_log <- glmmTMB(trial_duration_log ~ location + (1 | fish_id),
dispformula = ~ fish_id, # models heteroscedasticity
family = gaussian(), data = transformed_data)
# Model with location-specific dispersion
model_var_disp_location_log <- glmmTMB(trial_duration_log ~ location + (1 | fish_id),
dispformula = ~ location, # models heteroscedasticity
family= gaussian(), data = transformed_data)
# Note that we did try more complicated log models with dispformula and replicate_num as a random effect BUT they had model Warnings and still did not pass the later DHARMa test.
# Compare model fit using AIC
AIC(model_basic, model_gamma, model_var_disp_fish_gam, model_var_disp_location_gam,
model_log, model_random_slopes_log, model_replicates_log, model_var_disp_replicates_log, model_replicate_slopes_log, model_var_disp_fish_log, model_var_disp_location_log)
## df AIC
## model_basic 4 827.3790
## model_gamma 4 450.2298
## model_var_disp_fish_gam 5 452.1830
## model_var_disp_location_gam 5 452.2169
## model_log 4 232.9429
## model_random_slopes_log 5 234.5998
## model_replicates_log 5 228.9943
## model_var_disp_replicates_log 6 229.1171
## model_replicate_slopes_log 9 222.6642
## model_var_disp_fish_log 5 234.9110
## model_var_disp_location_log 5 234.9428
We have four models with very similar AIC values, and all use the log-transformed data.
We will test all competing models.
# model_log
simulation_output <- simulateResiduals(fittedModel = model_replicates_log, n = 1000, plot=TRUE) # Simulate residuals
# model_var_disp_fish_log
simulation_output2 <- simulateResiduals(fittedModel = model_var_disp_replicates_log, n = 1000, plot=TRUE)# Simulate residuals
# model_var_disp_location_log
simulation_output3 <- simulateResiduals(fittedModel = model_replicate_slopes_log, n = 1000, plot=TRUE)# Simulate residuals
None of the models pass the DHARMa tests.
We know from early data explorations that some fish appear to have very different mean trial times. We will try identifying fish with significantly different trial times and grouping the dispersion by fish with lower and higher mean times.
fish_variance <- transformed_data %>%
group_by(fish_id) %>%
summarise(
n_trials = n(),
mean_duration = mean(trial_duration_log, na.rm = TRUE),
sd_duration = sd(trial_duration_log, na.rm = TRUE),
var_duration = var(trial_duration_log, na.rm = TRUE)
) %>%
arrange(desc(var_duration))
# Use same table, define a threshold (e.g., > Q3 + 1.5*IQR)
iqr_mean <- IQR(fish_variance$mean_duration)
q3_mean <- quantile(fish_variance$mean_duration, 0.75)
mean_cutoff <- q3_mean + 1.5 * iqr_mean
fish_variance <- fish_variance %>%
mutate(high_mean = mean_duration > mean_cutoff)
fish_variance %>% filter(high_mean == TRUE)
## # A tibble: 2 × 6
## fish_id n_trials mean_duration sd_duration var_duration high_mean
## <dbl> <int> <dbl> <dbl> <dbl> <lgl>
## 1 7 6 2.48 0.889 0.790 TRUE
## 2 64 6 3.73 0.628 0.395 TRUE
# Join high_mean grouping info to the main data
# This labels each fish as either "high_mean" or "normal" based on mean trial duration
transformed_data <- transformed_data %>%
left_join(
fish_variance %>% dplyr::select(fish_id, high_mean),
by = "fish_id"
) %>%
mutate(mean_group = ifelse(high_mean == TRUE, "high_mean", "normal"))
It looks like two fish have a significantly higher mean. Instead of treating all fish equally in the random effects structure, we explore whether grouping fish by mean behavior improves model performance.
# Model 1: Include individual fish and replicate number as random intercepts
# - Allow the residual variance to vary depending on whether the fish is in the "high_mean" or "normal" group
model_disp_mean_group <- glmmTMB(trial_duration_log ~ location + (1 | fish_id) + (1 | replicate_num),
dispformula = ~ mean_group,
family = gaussian(),
data = transformed_data)
# Model 2: Use mean_group (not individual fish) as the random intercept
# - Residual variance is allowed to differ by location instead of mean_group
model_disp_mean_group2 <- glmmTMB(trial_duration_log ~ location + (1 | mean_group) + (1 | replicate_num),
dispformula = ~ mean_group,
family = gaussian(),
data = transformed_data)
AIC(model_replicate_slopes_log, model_disp_mean_group,model_disp_mean_group2)
## df AIC
## model_replicate_slopes_log 9 222.6642
## model_disp_mean_group 6 220.7649
## model_disp_mean_group2 6 227.7122
simulateResiduals(model_disp_mean_group, plot = TRUE)
## Object of Class DHARMa with simulated residuals based on 250 simulations with refit = FALSE . See ?DHARMa::simulateResiduals for help.
##
## Scaled residual values: 0.416 0.296 0.58 0.284 0.352 0.464 0.668 0.54 0.556 0.476 0.62 0.608 0.248 0.312 0.216 0.164 0.184 0.22 0.184 0.132 ...
simulateResiduals(model_disp_mean_group2, plot = TRUE)
## Object of Class DHARMa with simulated residuals based on 250 simulations with refit = FALSE . See ?DHARMa::simulateResiduals for help.
##
## Scaled residual values: 0.184 0.148 0.288 0.128 0.172 0.232 0.312 0.264 0.24 0.244 0.312 0.3 0.124 0.148 0.12 0.096 0.104 0.148 0.092 0.08 ...
Of all the models we tested, model_disp_mean_group and model_replicate_slopes_log have the lowest AIC values. We will go with model_replicate_slopes_log model.
# Model used
model_replicate_slopes_log <- glmmTMB(trial_duration_log ~ location * replicate_num +
(replicate_num | fish_id), # random intercepts + slopes per fish
dispformula = ~ replicate_num,
data = transformed_data, family = gaussian())
drop1(model_replicate_slopes_log, test='Chisq')
## Single term deletions
##
## Model:
## trial_duration_log ~ location * replicate_num + (replicate_num |
## fish_id)
## Df AIC LRT Pr(>Chi)
## <none> 222.66
## location:replicate_num 1 228.82 8.1521 0.004301 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Model used
#model_replicate_slopes_log <- glmmTMB(trial_duration_log ~ location * replicate_num +
# dispformula = ~ replicate_num,
# (replicate_num | fish_id), # random intercepts + slopes per fish
# data = transformed_data, family = gaussian())
model_replicate_slopes_log_no_fish <- glmmTMB(trial_duration_log ~ location * replicate_num,
dispformula = ~ replicate_num,
data = transformed_data, family = gaussian())
anova(model_replicate_slopes_log,model_replicate_slopes_log_no_fish)
## Data: transformed_data
## Models:
## model_replicate_slopes_log_no_fish: trial_duration_log ~ location * replicate_num, zi=~0, disp=~replicate_num
## model_replicate_slopes_log: trial_duration_log ~ location * replicate_num + (replicate_num | , zi=~0, disp=~replicate_num
## model_replicate_slopes_log: fish_id), zi=~0, disp=~replicate_num
## Df AIC BIC logLik deviance Chisq
## model_replicate_slopes_log_no_fish 6 319.82 336.19 -153.91 307.82
## model_replicate_slopes_log 9 222.66 247.21 -102.33 204.66 103.16
## Chi Df Pr(>Chisq)
## model_replicate_slopes_log_no_fish
## model_replicate_slopes_log 3 < 2.2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(model_replicate_slopes_log)
## Family: gaussian ( identity )
## Formula:
## trial_duration_log ~ location * replicate_num + (replicate_num | fish_id)
## Dispersion: ~replicate_num
## Data: transformed_data
##
## AIC BIC logLik deviance df.resid
## 222.7 247.2 -102.3 204.7 104
##
## Random effects:
##
## Conditional model:
## Groups Name Variance Std.Dev. Corr
## fish_id (Intercept) 0.73260 0.8559
## replicate_num 0.00244 0.0494 -0.33
## Residual NA NA
## Number of obs: 113, groups: fish_id, 19
##
## Conditional model:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.46082 0.31656 1.456 0.14547
## locationOXFORD 1.22183 0.45296 2.697 0.00699 **
## replicate_num 0.04954 0.04036 1.227 0.21967
## locationOXFORD:replicate_num -0.17742 0.05551 -3.196 0.00139 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Dispersion model:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.40577 0.20487 -1.981 0.0476 *
## replicate_num -0.10820 0.05388 -2.008 0.0446 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Calculate model-predicted means
# Estimated marginal means on the log scale
emm <- emmeans(model_replicate_slopes_log, ~ location | replicate_num,
at = list(replicate_num = 1:6))
emm_df <- as.data.frame(emm)
emm_df <- emm_df %>%
mutate(
response_mean = exp(emmean) - log_offset,
lower_CL = exp(lower.CL) - log_offset,
upper_CL = exp(upper.CL) - log_offset)
Plot using model-predicted means. Note the log-scale so all the points fit nicely on the plot.
plot_data <- transformed_data %>%
mutate(trial_duration_raw = exp(trial_duration_log) - log_offset)
# Ensure Oxford comes first
plot_data$location <- factor(plot_data$location, levels = c("OXFORD", "LIRS"))
emm_df$location <- factor(emm_df$location, levels = c("OXFORD", "LIRS"))
# Set dodge width (adjust to control spacing)
dodge_width <- 0.4
dodge <- position_dodge(width = 0.4)
PhaseOne_trial_time <- ggplot() +
# Raw jittered data
geom_point(data = plot_data,
aes(x = replicate_num, y = trial_duration_raw, color = location),
position = dodge,
alpha = 0.3, size = 1.5, shape = 16) +
# Model-based error bars
geom_errorbar(data = emm_df,
aes(x = replicate_num, ymin = lower_CL, ymax = upper_CL, color = location),
width = 0.15, linewidth = 0.7,
position = position_dodge(width = 0.4)) +
# Model-based means
geom_point(data = emm_df,
aes(x = replicate_num, y = response_mean, fill = location),
shape = 21, size = 3, stroke = 0.2, color = "black",
position = position_dodge(width = 0.4)) +
# Unify legends
guides(
fill = guide_legend(title = "Location"),
color = guide_legend(title = "Location")) +
# Axes and theme
scale_y_continuous(
trans = "log1p", # log(x + 1), handles 0 nicely
breaks = c(0.5, 1, 2, 5, 10, 20), # adjust based on your data range
labels = scales::label_number()) +
scale_x_continuous(breaks = 1:6) + # Replicate ticks
scale_color_manual(values = c(OXFORD = "#002147", LIRS = "#C44E52")) +
scale_fill_manual(values = c(OXFORD = "#002147", LIRS = "#C44E52")) +
labs(x = "Replicate Number",
y = "Trial duration (seconds, log scale)",
title = "Model Estimates and Raw Data by Location") +
guides(fill = guide_legend(title = "Location"),
color = guide_legend(title = "Location")) +
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(hjust = 0.5, face = "bold"),
axis.title = element_text(size = 12),
axis.text = element_text(size = 11),
panel.grid.major.x = element_line(color = "grey90"),
panel.grid.minor = element_blank(),
panel.grid.major.y = element_line(color = "grey90"),
axis.line = element_line(color = "black"),
axis.ticks = element_line(color = "black"),
legend.position = "right")
PhaseOne_trial_time
# Ensure proper data types for consistency and plotting
data_T <- data_T %>%
mutate(
fish_id = factor(fish_id), # Convert fish_id to factor
test_result = factor(test_result, levels = c("fail", "pass")), # Set test_result as ordered factor
replicate_num = as.integer(replicate_num)) # Ensure replicate_num is integer
# Summarise total number of unique fish and total replicates per location
summary_table <- data_T %>%
group_by(location) %>%
summarise(n_fish = n_distinct(fish_id), # Count of unique fish per location
total_replicates = n()) # Total number of replicate entries
# Display the summary table
print(summary_table)
## # A tibble: 2 × 3
## location n_fish total_replicates
## <chr> <int> <int>
## 1 LIRS 9 90
## 2 OXFORD 9 90
## ---- Count Replicates per Fish at Each Location ----
# Count number of replicates for each fish within each location
replicates_per_fish <- data_T %>%
group_by(location, fish_id) %>%
summarise(
n_replicates = n(), # Number of replicates for each fish
.groups = "drop")
# Display the per-fish replicate counts
print(replicates_per_fish)
## # A tibble: 18 × 3
## location fish_id n_replicates
## <chr> <fct> <int>
## 1 LIRS 1 10
## 2 LIRS 2 10
## 3 LIRS 3 10
## 4 LIRS 4 10
## 5 LIRS 5 10
## 6 LIRS 6 10
## 7 LIRS 7 10
## 8 LIRS 8 10
## 9 LIRS 10 10
## 10 OXFORD 49 10
## 11 OXFORD 53 10
## 12 OXFORD 54 10
## 13 OXFORD 60 10
## 14 OXFORD 62 10
## 15 OXFORD 63 10
## 16 OXFORD 64 10
## 17 OXFORD 65 10
## 18 OXFORD 66 10
# Create a combined variable for custom coloring
data_T <- data_T %>%
mutate(
fish_id = factor(fish_id, levels = sort(unique(as.numeric(as.character(fish_id))))),
replicate_num = as.integer(replicate_num),
location = factor(location),
test_result = factor(test_result, levels = c("fail", "pass")),
location_result = paste(location, test_result, sep = "_"))
data_T$fish_id <- factor(data_T$fish_id, levels = sort(unique(data_T$fish_id)))
# Define custom colors for each location-result combination
custom_colors <- c(
"OXFORD_fail" = "#C44E52", # red
"OXFORD_pass" = "snow3", # green
"LIRS_fail" = "#002147", # blue
"LIRS_pass" = "#2ECC71") # yellow
# Plot
detour_test <- ggplot(data_T, aes(x = replicate_num, y = fish_id, fill = location_result)) +
geom_tile(color = "white") +
scale_fill_manual(
values = custom_colors,
labels = c(
"OXFORD_fail" = "Oxford – Fail",
"OXFORD_pass" = "Oxford – Pass",
"LIRS_fail" = "LIRS – Fail",
"LIRS_pass" = "LIRS – Pass"
),
name = "Location & Outcome" # ← legend title
) +
scale_x_continuous(breaks = unique(data_T$replicate_num)) + # Show all replicate numbers
scale_y_discrete(limits = rev(levels(data_T$fish_id))) +
labs(
title = "Test Results per Fish by Replicate and Location",
x = "Replicate Number",
y = "Fish ID",
fill = "Location & Result"
) +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 0, hjust = 0.5), # Horizontal x-axis labels
panel.grid = element_blank()
)
detour_test
# Remove titles
detour_test_clean <- detour_test + labs(title = NULL)
PhaseOne_trial_time_clean <- PhaseOne_trial_time + labs(title = NULL)
# Combine with annotation
detour_plot <- (PhaseOne_trial_time_clean + detour_test_clean) +
plot_annotation(tag_levels = 'A') # Adds A, B, etc.
detour_plot
ggsave("detour_figure.png", plot = detour_plot, width = 12, height = 6, dpi = 300)