Downloading libraries for project:
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## ── 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.2 ✔ tibble 3.3.0
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.1.0
## ── 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
library(lubridate)
library(patchwork)
## Warning: package 'patchwork' was built under R version 4.5.2
library(ggplot2)
library(readr)
library(cowplot)
## Warning: package 'cowplot' was built under R version 4.5.2
##
## Attaching package: 'cowplot'
##
## The following object is masked from 'package:patchwork':
##
## align_plots
##
## The following object is masked from 'package:lubridate':
##
## stamp
library(rlang)
##
## Attaching package: 'rlang'
##
## The following objects are masked from 'package:purrr':
##
## %@%, flatten, flatten_chr, flatten_dbl, flatten_int, flatten_lgl,
## flatten_raw, invoke, splice
library(knitr)
library(kableExtra)
## Warning: package 'kableExtra' was built under R version 4.5.2
##
## Attaching package: 'kableExtra'
##
## The following object is masked from 'package:dplyr':
##
## group_rows
library(nlme)
##
## Attaching package: 'nlme'
##
## The following object is masked from 'package:dplyr':
##
## collapse
library(purrr)
library(emmeans)
## Warning: package 'emmeans' was built under R version 4.5.2
## Welcome to emmeans.
## Caution: You lose important information if you filter this package's results.
## See '? untidy'
Importing data
Functions for creating figures:
# importing data sets
Flux_Data_With_Covars <- read_csv("C:/Users/KAUFMADA/Documents/R_Files/windrow-fluxes-co2-model-improvements/windrow-fluxes-co2-model-improvements/data_clean/Flux_Data_With_Covars.csv")
## Rows: 21090 Columns: 82
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Pile, Chamber_Corrected, Turning_Status, Date, DATE_TIME
## dbl (77): Chamber_Elevation, DOY, air_temperature, RH, VPD, Tdew, wind_speed...
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Cleaning data: Removing 9999 and -9999 which is NAN Removing unrealistic pile moisture and temperature data - 0-99 c range for pile temp - volumetric moisture 0.01 - 0.90
Creating daily observations (daily_pile_doy) where values are averaged by day for more interpretable figures
Creating weekly observation (summarise_weekly_mean_se) where values are averaged by week
# function used to remove NA values which LICOR uses -9999 for:
clean_numeric <- function(x,
min_val = -Inf,
max_val = Inf,
bad_values = c(-9999, -9999, 9999, 99999)) {
x <- as.numeric(x)
x[x %in% bad_values] <- NA_real_
x[x < min_val | x > max_val] <- NA_real_
x
}
# function that calculates the SE of a function:
se <- function(x) sd(x, na.rm = TRUE) / sqrt(sum(!is.na(x)))
Cleaned_Data <- Flux_Data_With_Covars %>%
mutate(
# DOY has decimals, so collapse to whole day
DOY = floor(DOY),
pile_temp = clean_numeric(TS_1.initial_value, min_val = 0, max_val = 99),
pile_moist = clean_numeric(SWC_1.initial_value, min_val = 0.01, max_val = 0.90),
BulkDensity = clean_numeric(BulkDensity, min_val = 0.01, max_val = 1)
)
daily_pile_doy <- Cleaned_Data %>%
group_by(Pile, DOY) %>%
summarise(
n_obs = sum(!is.na(pile_temp) | !is.na(pile_moist)),
pile_temp_mean = mean(pile_temp, na.rm = TRUE),
pile_temp_se = se(pile_temp),
pile_moist_mean = mean(pile_moist, na.rm = TRUE),
pile_moist_se = se(pile_moist),
.groups = "drop"
)
# making weekly figures to reduce noise for final paper figures:
summarise_weekly_mean_se <- function(df,
doy,
value,
group,
min_n = 2) {
doy <- enquo(doy)
value <- enquo(value)
group <- enquo(group)
df %>%
filter(!is.na(!!value), !is.na(!!doy), !is.na(!!group)) %>%
mutate(
week = floor((!!doy - 1) / 7) + 1
) %>%
group_by(!!group, week) %>%
summarise(
DOY = mean(!!doy, na.rm = TRUE), # x-position for plotting
mean = mean(!!value, na.rm = TRUE),
sd = sd(!!value, na.rm = TRUE),
n = sum(!is.na(!!value)),
se = sd / sqrt(n),
.groups = "drop"
) %>%
filter(n >= min_n)
}
weekly_moist <- summarise_weekly_mean_se(
df = Cleaned_Data,
doy = DOY,
value = pile_moist,
group = Pile,
min_n = 2
)
weekly_temp <- summarise_weekly_mean_se(
df = Cleaned_Data,
doy = DOY,
value = pile_temp,
group = Pile,
min_n = 2
)
Creating a function to Plot Temperature and Moisture
plot_points_err <- function(df,
x, y_mean, y_se,
group,
cols = NULL,
shapes = NULL,
point_size = 2.0,
point_alpha = 0.85,
err_alpha = 0.6,
err_lwd = 0.4,
err_width = 0,
base_size = 12,
legend_position = "top",
drop_na = TRUE,
x_lab = NULL,
y_lab = NULL,
legend_title = NULL,
vline = NULL,
vlines_red = NULL,
vlines_black = NULL,
vline_lwd = 0.4,
vline_alpha = 0.8,
vline_linetype = "dashed",
vline_red_linetype = "solid",
vline_black_linetype = "dashed",
vline_red_colour = "#9b1c31",
vline_black_colour = "#000000"){
# capture column names
x <- enquo(x)
y_mean <- enquo(y_mean)
y_se <- enquo(y_se)
group <- enquo(group)
# optionally drop NA rows for required columns
if (drop_na) {
df <- df[complete.cases(
df[, c(as_name(x), as_name(y_mean), as_name(y_se), as_name(group))]
), ]
}
p <- ggplot(df, aes(x = !!x, y = !!y_mean)) +
# error bars first (so points draw on top)
geom_errorbar(
aes(ymin = (!!y_mean) - (!!y_se),
ymax = (!!y_mean) + (!!y_se),
colour = !!group),
width = err_width,
alpha = err_alpha,
linewidth = err_lwd
)
if (!is.null(vlines_red)) {
p <- p + geom_vline(
xintercept = vlines_red,
colour = vline_red_colour,
linetype = vline_red_linetype,
linewidth = vline_lwd,
alpha = vline_alpha
)
}
if (!is.null(vlines_black)) {
p <- p + geom_vline(
xintercept = vlines_black,
colour = vline_black_colour,
linetype = vline_black_linetype,
linewidth = vline_lwd,
alpha = vline_alpha
)
}
p <- p +
# points (no lines)
geom_point(
aes(colour = !!group, shape = !!group),
size = point_size,
alpha = point_alpha
) +
theme_bw(base_size = base_size) +
theme(
legend.position = legend_position,
panel.grid.minor = element_blank()
)
# manual scales if provided (keeps consistent styling across plots)
if (!is.null(cols)) {
p <- p + scale_colour_manual(values = cols)
}
if (!is.null(shapes)) {
p <- p + scale_shape_manual(values = shapes)
}
# ── LABELS SECTION ─────────────────────────────
if (!is.null(x_lab) || !is.null(y_lab) || !is.null(legend_title)) {
p <- p + labs(
x = x_lab,
y = y_lab,
colour = legend_title,
shape = legend_title
)
}
return(p)
}
Average Temperature and Moisture Plots with SE bars for each pile done by DOY and Week
#turning data to add to plots if needed
turns_both_red <- c(159, 186, 208, 234, 262, 290)
turns_control_black <- c(173, 191, 222, 249, 276)
turns_both_red_week <- c(159, 186, 208, 234, 262, 290)/7
turns_control_black_week <- c(173, 191, 222, 249, 276)/7
pile_cols <- c(C = "#000000", E = "#9b1c31")
pile_shapes <- c(C = 16, E = 17)
fig_moist_doy <- plot_points_err(
daily_pile_doy,
x = DOY,
y_mean = pile_moist_mean,
y_se = pile_moist_se,
group = Pile,
cols = pile_cols,
shapes = pile_shapes,
point_size = 1.8,
x_lab = "Day of year",
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
legend_title = "Pile",
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_moist_doy
fig_moist_weekly <- plot_points_err(
weekly_moist,
x = week,
y_mean = mean,
y_se = se,
group = Pile,
cols = pile_cols,
shapes = pile_shapes,
point_size = 1.8,
x_lab = "Week of year",
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
legend_title = "Pile",
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_moist_weekly
fig_temp_doy <- plot_points_err(
daily_pile_doy,
x = DOY,
y_mean =pile_temp_mean,
y_se = pile_temp_se,
group = Pile,
cols = pile_cols,
shapes = pile_shapes,
point_size = 1.8,
x_lab = "Day of year",
y_lab = expression("Pile Temperature Degrees Celsius"),
legend_title = "Pile",
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_temp_doy
fig_temp_weekly <- plot_points_err(
weekly_temp,
x = week,
y_mean =mean,
y_se = se,
group = Pile,
cols = pile_cols,
shapes = pile_shapes,
point_size = 1.8,
x_lab = "Week of year",
y_lab = expression("Pile Temperature Degrees Celsius"),
legend_title = "Pile",
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_temp_weekly
Plotting chamber pile temperature all points (by chamber individually or all chambers in one plot) with turning events
library(patchwork)
chamber_cols <- c(
"C1" = "#E41A1C", # red
"C2" = "#377EB8", # blue
"C3" = "#4DAF4A", # green
"C4" = "#FF7F00", # orange
"C5" = "#984EA3", # purple
"C6" = "#A65628" # brown
)
plot_temp <- function(df,
x,
y,
group,
cols = NULL,
base_size = 11,
legend_position = "top",
x_lab = NULL,
y_lab = "Temperature (°C)",
legend_title = NULL,
point_size = 0.8,
point_alpha = 0.4,
vlines_red = NULL,
vlines_black = NULL,
vline_lwd = 0.4,
vline_red_linetype = "solid",
vline_black_linetype = "dashed",
vline_red_colour = "#9b1c31",
vline_black_colour = "#000000") {
x <- enquo(x)
y <- enquo(y)
group <- enquo(group)
p <- ggplot(df, aes(x = !!x, y = !!y,
colour = factor(!!group),
group = factor(!!group))) +
geom_point(size = point_size, alpha = point_alpha, shape = 16) +
theme_bw(base_size = base_size) +
theme(
legend.position = legend_position,
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
axis.text = element_text(size = 9),
axis.title = element_text(size = 10),
legend.text = element_text(size = 8),
legend.key.size = unit(0.35, "cm")
) +
labs(x = x_lab,
y = y_lab,
colour = legend_title)
if (!is.null(cols)) {
p <- p + scale_colour_manual(values = cols)
}
if (!is.null(vlines_red)) {
p <- p + geom_vline(xintercept = vlines_red,
colour = vline_red_colour,
linetype = vline_red_linetype,
linewidth = vline_lwd,
alpha = 0.8)
}
if (!is.null(vlines_black)) {
p <- p + geom_vline(xintercept = vlines_black,
colour = vline_black_colour,
linetype = vline_black_linetype,
linewidth = vline_lwd,
alpha = 0.8)
}
return(p)
}
fig_temp_chamber_raw <- plot_temp(
df = Cleaned_Data,
x = DOY.initial_value,
y = pile_temp,
group = Chamber_Corrected,
cols = chamber_cols,
y_lab = expression("Temperature (°C)"),
x_lab = "Day of year",
legend_title = "Chamber",
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_temp_chamber_raw
## Warning: Removed 2478 rows containing missing values or values outside the scale range
## (`geom_point()`).
chamber_plots <- Cleaned_Data %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_temp,
group = Chamber_Corrected,
y_lab = expression("Temperature (°C)"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols,
vlines_red = turns_both_red,
vlines_black = turns_control_black
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col <- chamber_plots[["C1"]] / chamber_plots[["C2"]] / chamber_plots[["C3"]]
right_col <- chamber_plots[["C4"]] / chamber_plots[["C5"]] / chamber_plots[["C6"]]
fig_temp_individual <- left_col | right_col
fig_temp_individual
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
chamber_plots_early <- Cleaned_Data %>%
# filter(DOY < 159) %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_temp,
group = Chamber_Corrected,
y_lab = expression("Temperature (°C)"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols)
+
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_early <- chamber_plots_early[["C1"]] / chamber_plots_early[["C2"]] / chamber_plots_early[["C3"]]
right_col_early <- chamber_plots_early[["C4"]] / chamber_plots_early[["C5"]] / chamber_plots_early[["C6"]]
fig_temp_individual_early <- left_col_early | right_col_early
fig_temp_individual_early
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
vlines_red_window <- turns_both_red[turns_both_red >= 159]
vlines_black_window <- turns_control_black[turns_control_black >= 159]
# C1, C2, C3 - control chambers: both red and black lines
control_plots <- Cleaned_Data %>%
filter( Chamber_Corrected %in% c("C1", "C2", "C3")) %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_temp,
group = Chamber_Corrected,
y_lab = expression("Temperature (°C)"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols,
vlines_red = vlines_red_window,
vlines_black = vlines_black_window,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
) +
ggtitle(.y) +
theme(legend.position = "none")
)
# C4, C5, C6 - experimental chambers: red lines only
exp_plots <- Cleaned_Data %>%
filter( Chamber_Corrected %in% c("C4", "C5", "C6")) %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_temp,
group = Chamber_Corrected,
y_lab = expression("Temperature (°C)"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols,
vlines_red = vlines_red_window,
vline_red_linetype = "solid"
# no vlines_black
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_early_II <- control_plots[["C1"]] / control_plots[["C2"]] / control_plots[["C3"]]
right_col_early_II <- exp_plots[["C4"]] / exp_plots[["C5"]] / exp_plots[["C6"]]
fig_temp_individual_early_II <- left_col_early_II | right_col_early_II
fig_temp_individual_early_II
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
Un averaged moisture by chamber, looking at whole experimental cycle and at early turning events.
library(patchwork)
chamber_cols <- c(
"C1" = "#E41A1C", # red
"C2" = "#377EB8", # blue
"C3" = "#4DAF4A", # green
"C4" = "#FF7F00", # orange
"C5" = "#984EA3", # purple
"C6" = "#A65628" # brown
)
plot_temp <- function(df,
x,
y,
group,
cols = NULL,
base_size = 11,
legend_position = "top",
x_lab = NULL,
y_lab = "Temperature (°C)",
legend_title = NULL,
point_size = 0.8,
point_alpha = 0.4,
vlines_red = NULL,
vlines_black = NULL,
vline_lwd = 0.4,
vline_red_linetype = "solid",
vline_black_linetype = "dashed",
vline_red_colour = "#9b1c31",
vline_black_colour = "#000000") {
x <- enquo(x)
y <- enquo(y)
group <- enquo(group)
p <- ggplot(df, aes(x = !!x, y = !!y,
colour = factor(!!group),
group = factor(!!group))) +
geom_point(size = point_size, alpha = point_alpha, shape = 16) +
theme_bw(base_size = base_size) +
theme(
legend.position = legend_position,
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
axis.text = element_text(size = 6),
axis.title = element_text(size = 6),
legend.text = element_text(size = 8),
legend.key.size = unit(0.35, "cm")
) +
labs(x = x_lab,
y = y_lab,
colour = legend_title)
if (!is.null(cols)) {
p <- p + scale_colour_manual(values = cols)
}
if (!is.null(vlines_red)) {
p <- p + geom_vline(xintercept = vlines_red,
colour = vline_red_colour,
linetype = vline_red_linetype,
linewidth = vline_lwd,
alpha = 0.8)
}
if (!is.null(vlines_black)) {
p <- p + geom_vline(xintercept = vlines_black,
colour = vline_black_colour,
linetype = vline_black_linetype,
linewidth = vline_lwd,
alpha = 0.8)
}
return(p)
}
fig_temp_chamber_raw <- plot_temp(
df = Cleaned_Data,
x = DOY.initial_value,
y = pile_moist,
group = Chamber_Corrected,
cols = chamber_cols,
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
x_lab = "Day of year",
legend_title = "Chamber",
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_temp_chamber_raw
## Warning: Removed 10118 rows containing missing values or values outside the scale range
## (`geom_point()`).
chamber_plots <- Cleaned_Data %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_moist,
group = Chamber_Corrected,
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols,
vlines_red = turns_both_red,
vlines_black = turns_control_black
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col <- chamber_plots[["C1"]] / chamber_plots[["C2"]] / chamber_plots[["C3"]]
right_col <- chamber_plots[["C4"]] / chamber_plots[["C5"]] / chamber_plots[["C6"]]
fig_temp_individual <- left_col | right_col
fig_temp_individual
## Warning: Removed 288 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1723 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 768 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1259 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 3138 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 2942 rows containing missing values or values outside the scale range
## (`geom_point()`).
chamber_plots_early <- Cleaned_Data %>%
filter(DOY < 159) %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_moist,
group = Chamber_Corrected,
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols)
+
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_early <- chamber_plots_early[["C1"]] / chamber_plots_early[["C2"]] / chamber_plots_early[["C3"]]
right_col_early <- chamber_plots_early[["C4"]] / chamber_plots_early[["C5"]] / chamber_plots_early[["C6"]]
fig_temp_individual_early <- left_col_early | right_col_early
fig_temp_individual_early
## Warning: Removed 75 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 170 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 175 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 55 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 140 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 185 rows containing missing values or values outside the scale range
## (`geom_point()`).
chamber_plots_early_II <- Cleaned_Data %>%
filter(DOY < 173) %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY.initial_value,
y = pile_moist,
group = Chamber_Corrected,
y_lab = expression("Volumetric water content (m"^3*" m"^-3*")"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols ) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_early_II <- chamber_plots_early_II[["C1"]] / chamber_plots_early_II[["C2"]] / chamber_plots_early_II[["C3"]]
right_col_early_II <- chamber_plots_early_II[["C4"]] / chamber_plots_early_II[["C5"]] / chamber_plots_early_II[["C6"]]
fig_temp_individual_early_II <- left_col_early_II | right_col_early_II
fig_temp_individual_early_II
## Warning: Removed 272 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 174 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 753 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 226 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 670 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 398 rows containing missing values or values outside the scale range
## (`geom_point()`).
Adding data _ by trace gas where each has individual emissions data
library(readxl)
co2_raw <- read.csv("C:/Users/KAUFMADA/Documents/R_Files/windrow-fluxes-co2-model-improvements/windrow-fluxes-co2-model-improvements/data_clean/CO2_R_2_Filtered.csv")
ch4_raw <- read.csv("C:/Users/KAUFMADA/Documents/R_Files/windrow-fluxes-co2-model-improvements/windrow-fluxes-co2-model-improvements/data_clean/CH4_R_2_Filtered.csv")
n2o_raw <- read.csv("C:/Users/KAUFMADA/Documents/R_Files/windrow-fluxes-co2-model-improvements/windrow-fluxes-co2-model-improvements/data_clean/N2O_R_2_Filtered.csv")
Plots flux values for each Trace Gas by Chamber
# Plotting Flux Vales by chamber for each trace gas.
co2_clean <- co2_raw %>%
filter(CO2_DRY.initial_value >= 0)
# Plot
chamber_plots_co2 <- co2_clean %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY,
y = FCO2_DRY.LIN,
group = Chamber_Corrected,
y_lab = expression("CO"[2]~"Flux"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_co2 <- chamber_plots_co2[["C1"]] / chamber_plots_co2[["C2"]] / chamber_plots_co2[["C3"]]
right_col_co2 <- chamber_plots_co2[["C4"]] / chamber_plots_co2[["C5"]] / chamber_plots_co2[["C6"]]
fig_co2_individual <- left_col_co2 | right_col_co2
fig_co2_individual
ch4_clean <- ch4_raw %>%
filter(CH4_DRY.initial_value >= 0,
CH4_DRY.initial_value <= 100000)
# Plot
chamber_plots_ch4 <- ch4_clean %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY,
y = FCH4_DRY.LIN,
group = Chamber_Corrected,
y_lab = expression("CH"[4]~"Flux"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_ch4 <- chamber_plots_ch4[["C1"]] / chamber_plots_ch4[["C2"]] / chamber_plots_ch4[["C3"]]
right_col_ch4 <- chamber_plots_ch4[["C4"]] / chamber_plots_ch4[["C5"]] / chamber_plots_ch4[["C6"]]
fig_ch4_individual <- left_col_ch4 | right_col_ch4
fig_ch4_individual
N2O_clean <- n2o_raw %>%
filter(N2O_DRY.initial_value >= 0,
N2O_DRY.initial_value <= 100000)
# Plot
chamber_plots_N2O <- N2O_clean %>%
split(.$Chamber_Corrected) %>%
imap(~ plot_temp(
df = .x,
x = DOY,
y = FN2O_DRY.LIN,
group = Chamber_Corrected,
y_lab = expression("N"[2]~"O Flux"),
x_lab = "Day of year",
legend_title = "Chamber",
cols = chamber_cols
) +
ggtitle(.y) +
theme(legend.position = "none")
)
left_col_N2O <- chamber_plots_N2O[["C1"]] / chamber_plots_N2O[["C2"]] / chamber_plots_N2O[["C3"]]
right_col_N2O <- chamber_plots_N2O[["C4"]] / chamber_plots_N2O[["C5"]] / chamber_plots_N2O[["C6"]]
fig_N2O_individual <- left_col_N2O | right_col_N2O
fig_N2O_individual
Plots where temperature and co2 emissions are plotted on the same graph individually by pile.
chambers <- unique(co2_raw$Chamber_Corrected)
chamber_plots_overlay <- setNames(lapply(chambers, function(ch) {
co2_ch <- co2_clean %>% filter(Chamber_Corrected == ch)
temp_ch <- Cleaned_Data %>% filter(Chamber_Corrected == ch)
# Scale factor per chamber
scale_factor <- max(co2_ch$FCO2_DRY.LIN, na.rm = TRUE) /
max(temp_ch$pile_temp, na.rm = TRUE)
ggplot() +
geom_point(data = co2_ch,
aes(x = DOY, y = FCO2_DRY.LIN),
colour = chamber_cols[[ch]], size = 0.8, alpha = 0.4, shape = 16) +
geom_point(data = temp_ch,
aes(x = DOY.initial_value, y = pile_temp * scale_factor),
colour = "black", size = 0.8, alpha = 0.4, shape = 17) +
scale_y_continuous(
name = expression("CO"[2]~"Flux"),
sec.axis = sec_axis(~ . / scale_factor,
name = expression("Temperature (°C)"))
) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
axis.title.y.right = element_text(colour = "black"),
axis.text.y.right = element_text(colour = "black")
) +
labs(x = "Day of year") +
ggtitle(ch)
}), chambers)
left_col_overlay <- chamber_plots_overlay[["C1"]] / chamber_plots_overlay[["C2"]] / chamber_plots_overlay[["C3"]]
right_col_overlay <- chamber_plots_overlay[["C4"]] / chamber_plots_overlay[["C5"]] / chamber_plots_overlay[["C6"]]
fig_overlay <- left_col_overlay | right_col_overlay
fig_overlay
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
Plots where temperature and ch4 emissions are plotted on the same graph individually by pile. Log transformed for better scales
chamber_plots_overlay_CH4 <- setNames(lapply(chambers, function(ch) {
CH4_ch <- ch4_clean %>% filter(Chamber_Corrected == ch)
temp_ch <- Cleaned_Data %>% filter(Chamber_Corrected == ch)
# Scale factor on log scale
scale_factor <- max(log1p(CH4_ch$FCH4_DRY.LIN), na.rm = TRUE) /
max(temp_ch$pile_temp, na.rm = TRUE)
ggplot() +
geom_point(data = CH4_ch,
aes(x = DOY, y = log1p(FCH4_DRY.LIN)),
colour = chamber_cols[[ch]], size = 0.8, alpha = 0.4, shape = 16) +
geom_point(data = temp_ch,
aes(x = DOY.initial_value, y = pile_temp * scale_factor),
colour = "black", size = 0.8, alpha = 0.4, shape = 17) +
scale_y_continuous(
name = expression("CH4"[4]*" Flux (log scale)"),
sec.axis = sec_axis(~ . / scale_factor,
name = expression("Temperature (°C)"))
) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
axis.title.y.right = element_text(colour = "black"),
axis.text.y.right = element_text(colour = "black")
) +
labs(x = "Day of year") +
ggtitle(ch)
}), chambers)
left_col_overlay_CH4 <- chamber_plots_overlay_CH4[["C1"]] / chamber_plots_overlay_CH4[["C2"]] / chamber_plots_overlay_CH4[["C3"]]
right_col_overlay_CH4 <- chamber_plots_overlay_CH4[["C4"]] / chamber_plots_overlay_CH4[["C5"]] / chamber_plots_overlay_CH4[["C6"]]
fig_overlay_CH4 <- left_col_overlay_CH4 | right_col_overlay_CH4
fig_overlay_CH4
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
overlaid temperature n2o data log scale
chamber_plots_overlay_n2o <- setNames(lapply(chambers, function(ch) {
n2o_ch <- N2O_clean %>% filter(Chamber_Corrected == ch)
temp_ch <- Cleaned_Data %>% filter(Chamber_Corrected == ch)
# Scale factor on log scale
scale_factor <- max(log1p(n2o_ch$FN2O_DRY.LIN), na.rm = TRUE) /
max(temp_ch$pile_temp, na.rm = TRUE)
ggplot() +
geom_point(data = n2o_ch,
aes(x = DOY, y = log1p(FN2O_DRY.LIN)),
colour = chamber_cols[[ch]], size = 0.8, alpha = 0.4, shape = 16) +
geom_point(data = temp_ch,
aes(x = DOY.initial_value, y = pile_temp * scale_factor),
colour = "black", size = 0.8, alpha = 0.4, shape = 17) +
scale_y_continuous(
name = expression("N"[2]*"O Flux (log scale)"),
sec.axis = sec_axis(~ . / scale_factor,
name = expression("Temperature (°C)"))
) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
axis.title.y.right = element_text(colour = "black"),
axis.text.y.right = element_text(colour = "black")
) +
labs(x = "Day of year") +
ggtitle(ch)
}), chambers)
left_col_overlay_n2o <- chamber_plots_overlay_n2o[["C1"]] / chamber_plots_overlay_n2o[["C2"]] / chamber_plots_overlay_n2o[["C3"]]
right_col_overlay_n2o <- chamber_plots_overlay_n2o[["C4"]] / chamber_plots_overlay_n2o[["C5"]] / chamber_plots_overlay_n2o[["C6"]]
fig_overlay_n2o <- left_col_overlay_n2o | right_col_overlay_n2o
fig_overlay_n2o
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
Creating Tables with Pile Variables:
pile_vars_temp <- Cleaned_Data %>%
filter(!is.na(pile_temp)) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_temp = mean(pile_temp),
sd_temp = sd(pile_temp),
se_temp = sd_temp/sqrt(n_obs)
)
pile_vars_water <- Cleaned_Data %>%
filter(!is.na(pile_moist)) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_moist = mean(pile_moist),
sd_moist = sd(pile_moist),
se_moist = sd_moist/sqrt(n_obs)
)
Summary statistics for volumetric water content by pile. Summary statistics for Temperature by pile. Box plots by pile dunn_results test by pile: to see which pairs are significantly different for temp
| Pile | n_obs | mean_moist | sd_moist | se_moist |
|---|---|---|---|---|
| C | 7195 | 0.365 | 0.252 | 0.003 |
| E | 3777 | 0.550 | 0.231 | 0.004 |
| Pile | n_obs | mean_temp | sd_temp | se_temp |
|---|---|---|---|---|
| C | 9192 | 54.069 | 8.992 | 0.094 |
| E | 9420 | 51.778 | 12.812 | 0.132 |
## Warning: Removed 2478 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
##
## Kruskal-Wallis rank sum test
##
## data: pile_temp by Chamber_Corrected
## Kruskal-Wallis chi-squared = 2059.9, df = 5, p-value < 2.2e-16
## Warning: package 'dunn.test' was built under R version 4.5.3
## Kruskal-Wallis rank sum test
##
## data: x and group
## Kruskal-Wallis chi-squared = 2059.9229, df = 5, p-value = 0
##
##
## Dunn's Pairwise Comparison of x by group
## (Bonferroni)
## Col Mean-│
## Row Mean │ C1 C2 C3 C4 C5
## ─────────┼───────────────────────────────────────────────────────
## C2 │ 14.01824
## │ 0.0000*
## │
## C3 │ 7.369934 -6.733721
## │ 0.0000* 0.0000*
## │
## C4 │ 14.72761 0.157561 7.153147
## │ 0.0000* 1.0000 0.0000*
## │
## C5 │ -15.06356 -27.64340 -21.73351 -28.69974
## │ 0.0000* 0.0000* 0.0000* 0.0000*
## │
## C6 │ 29.05269 13.71717 21.16311 14.12608 42.03163
## │ 0.0000* 0.0000* 0.0000* 0.0000* 0.0000*
##
## α = 0.05
## Reject Ho if p ≤ α/2, where p = Pr(Z ≥ |z|)
## Warning: package 'rstatix' was built under R version 4.5.3
##
## Attaching package: 'rstatix'
##
## The following object is masked from 'package:stats':
##
## filter
## Warning: package 'ggpubr' was built under R version 4.5.3
##
## Attaching package: 'ggpubr'
##
## The following object is masked from 'package:cowplot':
##
## get_legend
## Warning: Removed 2478 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Warning: Removed 2478 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
Measuring days in thermophillic stage by chamber
Cleaned_Data %>%
group_by(Chamber_Corrected) %>%
summarise(
max_temp = max(pile_temp, na.rm = TRUE),
n_obs_above_70 = sum(pile_temp > 70, na.rm = TRUE),
n_obs_above_75 = sum(pile_temp > 75, na.rm = TRUE),
first_above_70 = min(DOY.initial_value[pile_temp > 70], na.rm = TRUE),
last_above_70 = max(DOY.initial_value[pile_temp > 70], na.rm = TRUE)
)
## # A tibble: 6 × 6
## Chamber_Corrected max_temp n_obs_above_70 n_obs_above_75 first_above_70
## <chr> <dbl> <int> <int> <dbl>
## 1 C1 71.6 3 0 153.
## 2 C2 82.3 320 190 160.
## 3 C3 73.4 49 0 153.
## 4 C4 77.5 128 3 160.
## 5 C5 75.4 163 19 154.
## 6 C6 73.4 43 0 160.
## # ℹ 1 more variable: last_above_70 <dbl>
Cleaned_Data %>%
group_by(Chamber_Corrected, floor(DOY.initial_value)) %>%
summarise(daily_mean_temp = mean(pile_temp, na.rm = TRUE), .groups = "drop") %>%
rename(DOY_day = `floor(DOY.initial_value)`) %>%
group_by(Chamber_Corrected) %>%
arrange(DOY_day) %>%
mutate(
above_55 = daily_mean_temp > 55,
# identify consecutive groups
group_id = cumsum(!above_55)
) %>%
filter(above_55) %>%
group_by(Chamber_Corrected, group_id) %>%
summarise(
consecutive_days = n(),
start_DOY = min(DOY_day),
end_DOY = max(DOY_day),
.groups = "drop"
) %>%
group_by(Chamber_Corrected) %>%
summarise(
max_consecutive_days_above_55 = max(consecutive_days),
start_DOY = start_DOY[which.max(consecutive_days)],
end_DOY = end_DOY[which.max(consecutive_days)]
)
## # A tibble: 6 × 4
## Chamber_Corrected max_consecutive_days_above_55 start_DOY end_DOY
## <chr> <int> <dbl> <dbl>
## 1 C1 17 195 213
## 2 C2 20 152 171
## 3 C3 20 152 171
## 4 C4 31 173 220
## 5 C5 43 159 227
## 6 C6 13 219 231
Looking at % measurements removed in cleaning - not sure about this need to review what processing was used to get to ch4/n2o/co2 raw as I think there was some initial cleaning.
library(gt)
## Warning: package 'gt' was built under R version 4.5.3
##
## Attaching package: 'gt'
## The following object is masked from 'package:cowplot':
##
## as_gtable
# Build summary table
obs_summary <- ch4_raw %>%
group_by(Chamber_Corrected) %>%
summarise(n_raw = n()) %>%
left_join(
ch4_clean %>% group_by(Chamber_Corrected) %>% summarise(n_clean = n())
) %>%
mutate(
n_removed = n_raw - n_clean,
pct_removed = round(n_removed / n_raw * 100, 1)
) %>%
left_join(
ch4_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_ch4 = round(median(FCH4_DRY.LIN, na.rm = TRUE), 1),
iqr_ch4 = round(IQR(FCH4_DRY.LIN, na.rm = TRUE), 1),
max_ch4 = round(max(FCH4_DRY.LIN, na.rm = TRUE), 1)
)
) %>%
left_join(
Cleaned_Data %>%
group_by(Chamber_Corrected) %>%
summarise(median_temp = round(median(pile_temp, na.rm = TRUE), 1))
)
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
# Render with gt
obs_summary %>%
gt() %>%
cols_label(
Chamber_Corrected = "Chamber",
n_raw = "Raw Obs.",
n_clean = "Clean Obs.",
n_removed = "Removed",
pct_removed = "% Removed",
median_ch4 = "Median CH₄ Flux",
iqr_ch4 = "IQR CH₄ Flux",
max_ch4 = "Max CH₄ Flux",
median_temp = "Median Temp (°C)"
) %>%
tab_header(
title = "CH₄ Data Quality and Summary Statistics by Chamber",
subtitle = "Raw vs. cleaned observations, removal rates, and flux summaries"
) %>%
tab_spanner(
label = "Observation Count",
columns = c(n_raw, n_clean, n_removed, pct_removed)
) %>%
tab_spanner(
label = "CH₄ Flux (nmol m⁻² s⁻¹)",
columns = c(median_ch4, iqr_ch4, max_ch4)
) %>%
tab_footnote(
footnote = "Raw observations: all measurements collected by the multiplexer. Clean observations: remaining after removal of negative initial values and values exceeding 50,000 ppb/ppm. % Removed reflects data lost to quality filtering.",
locations = cells_title()
) %>%
tab_style(
style = cell_fill(color = "#f7f7f7"),
locations = cells_body(rows = seq(1, nrow(obs_summary), 2))
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_column_labels()
) %>%
opt_table_font(font = "Times New Roman")
| CH₄ Data Quality and Summary Statistics by Chamber1 | ||||||||
| Raw vs. cleaned observations, removal rates, and flux summaries1 | ||||||||
| Chamber |
Observation Count
|
CH₄ Flux (nmol m⁻² s⁻¹)
|
Median Temp (°C) | |||||
|---|---|---|---|---|---|---|---|---|
| Raw Obs. | Clean Obs. | Removed | % Removed | Median CH₄ Flux | IQR CH₄ Flux | Max CH₄ Flux | ||
| C1 | 1498 | 1470 | 28 | 1.9 | 1032.2 | 2447.4 | 49978.7 | 56.2 |
| C2 | 2248 | 2101 | 147 | 6.5 | 9452.2 | 12937.6 | 89459.9 | 51.0 |
| C3 | 1609 | 1308 | 301 | 18.7 | 265.3 | 270.9 | 14499.1 | 54.3 |
| C4 | 1348 | 1341 | 7 | 0.5 | 721.2 | 1982.1 | 24203.6 | 51.0 |
| C5 | 1626 | 1280 | 346 | 21.3 | 2646.1 | 4822.8 | 73778.6 | 62.7 |
| C6 | 1963 | 1505 | 458 | 23.3 | 2055.2 | 5205.9 | 41800.1 | 48.8 |
| 1 Raw observations: all measurements collected by the multiplexer. Clean observations: remaining after removal of negative initial values and values exceeding 50,000 ppb/ppm. % Removed reflects data lost to quality filtering. | ||||||||
library(gt)
# Build summary table
obs_summary <- ch4_raw %>%
group_by(Chamber_Corrected) %>%
summarise(n_raw_ch4 = n()) %>%
left_join(
ch4_clean %>% group_by(Chamber_Corrected) %>% summarise(n_clean_ch4 = n())
) %>%
mutate(
n_removed_ch4 = n_raw_ch4 - n_clean_ch4,
pct_removed_ch4 = round(n_removed_ch4 / n_raw_ch4 * 100, 1)
) %>%
left_join(
co2_raw %>%
group_by(Chamber_Corrected) %>%
summarise(n_raw_co2 = n())
) %>%
left_join(
co2_clean %>% group_by(Chamber_Corrected) %>% summarise(n_clean_co2 = n())
) %>%
mutate(
n_removed_co2 = n_raw_co2 - n_clean_co2,
pct_removed_co2 = round(n_removed_co2 / n_raw_co2 * 100, 1)
) %>%
left_join(
n2o_raw %>%
group_by(Chamber_Corrected) %>%
summarise(n_raw_n2o = n())
) %>%
left_join(
N2O_clean %>% group_by(Chamber_Corrected) %>% summarise(n_clean_n2o = n())
) %>%
mutate(
n_removed_n2o = n_raw_n2o - n_clean_n2o,
pct_removed_n2o = round(n_removed_n2o / n_raw_n2o * 100, 1)
) %>%
# Flux summaries
left_join(
ch4_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_ch4 = round(median(FCH4_DRY.LIN, na.rm = TRUE), 1),
iqr_ch4 = round(IQR(FCH4_DRY.LIN, na.rm = TRUE), 1)
)
) %>%
left_join(
co2_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_co2 = round(median(FCO2_DRY.LIN, na.rm = TRUE), 1),
iqr_co2 = round(IQR(FCO2_DRY.LIN, na.rm = TRUE), 1)
)
) %>%
left_join(
N2O_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_n2o = round(median(FN2O_DRY.LIN, na.rm = TRUE), 1),
iqr_n2o = round(IQR(FN2O_DRY.LIN, na.rm = TRUE), 1)
)
) %>%
left_join(
Cleaned_Data %>%
group_by(Chamber_Corrected) %>%
summarise(median_temp = round(median(pile_temp, na.rm = TRUE), 1))
)
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
## Joining with `by = join_by(Chamber_Corrected)`
# Render with gt
obs_summary %>%
gt() %>%
cols_label(
Chamber_Corrected = "Chamber",
n_raw_ch4 = "Raw",
n_clean_ch4 = "Clean",
pct_removed_ch4 = "% Removed",
n_raw_co2 = "Raw",
n_clean_co2 = "Clean",
pct_removed_co2 = "% Removed",
n_raw_n2o = "Raw",
n_clean_n2o = "Clean",
pct_removed_n2o = "% Removed",
median_ch4 = "Median",
iqr_ch4 = "IQR",
median_co2 = "Median",
iqr_co2 = "IQR",
median_n2o = "Median",
iqr_n2o = "IQR",
median_temp = "Median Temp (°C)"
) %>%
cols_hide(c(n_removed_ch4, n_removed_co2, n_removed_n2o)) %>%
tab_header(
title = "Data Quality and Flux Summary Statistics by Chamber",
subtitle = "Raw vs. cleaned observations and median flux values for all three trace gases"
) %>%
tab_spanner(
label = "CH₄ Observations",
columns = c(n_raw_ch4, n_clean_ch4, pct_removed_ch4)
) %>%
tab_spanner(
label = "CO₂ Observations",
columns = c(n_raw_co2, n_clean_co2, pct_removed_co2)
) %>%
tab_spanner(
label = "N₂O Observations",
columns = c(n_raw_n2o, n_clean_n2o, pct_removed_n2o)
) %>%
tab_spanner(
label = "CH₄ Flux",
columns = c(median_ch4, iqr_ch4)
) %>%
tab_spanner(
label = "CO₂ Flux",
columns = c(median_co2, iqr_co2)
) %>%
tab_spanner(
label = "N₂O Flux",
columns = c(median_n2o, iqr_n2o)
) %>%
tab_footnote(
footnote = "Raw: all multiplexer observations. Clean: remaining after quality filtering (removal of negative initial values; CH₄ capped at 50,000 ppb). % Removed reflects data lost to filtering. Flux units: nmol m⁻² s⁻¹. Median and IQR used given heavily right-skewed flux distributions.",
locations = cells_title()
) %>%
tab_style(
style = cell_fill(color = "#f7f7f7"),
locations = cells_body(rows = seq(1, 6, 2))
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_column_labels()
) %>%
opt_table_font(font = "Times New Roman")
| Data Quality and Flux Summary Statistics by Chamber1 | ||||||||||||||||
| Raw vs. cleaned observations and median flux values for all three trace gases1 | ||||||||||||||||
| Chamber |
CH₄ Observations
|
CO₂ Observations
|
N₂O Observations
|
CH₄ Flux
|
CO₂ Flux
|
N₂O Flux
|
Median Temp (°C) | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Raw | Clean | % Removed | Raw | Clean | % Removed | Raw | Clean | % Removed | Median | IQR | Median | IQR | Median | IQR | ||
| C1 | 1498 | 1470 | 1.9 | 1769 | 1711 | 3.3 | 2146 | 2146 | 0 | 1032.2 | 2447.4 | 111.3 | 97.7 | 55.6 | 81.1 | 56.2 |
| C2 | 2248 | 2101 | 6.5 | 1683 | 1682 | 0.1 | 2638 | 2638 | 0 | 9452.2 | 12937.6 | 240.6 | 292.2 | 121.6 | 229.1 | 51.0 |
| C3 | 1609 | 1308 | 18.7 | 1612 | 1596 | 1.0 | 2122 | 2122 | 0 | 265.3 | 270.9 | 93.3 | 111.3 | 37.8 | 80.0 | 54.3 |
| C4 | 1348 | 1341 | 0.5 | 1413 | 1369 | 3.1 | 2644 | 2644 | 0 | 721.2 | 1982.1 | 116.3 | 135.2 | 34.4 | 101.9 | 51.0 |
| C5 | 1626 | 1280 | 21.3 | 1655 | 1646 | 0.5 | 2306 | 2305 | 0 | 2646.1 | 4822.8 | 196.1 | 208.6 | 35.1 | 70.4 | 62.7 |
| C6 | 1963 | 1505 | 23.3 | 1147 | 1118 | 2.5 | 1961 | 1961 | 0 | 2055.2 | 5205.9 | 126.5 | 127.3 | 36.5 | 87.6 | 48.8 |
| 1 Raw: all multiplexer observations. Clean: remaining after quality filtering (removal of negative initial values; CH₄ capped at 50,000 ppb). % Removed reflects data lost to filtering. Flux units: nmol m⁻² s⁻¹. Median and IQR used given heavily right-skewed flux distributions. | ||||||||||||||||
Making Chart with Average values by Pile and SD,SE for each chamber and trace gas
# CO2
co2_clean %>%
group_by(Chamber_Corrected) %>%
summarise(mean_co2 = mean(FCO2_DRY.LIN, na.rm = TRUE))
## # A tibble: 6 × 2
## Chamber_Corrected mean_co2
## <chr> <dbl>
## 1 C1 127.
## 2 C2 323.
## 3 C3 125.
## 4 C4 156.
## 5 C5 243.
## 6 C6 164.
# CH4
ch4_clean %>%
group_by(Chamber_Corrected) %>%
summarise(mean_ch4 = mean(FCH4_DRY.LIN, na.rm = TRUE))
## # A tibble: 6 × 2
## Chamber_Corrected mean_ch4
## <chr> <dbl>
## 1 C1 2435.
## 2 C2 11190.
## 3 C3 418.
## 4 C4 1684.
## 5 C5 4339.
## 6 C6 4720.
# N2O
N2O_clean %>%
group_by(Chamber_Corrected) %>%
summarise(mean_n2o = mean(FN2O_DRY.LIN, na.rm = TRUE))
## # A tibble: 6 × 2
## Chamber_Corrected mean_n2o
## <chr> <dbl>
## 1 C1 112.
## 2 C2 254.
## 3 C3 82.0
## 4 C4 78.4
## 5 C5 97.9
## 6 C6 126.
pile_vars_co2 <- co2_raw %>%
filter(!is.na(FCO2_DRY.LIN)) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_co2 = mean(FCO2_DRY.LIN),
sd_co2 = sd(FCO2_DRY.LIN),
se_co2 = sd_co2/sqrt(n_obs)
)
pile_vars_ch4 <- ch4_raw %>%
filter(!is.na(FCH4_DRY.LIN),
CH4_DRY.initial_value >= 0,
CH4_DRY.initial_value <= 50000) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_ch4 = mean(FCH4_DRY.LIN),
sd_ch4 = sd(FCH4_DRY.LIN),
se_ch4 = sd_ch4 / sqrt(n_obs)
)
pile_vars_n2o <- n2o_raw %>%
filter(!is.na(FN2O_DRY.LIN)) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_n2o = mean(FN2O_DRY.LIN),
sd_n2o = sd(FN2O_DRY.LIN),
se_n2o = sd_n2o/sqrt(n_obs)
)
# CH4
ch4_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_ch4 = median(FCH4_DRY.LIN, na.rm = TRUE),
iqr_ch4 = IQR(FCH4_DRY.LIN, na.rm = TRUE),
max_ch4 = max(FCH4_DRY.LIN, na.rm = TRUE)
)
## # A tibble: 6 × 4
## Chamber_Corrected median_ch4 iqr_ch4 max_ch4
## <chr> <dbl> <dbl> <dbl>
## 1 C1 1032. 2447. 49979.
## 2 C2 9452. 12938. 89460.
## 3 C3 265. 271. 14499.
## 4 C4 721. 1982. 24204.
## 5 C5 2646. 4823. 73779.
## 6 C6 2055. 5206. 41800.
# CO2
co2_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_co2 = median(FCO2_DRY.LIN, na.rm = TRUE),
iqr_co2 = IQR(FCO2_DRY.LIN, na.rm = TRUE),
max_co2 = max(FCO2_DRY.LIN, na.rm = TRUE)
)
## # A tibble: 6 × 4
## Chamber_Corrected median_co2 iqr_co2 max_co2
## <chr> <dbl> <dbl> <dbl>
## 1 C1 111. 97.7 1100.
## 2 C2 241. 292. 2312.
## 3 C3 93.3 111. 548.
## 4 C4 116. 135. 1911.
## 5 C5 196. 209. 1800.
## 6 C6 127. 127. 1272.
# N2O
N2O_clean %>%
group_by(Chamber_Corrected) %>%
summarise(
median_n2o = median(FN2O_DRY.LIN, na.rm = TRUE),
iqr_n2o = IQR(FN2O_DRY.LIN, na.rm = TRUE),
max_n2o = max(FN2O_DRY.LIN, na.rm = TRUE)
)
## # A tibble: 6 × 4
## Chamber_Corrected median_n2o iqr_n2o max_n2o
## <chr> <dbl> <dbl> <dbl>
## 1 C1 55.6 81.1 2578.
## 2 C2 122. 229. 3564.
## 3 C3 37.8 80.0 1621.
## 4 C4 34.4 102. 2513.
## 5 C5 35.1 70.4 1681.
## 6 C6 36.5 87.6 5127.
Displaying Average Values for Each Trace Gas by Pile:
kable(
pile_vars_co2,
digits = 3,
caption = "Summary statistics for co2 flux by pile."
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("bordered", "striped")
)
| Pile | n_obs | mean_co2 | sd_co2 | se_co2 |
|---|---|---|---|---|
| C | 5064 | 190.076 | 202.288 | 2.843 |
| E | 4215 | 191.757 | 193.987 | 2.988 |
kable(
pile_vars_ch4,
digits = 3,
caption = "Summary statistics for ch4 flux by pile."
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("bordered", "striped")
)
| Pile | n_obs | mean_ch4 | sd_ch4 | se_ch4 |
|---|---|---|---|---|
| C | 4252 | 4115.308 | 8180.516 | 125.454 |
| E | 3673 | 2754.121 | 5028.936 | 82.979 |
kable(
pile_vars_n2o,
digits = 3,
caption = "Summary statistics for n2o flux by pile."
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("bordered", "striped")
)
| Pile | n_obs | mean_n2o | sd_n2o | se_n2o |
|---|---|---|---|---|
| C | 6906 | 157.089 | 295.036 | 3.550 |
| E | 6911 | 98.481 | 245.689 | 2.955 |
Creating Daily and Weekly Plots for all Fluxes
# N2O
daily_n2o <- n2o_raw %>%
mutate(DOY = floor(DOY)) %>% # collapse fractional DOY
group_by(Pile, DOY) %>%
summarise(
n_obs = sum(!is.na(FN2O_DRY.LIN)),
n2o_mean = mean(FN2O_DRY.LIN, na.rm = TRUE),
n2o_se = se(FN2O_DRY.LIN),
.groups = "drop"
) %>%
filter(n_obs >= 3) # tweak threshold if needed
# CO2
daily_co2 <- co2_raw %>%
mutate(DOY = floor(DOY)) %>%
group_by(Pile, DOY) %>%
summarise(
n_obs = sum(!is.na(FCO2_DRY.LIN)),
co2_mean = mean(FCO2_DRY.LIN, na.rm = TRUE),
co2_se = se(FCO2_DRY.LIN),
.groups = "drop"
) %>%
filter(n_obs >= 3)
# CH4
daily_ch4 <- ch4_raw %>%
mutate(DOY = floor(DOY)) %>%
group_by(Pile, DOY) %>%
summarise(
n_obs = sum(!is.na(FCH4_DRY.LIN)),
ch4_mean = mean(FCH4_DRY.LIN, na.rm = TRUE),
ch4_se = se(FCH4_DRY.LIN),
.groups = "drop"
) %>%
filter(n_obs >= 3)
weekly_n2o <- n2o_raw %>%
mutate(
week = floor((DOY - 1) / 7) + 1
) %>%
group_by(Pile, week) %>%
summarise(
n_obs = sum(!is.na(FN2O_DRY.LIN)),
n2o_mean = mean(FN2O_DRY.LIN, na.rm = TRUE),
n2o_se = sd(FN2O_DRY.LIN, na.rm = TRUE) / sqrt(n_obs),
DOY = mean(DOY, na.rm = TRUE), # x-position for plotting (optional but useful)
.groups = "drop"
) %>%
filter(n_obs >= 3)
# CO2
weekly_co2 <- co2_raw %>%
mutate(
week = floor((DOY - 1) / 7) + 1
) %>%
group_by(Pile, week) %>%
summarise(
n_obs = sum(!is.na(FCO2_DRY.LIN)),
co2_mean = mean(FCO2_DRY.LIN, na.rm = TRUE),
co2_se = sd(FCO2_DRY.LIN, na.rm = TRUE) / sqrt(n_obs),
DOY = mean(DOY, na.rm = TRUE),
.groups = "drop"
) %>%
filter(n_obs >= 3)
# CH4
weekly_ch4 <- ch4_raw %>%
mutate(
week = floor((DOY - 1) / 7) + 1
) %>%
group_by(Pile, week) %>%
summarise(
n_obs = sum(!is.na(FCH4_DRY.LIN)),
ch4_mean = mean(FCH4_DRY.LIN, na.rm = TRUE),
ch4_se = sd(FCH4_DRY.LIN, na.rm = TRUE) / sqrt(n_obs),
DOY = mean(DOY, na.rm = TRUE),
.groups = "drop"
) %>%
filter(n_obs >= 3)
Plotting Values Daily and Weekly Plots for all tg Fluxes by pile with turning events
fig_co2_daily <- plot_points_err(
daily_co2,
x = DOY, y_mean = co2_mean, y_se = co2_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_co2_daily
fig_co2_weekly <- plot_points_err(
weekly_co2,
x = week, y_mean = co2_mean, y_se = co2_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_co2_weekly
fig_ch4_daily <- plot_points_err(
daily_ch4,
x = DOY, y_mean = ch4_mean, y_se = ch4_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_ch4_daily
fig_ch4_weekly <- plot_points_err(
weekly_ch4,
x = week, y_mean = ch4_mean, y_se = ch4_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_ch4_weekly
fig_n2o_daily <- plot_points_err(
daily_n2o,
x = DOY, y_mean = n2o_mean, y_se = n2o_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_n2o_daily
fig_n2o_weekly <- plot_points_err(
weekly_n2o,
x = week, y_mean = n2o_mean, y_se = n2o_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_n2o_weekly
Co2 100 year Equivalence Values
Co2 1 Ch4 28 N2o 265
Step 1: using flux data with covars cleaning again but together not individually to only use measurements that happened at the same time.
Flux_All_Gases_QC <- Flux_Data_With_Covars %>%
filter(
# CO2
FCO2_DRY.LIN > 0,
FCO2_DRY.LIN_R2 > 0.9,
# CH4
FCH4_DRY.LIN > 0,
FCH4_DRY.LIN_R2 > 0.9,
# N2O
FN2O_DRY.LIN > 0,
FN2O_DRY.LIN_R2 > 0.9
)
Flux_Data_With_Covars %>%
summarise(
total_obs = n(),
pass_all = sum(
FCO2_DRY.LIN > 0 & FCO2_DRY.LIN_R2 > 0.9 &
FCH4_DRY.LIN > 0 & FCH4_DRY.LIN_R2 > 0.9 &
FN2O_DRY.LIN > 0 & FN2O_DRY.LIN_R2 > 0.9,
na.rm = TRUE
),
percent_retained = 100 * pass_all / total_obs
)
## # A tibble: 1 × 3
## total_obs pass_all percent_retained
## <int> <int> <dbl>
## 1 21090 5558 26.4
Flux_All_Gases_QC <- Flux_All_Gases_QC %>%
mutate(
# keep units explicit
CO2_umol = FCO2_DRY.LIN, # µmol m-2 s-1
N2O_umol = FN2O_DRY.LIN, # µmol m-2 s-1
CH4_umol = FCH4_DRY.LIN * 0.001, # nmol -> µmol
# GWP100 CO2-equivalent (µmol CO2-eq m-2 s-1)
GHG_CO2e_umol = CO2_umol + 28 * CH4_umol + 265 * N2O_umol
)
Summary Stats of Co2e by pile when looking at measurements where all three gases passed qa/qc
pile_vars_co2e <- Flux_All_Gases_QC %>%
filter(!is.na(GHG_CO2e_umol)) %>%
group_by(Pile) %>%
summarise(
n_obs = n(),
mean_CO2e = mean(GHG_CO2e_umol),
sd_CO2e = sd(GHG_CO2e_umol),
se_CO2e = sd_CO2e/sqrt(n_obs)
)
kable(
pile_vars_co2e,
digits = 3,
caption = "Summary statistics for co2e flux by pile."
) %>%
kable_styling(
full_width = FALSE,
position = "center",
bootstrap_options = c("bordered", "striped")
)
| Pile | n_obs | mean_CO2e | sd_CO2e | se_CO2e |
|---|---|---|---|---|
| C | 3269 | 59570.66 | 101672.24 | 1778.259 |
| E | 2289 | 40046.13 | 96185.23 | 2010.414 |
DOY and Week Plots for measurements that pass cleaning for all TGs GHG potential co2 equivalent
# daily co2e
daily_co2e <- Flux_All_Gases_QC %>%
mutate(DOY = floor(DOY)) %>%
group_by(Pile, DOY) %>%
summarise(
n_obs = sum(!is.na(GHG_CO2e_umol)),
co2e_mean = mean(GHG_CO2e_umol, na.rm = TRUE),
co2e_se = sd(GHG_CO2e_umol, na.rm = TRUE)/sqrt(n_obs),
.groups = "drop"
) %>%
filter(n_obs >= 3)
# weekly co2e
weekly_co2e <- Flux_All_Gases_QC %>%
mutate(
week = floor((DOY - 1) / 7) + 1
) %>%
group_by(Pile, week) %>%
summarise(
n_obs = sum(!is.na(GHG_CO2e_umol)),
co2e_mean = mean(GHG_CO2e_umol, na.rm = TRUE),
co2e_se = sd(GHG_CO2e_umol, na.rm = TRUE) / sqrt(n_obs),
DOY = mean(DOY, na.rm = TRUE),
.groups = "drop"
) %>%
filter(n_obs >= 3)
# CO2
fig__co2e_daily <- plot_points_err(
daily_co2e,
x = DOY, y_mean = co2e_mean, y_se = co2e_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig__co2e_daily
fig_co2e_weekly <- plot_points_err(
weekly_co2e,
x = week, y_mean =co2e_mean, y_se = co2e_se,
group = Pile,
cols = pile_cols, shapes = pile_shapes,
point_size = 1.4,
vlines_red = turns_both_red_week,
vlines_black = turns_control_black_week,
vline_red_linetype = "solid",
vline_black_linetype = "dashed"
)
fig_co2e_weekly
Average ghg per day (individual per gas NOT measurements that all tgs pass cleaning)
daily_all3 <- daily_co2 %>%
select(Pile, DOY, n_obs_co2 = n_obs, co2_mean, co2_se) %>%
inner_join(
daily_ch4 %>% select(Pile, DOY, n_obs_ch4 = n_obs, ch4_mean, ch4_se),
by = c("Pile", "DOY")
) %>%
inner_join(
daily_n2o %>% select(Pile, DOY, n_obs_n2o = n_obs, n2o_mean, n2o_se),
by = c("Pile", "DOY")
)
daily_all3 <- daily_all3 %>%
mutate(
ch4_mean_umol = ch4_mean * 0.001,
ch4_se_umol = ch4_se * 0.001,
# CO2-equivalent in µmol CO2-eq m-2 s-1 (using your factors)
co2e_mean = co2_mean + 28 * ch4_mean_umol + 273 * n2o_mean
)
# not going to error bars for this as it is messy but for now we assume they are independent so each one is one measurement and not correlated which is not true:
daily_all3 <- daily_all3 %>%
mutate(
co2e_se = sqrt(
co2_se^2 +
(28 * ch4_se_umol)^2 +
(273 * n2o_se)^2
)
)
fig_co2e_daily_all3 <- plot_points_err(
daily_all3,
x = DOY,
y_mean = co2e_mean,
y_se = co2e_se,
group = Pile,
cols = pile_cols,
shapes = pile_shapes,
point_size = 1.6,
vlines_red = turns_both_red,
vlines_black = turns_control_black,
vline_red_linetype = "solid",
vline_black_linetype = "dashed",
x_lab = "Day of year",
y_lab = expression("CO"[2]*"-eq flux ("*mu*"mol m"^-2*" s"^-1*")"),
legend_title = "Pile"
)
fig_co2e_daily_all3
This is good, I don’t think I need to do a weekly version. Depending on
what we want to do we can revisit.
Now we have looked at the differences in each trace gas and all of them together as well as the baseline differences between the piles (temp, moisture). We did not plot other variables like bulk density but the limited number of samples makes this a little difficult. - Not sure how we should include this, in other parts I have this as just a mean , sd , se.
Variable Unit Value SD SE n Bulk Density C g/cm^3 0.17 3.58 1.03 12 Bulk Density E g/cm^3 0.20 5.17 1.49 12
Basic Model Building:
Numerous models have been employed throughout the process of writing this paper: Here I will present all the models, explain them, and then we can choose the model which best explains the data.
Model 1: Linear Mixed Effects Model:
Here we are looking at the magnitude of fluxes between piles taking into account random effects from chambers and an auto correlation structure.
Creating LME for each of the trace gases and co2 equivalent: with auto correlation and variance structure.
# Log transforming co2 data:
# log transforming flux data for each flux, grouping chambers together and arranging by doy
dat_co2 <- co2_clean %>%
mutate(log_flux = log(FCO2_DRY.LIN))%>%
arrange(Chamber_Corrected, DOY) %>%
group_by(Chamber_Corrected) %>%
mutate(t_index = row_number()) %>%
ungroup()
# applying model with random effects and autocorrelation
m_co2 <- lme(
log_flux ~ Pile,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2,
method = "REML"
)
# log transforming flux data for each flux, grouping chambers together and arranging by doy
dat_ch4 <- ch4_clean %>%
mutate(log_flux = log(FCH4_DRY.LIN))%>%
arrange(Chamber_Corrected, DOY) %>%
group_by(Chamber_Corrected) %>%
mutate(t_index = row_number()) %>%
ungroup()
# applying model
m_ch4 <- lme(
log_flux ~ Pile,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4,
method = "REML"
)
# log transforming flux data for each flux, grouping chambers together and arranging by doy
dat_n2o <- N2O_clean %>%
mutate(log_flux = log(FN2O_DRY.LIN))%>%
arrange(Chamber_Corrected, DOY) %>%
group_by(Chamber_Corrected) %>%
mutate(t_index = row_number()) %>%
ungroup()
# applying model
m_n2o <- lme(
log_flux ~ Pile,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o,
method = "REML"
)
dat_co2e <- Flux_All_Gases_QC %>%
mutate(log_flux = log(GHG_CO2e_umol))%>%
arrange(Chamber_Corrected, DOY) %>%
group_by(Chamber_Corrected) %>%
mutate(t_index = row_number()) %>%
ungroup()
# applying model
m_co2e <- lme(
log_flux ~ Pile,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e,
method = "REML"
)
summary(m_co2)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.86578656 0.2516751 9116 19.33360059 1.191418e-81
## PileE -0.01405495 0.3589091 4 -0.03916019 9.706392e-01
summary(m_ch4)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 6.9920703 0.7160137 8999 9.7652744 2.049989e-22
## PileE -0.5749777 1.0402128 4 -0.5527501 6.098692e-01
summary(m_n2o)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.0477276 0.2135637 13810 18.95326 4.177942e-79
## PileE -0.4672754 0.3152133 4 -1.48241 2.123687e-01
summary(m_co2e)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 9.9295460 0.3194308 5552 31.0851212 1.003075e-195
## PileE -0.3951411 0.4905084 4 -0.8055747 4.656491e-01
extract_pile_effect <- function(model, gas_name) {
tt <- summary(model)$tTable
beta <- tt["PileE", "Value"]
se <- tt["PileE", "Std.Error"]
pval <- tt["PileE", "p-value"]
data.frame(
Gas = gas_name,
Ratio_E_over_C = exp(beta),
CI_low = exp(beta - 1.96 * se),
CI_high = exp(beta + 1.96 * se),
p_value = pval
)
}
results_lme <- bind_rows(
extract_pile_effect(m_co2, "CO2"),
extract_pile_effect(m_ch4, "CH4"),
extract_pile_effect(m_n2o, "N2O"),
extract_pile_effect(m_co2e, "CO2e")
)
results_lme
## Gas Ratio_E_over_C CI_low CI_high p_value
## 1 CO2 0.9860434 0.48796251 1.992533 0.9706392
## 2 CH4 0.5627174 0.07325602 4.322523 0.6098692
## 3 N2O 0.6267075 0.33787022 1.162465 0.2123687
## 4 CO2e 0.6735850 0.25755097 1.761658 0.4656491
knitr::kable(
results_lme,
digits = 3,
caption = "Pile effect estimates from log-scale linear mixed-effects models."
)
| Gas | Ratio_E_over_C | CI_low | CI_high | p_value |
|---|---|---|---|---|
| CO2 | 0.986 | 0.488 | 1.993 | 0.971 |
| CH4 | 0.563 | 0.073 | 4.323 | 0.610 |
| N2O | 0.627 | 0.338 | 1.162 | 0.212 |
| CO2e | 0.674 | 0.258 | 1.762 | 0.466 |
dat_co2 <- dat_co2 %>%
group_by(Chamber_Corrected) %>%
mutate(
Chamber_Elevation_filled = ifelse(
is.na(Chamber_Elevation),
median(Chamber_Elevation, na.rm = TRUE), # or mean(...)
Chamber_Elevation
)
) %>%
ungroup()
dat_co2 <- dat_co2 %>%
mutate(Chamber_Elevation_sc = as.numeric(scale(Chamber_Elevation_filled)))
m_co2_elev <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2,
method = "REML",
na.action = na.omit
)
dat_ch4 <- dat_ch4 %>%
group_by(Chamber_Corrected) %>%
mutate(
Chamber_Elevation_filled = ifelse(
is.na(Chamber_Elevation),
median(Chamber_Elevation, na.rm = TRUE), # or mean(...)
Chamber_Elevation
)
) %>%
ungroup()
dat_ch4 <- dat_ch4 %>%
mutate(Chamber_Elevation_sc = as.numeric(scale(Chamber_Elevation_filled)))
m_ch4_elev <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4,
method = "REML",
na.action = na.omit
)
dat_n2o <- dat_n2o %>%
group_by(Chamber_Corrected) %>%
mutate(
Chamber_Elevation_filled = ifelse(
is.na(Chamber_Elevation),
median(Chamber_Elevation, na.rm = TRUE), # or mean(...)
Chamber_Elevation
)
) %>%
ungroup()
dat_n2o <- dat_n2o %>%
mutate(Chamber_Elevation_sc = as.numeric(scale(Chamber_Elevation_filled)))
m_n2o_elev <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o,
method = "REML",
na.action = na.omit
)
dat_co2e <- dat_co2e %>%
group_by(Chamber_Corrected) %>%
mutate(
Chamber_Elevation_filled = ifelse(
is.na(Chamber_Elevation),
median(Chamber_Elevation, na.rm = TRUE), # or mean(...)
Chamber_Elevation
)
) %>%
ungroup()
dat_co2e <- dat_co2e %>%
mutate(Chamber_Elevation_sc = as.numeric(scale(Chamber_Elevation_filled)))
m_co2e_elev <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e,
method = "REML",
na.action = na.omit
)
results_lme_elv <- bind_rows(
extract_pile_effect(m_co2e_elev, "CO2"),
extract_pile_effect(m_ch4_elev, "CH4"),
extract_pile_effect(m_n2o_elev, "N2O"),
extract_pile_effect(m_co2e_elev, "CO2e")
)
results_lme_elv
## Gas Ratio_E_over_C CI_low CI_high p_value
## 1 CO2 0.6089703 0.20520084 1.807229 0.4220016
## 2 CH4 0.5512935 0.05486585 5.539411 0.6395743
## 3 N2O 0.6640129 0.32085816 1.374168 0.3317564
## 4 CO2e 0.6089703 0.20520084 1.807229 0.4220016
Adding temperature to the LME_Elevation_Included - Looking at interaction effect of temperature - emissions are more effected by temperature in the control vs experimental pile which to me makes sense but there are alot of factors - such as different ranges in each pile for temperature. Overall this goes with what we have seen.
Using ML to look at interactions:
Temperature values ≤ 0°C were removed and remaining missing values replaced with the column mean, then z-score standardised. A linear mixed effects model was fitted with log CO₂ flux as the response, including pile treatment, scaled temperature, their interaction, and chamber elevation as fixed effects.
Chamber identity was included as a random intercept, with AR(1) autocorrelation within chambers and pile-level variance heterogeneity accounted for.
The interaction term specifically tests whether temperature drives flux differently between the two piles.
Model fit was formally compared against the elevation-only baseline using a likelihood ratio test (models refitted with ML for comparison).
Predicted flux trajectories were generated across the full temperature range at the population level, holding elevation at its mean, and back-transformed from log scale for visualization.The final plot overlays raw observations with model predictions for each pile, allowing direct visual comparison of the temperature-flux relationship between treatment groups.
dat_co2_int <- dat_co2 %>%
mutate(Pile = factor(Pile, levels = c("C", "E"))) %>%
# remove invalid temperature values
mutate(
TS_1.initial_value = ifelse(
TS_1.initial_value <= 0,
NA,
TS_1.initial_value
)
) %>%
# fill missing values with the mean
mutate(
TS_1_filled = ifelse(
is.na(TS_1.initial_value),
mean(TS_1.initial_value, na.rm = TRUE),
TS_1.initial_value
)
) %>%
# scale AFTER filling
mutate(
pile_temp_sc = as.numeric(scale(TS_1_filled))
)
m_co2_temp_int <- lme(
log_flux ~ Pile * pile_temp_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_int,
method = "REML",
na.action = na.omit
)
summary(m_co2_temp_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.874121106 0.28817080 9113 16.91400094 3.249538e-63
## PileE -0.004580671 0.40909527 4 -0.01119708 9.916024e-01
## pile_temp_sc 0.402285365 0.02836424 9113 14.18283578 3.532553e-45
## Chamber_Elevation_sc 0.098878046 0.02206649 9113 4.48091457 7.523048e-06
## PileE:pile_temp_sc -0.095941261 0.04377622 9113 -2.19162953 2.843149e-02
m_base_ml <- update(m_co2_elev, method = "ML")
m_int_ml <- update(m_co2_temp_int, method = "ML")
anova(m_base_ml , m_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_base_ml 1 7 4863.047 4912.876 -2424.524
## m_int_ml 2 9 4623.972 4688.038 -2302.986 1 vs 2 243.0756 <.0001
# temperature range (scaled)
temp_seq <- seq(
min(dat_co2_int$pile_temp_sc),
max(dat_co2_int$pile_temp_sc),
length.out = 100
)
newdat <- expand.grid(
pile_temp_sc = temp_seq,
Pile = levels(dat_co2_int$Pile)
)
# hold elevation at its mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict population-level (fixed effects only)
newdat$pred_log <- predict(m_co2_temp_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_co2_int,
aes(x = pile_temp_sc,
y = exp(log_flux),
colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat,
aes(x = pile_temp_sc,
y = pred_flux,
colour = Pile),
linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile temperature (scaled)",
y = expression(CO[2]~flux~(back~transformed)),
colour = "Pile"
)
Looking at ch4 response to temperature by pile: using the same methods as the above chunk
dat_ch4_int <- dat_ch4 %>%
mutate(Pile = factor(Pile, levels = c("C", "E"))) %>%
# remove invalid temperature values
mutate(
TS_1.initial_value = ifelse(
TS_1.initial_value <= 0,
NA,
TS_1.initial_value
)
) %>%
# fill missing values with the mean
mutate(
TS_1_filled = ifelse(
is.na(TS_1.initial_value),
mean(TS_1.initial_value, na.rm = TRUE),
TS_1.initial_value
)
) %>%
# scale AFTER filling
mutate(
pile_temp_sc = as.numeric(scale(TS_1_filled))
)
m_ch4_temp_int <- lme(
log_flux ~ Pile * pile_temp_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_int,
method = "REML",
na.action = na.omit
)
summary(m_ch4_temp_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 6.98706420 0.79567949 8996 8.7812546 1.913390e-18
## PileE -0.56873603 1.14602281 4 -0.4962694 6.457371e-01
## pile_temp_sc -0.01422908 0.03785767 8996 -0.3758573 7.070319e-01
## Chamber_Elevation_sc 0.25677267 0.04009886 8996 6.4034905 1.594647e-10
## PileE:pile_temp_sc 0.37877092 0.07342254 8996 5.1587829 2.538605e-07
m_base_ml <- update(m_ch4_elev, method = "ML")
m_int_ml <- update(m_ch4_temp_int, method = "ML")
anova(m_base_ml , m_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_base_ml 1 7 10002.237 10051.98 -4994.119
## m_int_ml 2 9 9974.029 10037.98 -4978.015 1 vs 2 32.20793 <.0001
# temperature range (scaled)
temp_seq <- seq(
min(dat_ch4_int$pile_temp_sc),
max(dat_ch4_int$pile_temp_sc),
length.out = 100
)
newdat <- expand.grid(
pile_temp_sc = temp_seq,
Pile = levels(dat_ch4_int$Pile)
)
# hold elevation at its mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict population-level (fixed effects only)
newdat$pred_log <- predict(m_ch4_temp_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_ch4_int,
aes(x = pile_temp_sc,
y = exp(log_flux),
colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat,
aes(x = pile_temp_sc,
y = pred_flux,
colour = Pile),
linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile temperature (scaled)",
y = expression(Ch[4]~flux~(back~transformed)),
colour = "Pile"
)
For both ch4 and co2 emissions the control pile flux respond more to increases in temperatures: It must be noted that is the not the same scale in terms of temps (as the control pile had a higher peak in temp - but it is similar)
Looking at N2o response to temperature by pile using the same methods as the above two piles:
dat_n2o_int <- dat_n2o %>%
mutate(Pile = factor(Pile, levels = c("C", "E"))) %>%
# remove invalid temperature values
mutate(
TS_1.initial_value = ifelse(
TS_1.initial_value <= 0,
NA,
TS_1.initial_value
)
) %>%
# fill missing values with the mean
mutate(
TS_1_filled = ifelse(
is.na(TS_1.initial_value),
mean(TS_1.initial_value, na.rm = TRUE),
TS_1.initial_value
)
) %>%
# scale AFTER filling
mutate(
pile_temp_sc = as.numeric(scale(TS_1_filled))
)
m_n2o_temp_int <- lme(
log_flux ~ Pile * pile_temp_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_int,
method = "REML",
na.action = na.omit
)
summary(m_n2o_temp_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.03288917 0.25383263 13807 15.8879854 2.421479e-56
## PileE -0.41736205 0.37020373 4 -1.1273848 3.226284e-01
## pile_temp_sc -0.01430343 0.03251108 13807 -0.4399557 6.599760e-01
## Chamber_Elevation_sc 0.25641189 0.03787699 13807 6.7695953 1.343577e-11
## PileE:pile_temp_sc -0.02934608 0.05247690 13807 -0.5592191 5.760213e-01
m_base_ml <- update(m_n2o_elev, method = "ML")
m_int_ml <- update(m_n2o_temp_int, method = "ML")
anova(m_base_ml , m_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_base_ml 1 7 10628.95 10681.69 -5307.477
## m_int_ml 2 9 10631.65 10699.45 -5306.823 1 vs 2 1.309517 0.5196
# temperature range (scaled)
temp_seq <- seq(
min(dat_n2o_int$pile_temp_sc),
max(dat_n2o_int$pile_temp_sc),
length.out = 100
)
newdat <- expand.grid(
pile_temp_sc = temp_seq,
Pile = levels(dat_n2o_int$Pile)
)
# hold elevation at its mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict population-level (fixed effects only)
newdat$pred_log <- predict(m_n2o_temp_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_n2o_int,
aes(x = pile_temp_sc,
y = exp(log_flux),
colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat,
aes(x = pile_temp_sc,
y = pred_flux,
colour = Pile),
linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile temperature (scaled)",
y = expression(n2o~flux~(back~transformed)),
colour = "Pile"
)
Less of an effect of temperature for N2o - not significant whereas ch4 and co2 whereas
Co2e response to temperature following the same procedure as for each tg
dat_co2e_int <- dat_co2e %>%
mutate(Pile = factor(Pile, levels = c("C", "E"))) %>%
# remove invalid temperature values
mutate(
TS_1.initial_value = ifelse(
TS_1.initial_value <= 0,
NA,
TS_1.initial_value
)
) %>%
# fill missing values with the mean
mutate(
TS_1_filled = ifelse(
is.na(TS_1.initial_value),
mean(TS_1.initial_value, na.rm = TRUE),
TS_1.initial_value
)
) %>%
# scale AFTER filling
mutate(
pile_temp_sc = as.numeric(scale(TS_1_filled))
)
m_co2e_temp_int <- lme(
log_flux ~ Pile * pile_temp_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e_int,
method = "REML",
na.action = na.omit
)
summary(m_co2e_temp_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 9.9584957 0.41351778 5549 24.0823880 5.753037e-122
## PileE -0.5311861 0.61399085 4 -0.8651369 4.357648e-01
## pile_temp_sc 0.1202260 0.04005672 5549 3.0013927 2.699437e-03
## Chamber_Elevation_sc 0.2199742 0.03335744 5549 6.5944558 4.663712e-11
## PileE:pile_temp_sc -0.3739876 0.06384617 5549 -5.8576365 4.964783e-09
m_base_ml <- update(m_co2e_elev, method = "ML")
m_int_ml <- update(m_co2e_temp_int, method = "ML")
anova(m_base_ml , m_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_base_ml 1 7 4827.596 4873.957 -2406.798
## m_int_ml 2 9 4797.290 4856.897 -2389.645 1 vs 2 34.30643 <.0001
# temperature range (scaled)
temp_seq <- seq(
min(dat_co2e_int$pile_temp_sc),
max(dat_co2e_int$pile_temp_sc),
length.out = 100
)
newdat <- expand.grid(
pile_temp_sc = temp_seq,
Pile = levels(dat_co2e_int$Pile)
)
# hold elevation at its mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict population-level (fixed effects only)
newdat$pred_log <- predict(m_co2e_temp_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_co2e_int,
aes(x = pile_temp_sc,
y = exp(log_flux),
colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat,
aes(x = pile_temp_sc,
y = pred_flux,
colour = Pile),
linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile temperature (scaled)",
y = expression(Co2e~flux~(back~transformed)),
colour = "Pile"
)
Similiar result to co2 and ch4 as they dominate: temperature is a factor
as in those models
Adding Moisture data: using same procedure:
# ---- 1) Build modeling dataframe (CO2 + moisture) ----
dat_co2_moist_int <- dat_co2 %>%
mutate(
Pile = factor(Pile, levels = c("C", "E")),
# moisture raw (your chosen column)
moist_raw = SWC_1.initial_value,
# OPTIONAL: screen impossible moisture values (adjust if needed)
moist_raw = ifelse(moist_raw < 0.01 | moist_raw > 0.90, NA, moist_raw),
# mean-fill, then scale
moist_filled = ifelse(
is.na(moist_raw),
mean(moist_raw, na.rm = TRUE),
moist_raw
),
pile_moist_sc = as.numeric(scale(moist_filled))
)
# sanity checks
stopifnot(nrow(dat_co2_moist_int) > 0)
stopifnot(all(!is.na(dat_co2_moist_int$pile_moist_sc)))
stopifnot(!is.null(levels(dat_co2_moist_int$Pile)))
# ---- 2) Fit moisture interaction model (REML) ----
m_co2_moist_int <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_moist_int,
method = "REML",
na.action = na.omit
)
summary(m_co2_moist_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.8817454565 0.27215793 9113 17.93717862 9.861628e-71
## PileE -0.0480349778 0.38774639 4 -0.12388246 9.073840e-01
## pile_moist_sc -0.0097671707 0.01026659 9113 -0.95135523 3.414493e-01
## Chamber_Elevation_sc 0.0752108093 0.02330188 9113 3.22767092 1.252440e-03
## PileE:pile_moist_sc -0.0004864577 0.02385422 9113 -0.02039294 9.837304e-01
# ---- 3) ML refits for valid model comparison ----
m_co2_elev_ml <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_moist_int, # IMPORTANT: same data object
method = "ML",
na.action = na.omit
)
m_co2_moist_int_ml <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_moist_int, # same data object
method = "ML",
na.action = na.omit
)
anova(m_co2_elev_ml, m_co2_moist_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_co2_elev_ml 1 7 4863.047 4912.876 -2424.524
## m_co2_moist_int_ml 2 9 4865.954 4930.020 -2423.977 1 vs 2 1.093577 0.5788
# ---- 4) Prediction grid + plot ----
moist_seq <- seq(
min(dat_co2_moist_int$pile_moist_sc, na.rm = TRUE),
max(dat_co2_moist_int$pile_moist_sc, na.rm = TRUE),
length.out = 100
)
newdat <- expand.grid(
pile_moist_sc = moist_seq,
Pile = levels(dat_co2_moist_int$Pile)
)
# hold elevation at mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict fixed effects only
newdat$pred_log <- predict(m_co2_moist_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_co2_moist_int,
aes(x = pile_moist_sc, y = exp(log_flux), colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat, aes(y = pred_flux), linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile moisture (scaled)",
y = expression(CO[2]~flux~(back~transformed)),
colour = "Pile"
)
Adding moisture does not improve the models co2 flux results
Looking and adding moisture data to ch4 results
dat_ch4_moist_int <- dat_ch4 %>%
mutate(
Pile = factor(Pile, levels = c("C", "E")),
# moisture raw (your chosen column)
moist_raw = SWC_1.initial_value,
# OPTIONAL: screen impossible moisture values (adjust if needed)
moist_raw = ifelse(moist_raw < 0.01 | moist_raw > 0.90, NA, moist_raw),
# mean-fill, then scale
moist_filled = ifelse(
is.na(moist_raw),
mean(moist_raw, na.rm = TRUE),
moist_raw
),
pile_moist_sc = as.numeric(scale(moist_filled))
)
# sanity checks
stopifnot(nrow(dat_ch4_moist_int) > 0)
stopifnot(all(!is.na(dat_ch4_moist_int$pile_moist_sc)))
stopifnot(!is.null(levels(dat_ch4_moist_int$Pile)))
# ---- 2) Fit moisture interaction model (REML) ----
m_ch4_moist_int <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_moist_int,
method = "REML",
na.action = na.omit
)
summary(m_ch4_moist_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 6.989573728 0.80993317 8996 8.6298153 7.192522e-18
## PileE -0.611832264 1.17071403 4 -0.5226146 6.288521e-01
## pile_moist_sc 0.028371806 0.01294715 8996 2.1913551 2.845166e-02
## Chamber_Elevation_sc 0.251379736 0.04004919 8996 6.2767742 3.616420e-10
## PileE:pile_moist_sc 0.008994654 0.02959614 8996 0.3039130 7.612012e-01
# ---- 3) ML refits for valid model comparison ----
m_ch4_elev_ml <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_moist_int, # IMPORTANT: same data object
method = "ML",
na.action = na.omit
)
m_ch4_moist_int_ml <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_moist_int, # same data object
method = "ML",
na.action = na.omit
)
anova(m_ch4_elev_ml, m_ch4_moist_int_ml)
## Model df AIC BIC logLik Test L.Ratio
## m_ch4_elev_ml 1 7 10002.237 10051.98 -4994.119
## m_ch4_moist_int_ml 2 9 9999.443 10063.39 -4990.721 1 vs 2 6.794511
## p-value
## m_ch4_elev_ml
## m_ch4_moist_int_ml 0.0335
# ---- 4) Prediction grid + plot ----
moist_seq <- seq(
min(dat_ch4_moist_int$pile_moist_sc, na.rm = TRUE),
max(dat_ch4_moist_int$pile_moist_sc, na.rm = TRUE),
length.out = 100
)
newdat <- expand.grid(
pile_moist_sc = moist_seq,
Pile = levels(dat_ch4_moist_int$Pile)
)
# hold elevation at mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict fixed effects only
newdat$pred_log <- predict(m_ch4_moist_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_ch4_moist_int,
aes(x = pile_moist_sc, y = exp(log_flux), colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat, aes(y = pred_flux), linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile moisture (scaled)",
y = expression(CH[4]~flux~(back~transformed)),
colour = "Pile"
)
Moisture improves ch4 model - not co2 which is not unexpected but you
would think it would be related. There is not an interaction effect
between pile and moisture that is significant
Looking and adding moisture data to n2o results - moisture impacts n2o flux but not differently by pile
dat_n2o_moist_int <- dat_n2o %>%
mutate(
Pile = factor(Pile, levels = c("C", "E")),
# moisture raw (your chosen column)
moist_raw = SWC_1.initial_value,
# OPTIONAL: screen impossible moisture values (adjust if needed)
moist_raw = ifelse(moist_raw < 0.01 | moist_raw > 0.9, NA, moist_raw),
# mean-fill, then scale
moist_filled = ifelse(
is.na(moist_raw),
mean(moist_raw, na.rm = TRUE),
moist_raw
),
pile_moist_sc = as.numeric(scale(moist_filled))
)
# sanity checks
stopifnot(nrow(dat_n2o_moist_int) > 0)
stopifnot(all(!is.na(dat_n2o_moist_int$pile_moist_sc)))
stopifnot(!is.null(levels(dat_n2o_moist_int$Pile)))
# ---- 2) Fit moisture interaction model (REML) ----
m_n2o_moist_int <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_moist_int,
method = "REML",
na.action = na.omit
)
summary(m_n2o_moist_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.03925726 0.25124353 13807 16.077060 1.231760e-57
## PileE -0.41750662 0.36635316 4 -1.139629 3.180516e-01
## pile_moist_sc 0.02914240 0.01063408 13807 2.740474 6.142959e-03
## Chamber_Elevation_sc 0.25826697 0.03772754 13807 6.845581 7.938154e-12
## PileE:pile_moist_sc -0.02916308 0.01653487 13807 -1.763732 7.779924e-02
# ---- 3) ML refits for valid model comparison ----
m_n2o_elev_ml <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_moist_int, # IMPORTANT: same data object
method = "ML",
na.action = na.omit
)
m_n2o_moist_int_ml <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_moist_int, # same data object
method = "ML",
na.action = na.omit
)
anova(m_n2o_elev_ml, m_n2o_moist_int_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_n2o_elev_ml 1 7 10628.95 10681.69 -5307.477
## m_n2o_moist_int_ml 2 9 10625.40 10693.20 -5303.700 1 vs 2 7.554127 0.0229
# ---- 4) Prediction grid + plot ----
moist_seq <- seq(
min(dat_n2o_moist_int$pile_moist_sc, na.rm = TRUE),
max(dat_n2o_moist_int$pile_moist_sc, na.rm = TRUE),
length.out = 100
)
newdat <- expand.grid(
pile_moist_sc = moist_seq,
Pile = levels(dat_n2o_moist_int$Pile)
)
# hold elevation at mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict fixed effects only
newdat$pred_log <- predict(m_n2o_moist_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_n2o_moist_int,
aes(x = pile_moist_sc, y = exp(log_flux), colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat, aes(y = pred_flux), linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile moisture (scaled)",
y = expression(n2o~flux~(back~transformed)),
colour = "Pile"
)
last of the adding moisture: so far moisture does improve model but
there is not a pile specific moisture impact vs temperature where we see
pile differences to the response.
dat_co2e_moist_int <- dat_co2e %>%
mutate(
Pile = factor(Pile, levels = c("C", "E")),
# moisture raw (your chosen column)
moist_raw = SWC_1.initial_value,
# OPTIONAL: screen impossible moisture values (adjust if needed)
moist_raw = ifelse(moist_raw < 0.01 | moist_raw > 0.9, NA, moist_raw),
# mean-fill, then scale
moist_filled = ifelse(
is.na(moist_raw),
mean(moist_raw, na.rm = TRUE),
moist_raw
),
pile_moist_sc = as.numeric(scale(moist_filled))
)
# sanity checks
stopifnot(nrow(dat_co2e_moist_int) > 0)
stopifnot(all(!is.na(dat_co2e_moist_int$pile_moist_sc)))
stopifnot(!is.null(levels(dat_co2e_moist_int$Pile)))
# ---- 2) Fit moisture interaction model (REML) ----
m_co2e_moist_int <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e_moist_int,
method = "REML",
na.action = na.omit
)
summary(m_co2e_moist_int)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 9.97973965 0.35869327 5549 27.8224891 1.350163e-159
## PileE -0.50195236 0.53945451 4 -0.9304814 4.047934e-01
## pile_moist_sc 0.05330505 0.01264031 5549 4.2170692 2.514364e-05
## Chamber_Elevation_sc 0.21235228 0.03283622 5549 6.4670137 1.085142e-10
## PileE:pile_moist_sc -0.08227815 0.03994925 5549 -2.0595668 3.948652e-02
# ---- 3) ML refits for valid model comparison ----
m_co2e_elev_ml <- lme(
log_flux ~ Pile + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e_moist_int, # IMPORTANT: same data object
method = "ML",
na.action = na.omit
)
m_co2e_moist_int_ml <- lme(
log_flux ~ Pile * pile_moist_sc + Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2e_moist_int, # same data object
method = "ML",
na.action = na.omit
)
anova(m_co2e_elev_ml, m_co2e_moist_int_ml)
## Model df AIC BIC logLik Test L.Ratio
## m_co2e_elev_ml 1 7 4827.596 4873.957 -2406.798
## m_co2e_moist_int_ml 2 9 4813.165 4872.772 -2397.583 1 vs 2 18.43094
## p-value
## m_co2e_elev_ml
## m_co2e_moist_int_ml 1e-04
# ---- 4) Prediction grid + plot ----
moist_seq <- seq(
min(dat_co2e_moist_int$pile_moist_sc, na.rm = TRUE),
max(dat_co2e_moist_int$pile_moist_sc, na.rm = TRUE),
length.out = 100
)
newdat <- expand.grid(
pile_moist_sc = moist_seq,
Pile = levels(dat_co2e_moist_int$Pile)
)
# hold elevation at mean (0 after scaling)
newdat$Chamber_Elevation_sc <- 0
# predict fixed effects only
newdat$pred_log <- predict(m_co2e_moist_int, newdata = newdat, level = 0)
newdat$pred_flux <- exp(newdat$pred_log)
ggplot(dat_co2e_moist_int,
aes(x = pile_moist_sc, y = exp(log_flux), colour = Pile)) +
geom_point(alpha = 0.15, size = 1) +
geom_line(data = newdat, aes(y = pred_flux), linewidth = 1.2) +
theme_bw(base_size = 12) +
theme(panel.grid.minor = element_blank()) +
labs(
x = "Pile moisture (scaled)",
y = expression(Co2e~flux~(back~transformed)),
colour = "Pile"
)
We see the model that included moisture and temp is better (but that is both I think temperature is a bigger part of that so I would take this with a grain of salt.) When looking at co2e there is a pile interaction but that must be from co2 as we looked individually and did not see an interaction for many of the trace gases.
Looking at impacts post turning -
# ── Post-turn flag ────────────────────────────────────────────────────────────
dat_co2_pt <- dat_co2_int %>%
mutate(
PostTurn_0_1d = if_else(
!is.na(DaysSinceLastTurn) &
DaysSinceLastTurn > 0 &
DaysSinceLastTurn <= 1,
1L, 0L
)
)
count(dat_co2_pt, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 4658
## 2 C 1 331
## 3 E 0 4019
## 4 E 1 114
# ── Remove invalid flux values ────────────────────────────────────────────────
dat_co2_pt_use <- dat_co2_pt %>%
filter(!is.na(FCO2_DRY.LIN), is.finite(FCO2_DRY.LIN))
count(dat_co2_pt_use, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 4658
## 2 C 1 331
## 3 E 0 4019
## 4 E 1 114
# ── Descriptive summary ───────────────────────────────────────────────────────
summary_co2 <- dat_co2_pt_use %>%
group_by(Pile, PostTurn_0_1d) %>%
summarise(
n = n(),
mean = mean(FCO2_DRY.LIN),
sd = sd(FCO2_DRY.LIN),
se = sd / sqrt(n),
median = median(FCO2_DRY.LIN),
.groups = "drop"
)
summary_co2
## # A tibble: 4 × 7
## Pile PostTurn_0_1d n mean sd se median
## <fct> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 C 0 4658 190. 179. 2.63 134.
## 2 C 1 331 220. 408. 22.4 95.5
## 3 E 0 4019 194. 196. 3.09 146.
## 4 E 1 114 154. 171. 16.0 126.
# ── Effect size table ─────────────────────────────────────────────────────────
co2_effect <- summary_co2 %>%
select(Pile, PostTurn_0_1d, mean, median) %>%
tidyr::pivot_wider(
names_from = PostTurn_0_1d,
values_from = c(mean, median),
names_prefix = "PostTurn="
) %>%
mutate(
mean_diff = `mean_PostTurn=1` - `mean_PostTurn=0`,
mean_ratio = `mean_PostTurn=1` / `mean_PostTurn=0`,
median_diff = `median_PostTurn=1` - `median_PostTurn=0`,
median_ratio = `median_PostTurn=1` / `median_PostTurn=0`
)
co2_effect
## # A tibble: 2 × 9
## Pile `mean_PostTurn=0` `mean_PostTurn=1` `median_PostTurn=0`
## <fct> <dbl> <dbl> <dbl>
## 1 C 190. 220. 134.
## 2 E 194. 154. 146.
## # ℹ 5 more variables: `median_PostTurn=1` <dbl>, mean_diff <dbl>,
## # mean_ratio <dbl>, median_diff <dbl>, median_ratio <dbl>
# ── Boxplot ───────────────────────────────────────────────────────────────────
ggplot(dat_co2_pt_use,
aes(x = factor(PostTurn_0_1d),
y = FCO2_DRY.LIN,
fill = Pile)) +
geom_boxplot(outlier.alpha = 0.3) +
scale_x_discrete(labels = c("0" = "Not post-turn",
"1" = "0–1 d post-turn")) +
labs(x = "", y = "CO₂ flux") +
theme_bw()
# ── Models (REML) — now include temperature and elevation as covariates ───────
# Interaction model: does the turning effect differ between piles?
m_co2_postturn <- lme(
fixed = log(FCO2_DRY.LIN) ~ Pile * PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_pt_use,
method = "REML",
na.action = na.omit
)
# Additive model: turning effect same across piles
m_co2_no_int <- lme(
fixed = log(FCO2_DRY.LIN) ~ Pile + PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_co2_pt_use,
method = "REML",
na.action = na.omit
)
# ── Model comparison (must use ML for LRT) ────────────────────────────────────
m_co2_postturn_ml <- update(m_co2_postturn, method = "ML")
m_co2_no_int_ml <- update(m_co2_no_int, method = "ML")
anova(m_co2_no_int_ml, m_co2_postturn_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_co2_no_int_ml 1 9 4619.119 4683.185 -2300.560
## m_co2_postturn_ml 2 10 4595.156 4666.341 -2287.578 1 vs 2 25.96302 <.0001
# ── Final summary (REML estimates) ───────────────────────────────────────────
summary(m_co2_postturn)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 4.87392355 0.28074325 9112 17.36078648 1.903702e-66
## PileE -0.01099229 0.39859501 4 -0.02757758 9.793201e-01
## PostTurn_0_1d 0.01075701 0.04587157 9112 0.23450272 8.146000e-01
## pile_temp_sc 0.36841119 0.02166840 9112 17.00223109 7.582838e-64
## Chamber_Elevation_sc 0.09407421 0.02204548 9112 4.26727872 1.998735e-05
## PileE:PostTurn_0_1d 0.47823877 0.09372350 9112 5.10265603 3.416751e-07
So if we look at day after turning between the pile there is a huge difference that is statistically sig, if you change this to 3 days there is too much noise but the mean is still higher. This is something that can be used to say that there are differences in how the piles respond to turning based on the treatment. Not sure what to say about this.
The same post turn analysis but for ch4
# ── Post-turn flag ────────────────────────────────────────────────────────────
dat_ch4_pt <- dat_ch4_int %>%
mutate(
PostTurn_0_1d = if_else(
!is.na(DaysSinceLastTurn) &
DaysSinceLastTurn > 0 &
DaysSinceLastTurn <= 1,
1L, 0L
)
)
count(dat_ch4_pt, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 4481
## 2 C 1 398
## 3 E 0 3998
## 4 E 1 128
# ── Remove invalid flux values ────────────────────────────────────────────────
dat_ch4_pt_use <- dat_ch4_pt %>%
filter(!is.na(FCH4_DRY.LIN), is.finite(FCH4_DRY.LIN))
count(dat_ch4_pt_use, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 4481
## 2 C 1 398
## 3 E 0 3998
## 4 E 1 128
# ── Descriptive summary ───────────────────────────────────────────────────────
summary_ch4 <- dat_ch4_pt_use %>%
group_by(Pile, PostTurn_0_1d) %>%
summarise(
n = n(),
mean = mean(FCH4_DRY.LIN),
sd = sd(FCH4_DRY.LIN),
se = sd / sqrt(n),
median = median(FCH4_DRY.LIN),
.groups = "drop"
)
summary_ch4
## # A tibble: 4 × 7
## Pile PostTurn_0_1d n mean sd se median
## <fct> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 C 0 4481 5591. 9321. 139. 1260.
## 2 C 1 398 6488. 14168. 710. 1377.
## 3 E 0 3998 3661. 6009. 95.0 1469.
## 4 E 1 128 2174. 1831. 162. 1616.
# ── Effect size table ─────────────────────────────────────────────────────────
ch4_effect <- summary_ch4 %>%
select(Pile, PostTurn_0_1d, mean, median) %>%
tidyr::pivot_wider(
names_from = PostTurn_0_1d,
values_from = c(mean, median),
names_prefix = "PostTurn="
) %>%
mutate(
mean_diff = `mean_PostTurn=1` - `mean_PostTurn=0`,
mean_ratio = `mean_PostTurn=1` / `mean_PostTurn=0`,
median_diff = `median_PostTurn=1` - `median_PostTurn=0`,
median_ratio = `median_PostTurn=1` / `median_PostTurn=0`
)
ch4_effect
## # A tibble: 2 × 9
## Pile `mean_PostTurn=0` `mean_PostTurn=1` `median_PostTurn=0`
## <fct> <dbl> <dbl> <dbl>
## 1 C 5591. 6488. 1260.
## 2 E 3661. 2174. 1469.
## # ℹ 5 more variables: `median_PostTurn=1` <dbl>, mean_diff <dbl>,
## # mean_ratio <dbl>, median_diff <dbl>, median_ratio <dbl>
# ── Boxplot ───────────────────────────────────────────────────────────────────
ggplot(dat_ch4_pt_use,
aes(x = factor(PostTurn_0_1d),
y = FCH4_DRY.LIN,
fill = Pile)) +
geom_boxplot(outlier.alpha = 0.3) +
scale_x_discrete(labels = c("0" = "Not post-turn",
"1" = "0–1 d post-turn")) +
labs(x = "", y = "CH₄ flux") + # fixed: was labelled CO₂
theme_bw()
# ── Models (REML) — include temperature and elevation as covariates ───────────
m_ch4_postturn <- lme(
fixed = log(FCH4_DRY.LIN) ~ Pile * PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_pt_use,
method = "REML",
na.action = na.omit
)
m_ch4_no_int <- lme(
fixed = log(FCH4_DRY.LIN) ~ Pile + PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_ch4_pt_use,
method = "REML",
na.action = na.omit
)
# ── Model comparison (ML required for LRT) ────────────────────────────────────
m_ch4_postturn_ml <- update(m_ch4_postturn, method = "ML")
m_ch4_no_int_ml <- update(m_ch4_no_int, method = "ML")
anova(m_ch4_no_int_ml, m_ch4_postturn_ml)
## Model df AIC BIC logLik Test L.Ratio p-value
## m_ch4_no_int_ml 1 9 9998.492 10062.44 -4990.246
## m_ch4_postturn_ml 2 10 9991.386 10062.44 -4985.693 1 vs 2 9.106052 0.0025
# ── Final summary (REML estimates) ───────────────────────────────────────────
summary(m_ch4_postturn)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 6.99468226 0.81693312 8995 8.5621235 1.290747e-17
## PileE -0.61117942 1.17816867 4 -0.5187538 6.313099e-01
## PostTurn_0_1d -0.12770462 0.05355946 8995 -2.3843523 1.712982e-02
## pile_temp_sc 0.07310311 0.03283966 8995 2.2260615 2.603474e-02
## Chamber_Elevation_sc 0.26270661 0.04030089 8995 6.5186300 7.476646e-11
## PileE:PostTurn_0_1d 0.43132846 0.14287812 8995 3.0188560 2.544423e-03
this is less clear than co2 but still stat sig - so ch4 emission post turning differ by pile and thus we should include interaction term.
n2o 1 day post turn analysis: does it differ between piles ?
# ── Post-turn flag ────────────────────────────────────────────────────────────
dat_n2o_pt <- dat_n2o_int %>%
mutate(
PostTurn_0_1d = if_else(
!is.na(DaysSinceLastTurn) &
DaysSinceLastTurn > 0 &
DaysSinceLastTurn <= 1,
1L, 0L
)
)
count(dat_n2o_pt, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 6397
## 2 C 1 509
## 3 E 0 6641
## 4 E 1 269
# ── Remove invalid flux values ────────────────────────────────────────────────
dat_n2o_pt_use <- dat_n2o_pt %>%
filter(!is.na(FN2O_DRY.LIN), is.finite(FN2O_DRY.LIN))
count(dat_n2o_pt_use, Pile, PostTurn_0_1d)
## # A tibble: 4 × 3
## Pile PostTurn_0_1d n
## <fct> <int> <int>
## 1 C 0 6397
## 2 C 1 509
## 3 E 0 6641
## 4 E 1 269
# ── Descriptive summary ───────────────────────────────────────────────────────
summary_n2o <- dat_n2o_pt_use %>%
group_by(Pile, PostTurn_0_1d) %>%
summarise(
n = n(),
mean = mean(FN2O_DRY.LIN),
sd = sd(FN2O_DRY.LIN),
se = sd / sqrt(n),
median = median(FN2O_DRY.LIN),
.groups = "drop"
)
summary_n2o
## # A tibble: 4 × 7
## Pile PostTurn_0_1d n mean sd se median
## <fct> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 C 0 6397 154. 298. 3.73 61.5
## 2 C 1 509 200. 247. 10.9 111.
## 3 E 0 6641 96.1 247. 3.03 34.7
## 4 E 1 269 154. 195. 11.9 89.9
# ── Effect size table ─────────────────────────────────────────────────────────
n2o_effect <- summary_n2o %>%
select(Pile, PostTurn_0_1d, mean, median) %>%
tidyr::pivot_wider(
names_from = PostTurn_0_1d,
values_from = c(mean, median),
names_prefix = "PostTurn="
) %>%
mutate(
mean_diff = `mean_PostTurn=1` - `mean_PostTurn=0`,
mean_ratio = `mean_PostTurn=1` / `mean_PostTurn=0`,
median_diff = `median_PostTurn=1` - `median_PostTurn=0`,
median_ratio = `median_PostTurn=1` / `median_PostTurn=0`
)
n2o_effect
## # A tibble: 2 × 9
## Pile `mean_PostTurn=0` `mean_PostTurn=1` `median_PostTurn=0`
## <fct> <dbl> <dbl> <dbl>
## 1 C 154. 200. 61.5
## 2 E 96.1 154. 34.7
## # ℹ 5 more variables: `median_PostTurn=1` <dbl>, mean_diff <dbl>,
## # mean_ratio <dbl>, median_diff <dbl>, median_ratio <dbl>
# ── Boxplot ───────────────────────────────────────────────────────────────────
ggplot(dat_n2o_pt_use,
aes(x = factor(PostTurn_0_1d),
y = FN2O_DRY.LIN,
fill = Pile)) +
geom_boxplot(outlier.alpha = 0.3) +
scale_x_discrete(labels = c("0" = "Not post-turn",
"1" = "0–1 d post-turn")) +
labs(x = "", y = "N₂O flux") + # fixed: was labelled CO₂
theme_bw()
# ── Models (REML) — include temperature and elevation as covariates ───────────
m_n2o_postturn <- lme(
fixed = log(FN2O_DRY.LIN) ~ Pile * PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_pt_use,
method = "REML",
na.action = na.omit
)
m_n2o_no_int <- lme(
fixed = log(FN2O_DRY.LIN) ~ Pile + PostTurn_0_1d +
pile_temp_sc +
Chamber_Elevation_sc,
random = ~ 1 | Chamber_Corrected,
correlation = corAR1(form = ~ t_index | Chamber_Corrected),
weights = varIdent(form = ~ 1 | Pile),
data = dat_n2o_pt_use,
method = "REML",
na.action = na.omit
)
# ── Model comparison (ML required for LRT) ────────────────────────────────────
m_n2o_postturn_ml <- update(m_n2o_postturn, method = "ML")
m_n2o_no_int_ml <- update(m_n2o_no_int, method = "ML")
anova(m_n2o_no_int_ml, m_n2o_postturn_ml)
## Model df AIC BIC logLik Test L.Ratio
## m_n2o_no_int_ml 1 9 10493.78 10561.58 -5237.89
## m_n2o_postturn_ml 2 10 10495.78 10571.11 -5237.89 1 vs 2 2.604356e-07
## p-value
## m_n2o_no_int_ml
## m_n2o_postturn_ml 0.9996
# ── Final summary (REML estimates) ───────────────────────────────────────────
summary(m_n2o_postturn)$tTable
## Value Std.Error DF t-value p-value
## (Intercept) 3.9918411914 0.23661130 13806 16.870881466 3.164463e-63
## PileE -0.4048616493 0.34676474 4 -1.167539854 3.078416e-01
## PostTurn_0_1d 0.5040294456 0.04870305 13806 10.349032067 5.217810e-25
## pile_temp_sc 0.0098176008 0.02558450 13806 0.383732347 7.011827e-01
## Chamber_Elevation_sc 0.1981037776 0.03789944 13806 5.227090447 1.747089e-07
## PileE:PostTurn_0_1d 0.0002533443 0.10052066 13806 0.002520321 9.979891e-01
Hmmmmm this one is weird so I’m not sure how to interpret it, but there is a stat difference p value of .9 at 1 day and .09 at 3 days and then at 7 days we see 0.00 something, this is the opposite of ch4 and co2 which the effect decreases with time.. So it seems n2o does not change immediately after turning, there is a delay vs co2 and ch4 where there is an immediate change, this might be mechanistic as it might be more microbe mediated vs built up release and aeration related ?
So there is a period after turning where n2o is different that is from like 2-3 days to about a week and then they are more similar, this pulse is very interesting.
| Looking at ratios of gases: |
| While the means are not significantly different the ratio of trace gases might be, we would expect the exp pile to have more a higher ch4/co2 ratio compared to the more aerated c pile. |
| ``` r # ── Build ratio dataset ─────────────────────────────────────────────────────── dat_ratio <- Flux_All_Gases_QC %>% mutate( CO2_umol = FCO2_DRY.LIN, CH4_umol = FCH4_DRY.LIN * 0.001 # nmol → µmol ) %>% filter(CO2_umol > 0, CH4_umol > 0) %>% # log undefined at <= 0 mutate( ratio_CO2_CH4 = CO2_umol / CH4_umol, log_ratio = log(ratio_CO2_CH4) ) %>% arrange(Chamber_Corrected, DOY) %>% group_by(Chamber_Corrected) %>% mutate(t_index = row_number()) %>% ungroup() |
| # ── Sanity check ───────────────────────────────────────────────────────────── dat_ratio %>% summarise( n_obs = n(), n_chambers = n_distinct(Chamber_Corrected), min_ratio = min(ratio_CO2_CH4), max_ratio = max(ratio_CO2_CH4) ) ``` |
## # A tibble: 1 × 4 ## n_obs n_chambers min_ratio max_ratio ## <int> <int> <dbl> <dbl> ## 1 5558 6 2.23 115095. |
| ``` r # ── Model ───────────────────────────────────────────────────────────────────── m_ratio <- lme( log_ratio ~ Pile, random = ~ 1 | Chamber_Corrected, correlation = corAR1(form = ~ t_index | Chamber_Corrected), weights = varIdent(form = ~ 1 | Pile), data = dat_ratio, method = “REML” ) |
| summary(m_ratio)$tTable ``` |
## Value Std.Error DF t-value p-value ## (Intercept) 4.7282131 0.6300319 5552 7.5047198 7.129997e-14 ## PileE 0.8996436 0.9315278 4 0.9657721 3.888485e-01 |
| ``` r # ── Pile means and contrast on response scale ───────────────────────────────── emm_link <- emmeans(m_ratio, ~ Pile) ctr_link_ci <- confint(pairs(emm_link)) |
| ratio_E_over_C <- as.data.frame(ctr_link_ci) %>% mutate( ratio = exp(estimate), ratio_low = exp(lower.CL), ratio_high = exp(upper.CL) ) ratio_E_over_C ``` |
## contrast estimate SE df lower.CL upper.CL ratio ratio_low ## 1 C - E -0.8996436 0.9315278 4 -3.48598 1.686692 0.4067146 0.03062375 ## ratio_high ## 1 5.401585 |
r # ── Pile means on response scale ────────────────────────────────────────────── confint(emmeans(m_ratio, ~ Pile, type = "response")) |
## Pile emmean SE df lower.CL upper.CL ## C 4.73 0.630 5 3.11 6.35 ## E 5.63 0.686 4 3.72 7.53 ## ## Degrees-of-freedom method: containment ## Confidence level used: 0.95 |
r # ── Diagnostics ─────────────────────────────────────────────────────────────── plot(m_ratio) |
r qqnorm(resid(m_ratio)); qqline(resid(m_ratio)) |
| We do not have to use specific figures we can edit this, what Zaie has done on the ratios is similiar. There is not a statistical difference in the ratio of co2 to ch4 throughout the whole experimental cycle as there just are not enough df and there is too much variance, what we do see if a large effect size for the whole experiment. |
| The difference by pile for the entire experimental cycle is not significant by pile: the CIs are large: still - mean for Control is higher lower which means the control emitted more ch4 per unit co2 or Pile E numerically shows a higher ratio (relatively less CH₄ per unit CO₂). |
| More can be done with ratios - need to work with Zaie |
Looking at response to turning: slope of max temp post turning declines more quickly in the e vs the c: why is that ?
library(broom)
library(gt)
# ============================================================
# POST-TURN TEMPERATURE ANALYSIS
# Peak temperature after each turning event
# ============================================================
# ── CONTROL PILE (C1, C2, C3) — all turning events ───────────────────────────
# Step 1 - Build post/pre turn window
all_turns_sorted <- sort(c(turns_both_red, turns_control_black))
post_turn_temp <- map_dfr(all_turns_sorted, function(turn) {
Cleaned_Data %>%
filter(Chamber_Corrected %in% c("C1", "C2", "C3"),
DOY.initial_value >= turn - 3,
DOY.initial_value <= turn + 12) %>%
mutate(
turn_event = turn,
days_post_turn = DOY.initial_value - turn
)
})
# Step 2 - Daily averages (min 5 obs, exclude turning day)
daily_avg_turns <- post_turn_temp %>%
mutate(day_bin = floor(days_post_turn)) %>%
filter(day_bin != 0) %>%
group_by(turn_event, Chamber_Corrected, day_bin) %>%
summarise(
daily_mean = mean(pile_temp, na.rm = TRUE),
n_obs = n(),
.groups = "drop"
) %>%
filter(n_obs >= 5)
# Step 3 - Qualify combos (days 1-5 must all be present)
qualified_combos <- daily_avg_turns %>%
filter(day_bin >= 1, day_bin <= 5) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(days_covered = n_distinct(day_bin), .groups = "drop") %>%
filter(days_covered >= 5)
# Step 4 - Pre-turn reference
pre_turn_ref <- daily_avg_turns %>%
filter(day_bin < 0) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
pre_turn_day = max(day_bin),
pre_turn_temp = daily_mean[day_bin == max(day_bin)],
.groups = "drop"
)
# Step 5 - Peak metrics (control)
peak_metrics <- daily_avg_turns %>%
inner_join(qualified_combos %>% select(turn_event, Chamber_Corrected),
by = c("turn_event", "Chamber_Corrected")) %>%
filter(day_bin >= 1) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
peak_temp = max(daily_mean, na.rm = TRUE),
peak_day = day_bin[which.max(daily_mean)],
temp_day1 = ifelse(any(day_bin == 1), daily_mean[day_bin == 1], NA_real_),
.groups = "drop"
) %>%
left_join(pre_turn_ref, by = c("turn_event", "Chamber_Corrected")) %>%
mutate(
spike_magnitude = ifelse(!is.na(pre_turn_temp) & peak_day > 1,
peak_temp - pre_turn_temp, NA_real_),
time_to_peak = ifelse(!is.na(temp_day1) & peak_day > 1 &
peak_temp > temp_day1, peak_day, NA_real_)
) %>%
left_join(
tibble(turn_event = all_turns_sorted,
turn_number = seq_along(all_turns_sorted)),
by = "turn_event"
)
## Warning: There was 1 warning in `summarise()`.
## ℹ In argument: `peak_temp = max(daily_mean, na.rm = TRUE)`.
## ℹ In group 7: `turn_event = 191` `Chamber_Corrected = "C2"`.
## Caused by warning in `max()`:
## ! no non-missing arguments to max; returning -Inf
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# ── EXPERIMENTAL PILE (C4, C5, C6) — both-turned events only ─────────────────
exp_turns_sorted <- sort(turns_both_red)
post_turn_temp_exp <- map_dfr(exp_turns_sorted, function(turn) {
Cleaned_Data %>%
filter(Chamber_Corrected %in% c("C4", "C5", "C6"),
DOY.initial_value >= turn - 3,
DOY.initial_value <= turn + 12) %>%
mutate(
turn_event = turn,
days_post_turn = DOY.initial_value - turn
)
})
daily_avg_turns_exp <- post_turn_temp_exp %>%
mutate(day_bin = floor(days_post_turn)) %>%
filter(day_bin != 0) %>%
group_by(turn_event, Chamber_Corrected, day_bin) %>%
summarise(
daily_mean = mean(pile_temp, na.rm = TRUE),
n_obs = n(),
.groups = "drop"
) %>%
filter(n_obs >= 5)
qualified_combos_exp <- daily_avg_turns_exp %>%
filter(day_bin >= 1, day_bin <= 5) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(days_covered = n_distinct(day_bin), .groups = "drop") %>%
filter(days_covered >= 5)
pre_turn_ref_exp <- daily_avg_turns_exp %>%
filter(day_bin < 0) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
pre_turn_day = max(day_bin),
pre_turn_temp = daily_mean[day_bin == max(day_bin)],
.groups = "drop"
)
peak_metrics_exp <- daily_avg_turns_exp %>%
inner_join(qualified_combos_exp %>% select(turn_event, Chamber_Corrected),
by = c("turn_event", "Chamber_Corrected")) %>%
filter(day_bin >= 1) %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
peak_temp = max(daily_mean, na.rm = TRUE),
peak_day = day_bin[which.max(daily_mean)],
temp_day1 = ifelse(any(day_bin == 1), daily_mean[day_bin == 1], NA_real_),
.groups = "drop"
) %>%
left_join(pre_turn_ref_exp, by = c("turn_event", "Chamber_Corrected")) %>%
mutate(
spike_magnitude = ifelse(!is.na(pre_turn_temp) & peak_day > 1,
peak_temp - pre_turn_temp, NA_real_),
time_to_peak = ifelse(!is.na(temp_day1) & peak_day > 1 &
peak_temp > temp_day1, peak_day, NA_real_)
) %>%
left_join(
tibble(turn_event = exp_turns_sorted,
turn_number = seq_along(exp_turns_sorted)),
by = "turn_event"
)
## Warning: There were 2 warnings in `summarise()`.
## The first warning was:
## ℹ In argument: `peak_temp = max(daily_mean, na.rm = TRUE)`.
## ℹ In group 8: `turn_event = 234` `Chamber_Corrected = "C5"`.
## Caused by warning in `max()`:
## ! no non-missing arguments to max; returning -Inf
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# ── FIGURES — Peak temperature only ──────────────────────────────────────────
p_peak <- ggplot(peak_metrics %>% filter(!is.na(peak_temp)),
aes(x = turn_number, y = peak_temp)) +
# raw chamber points — opaque to show variability
geom_point(aes(colour = Chamber_Corrected),
size = 2.5, alpha = 0.35) +
# chamber lines — faint to connect dots per chamber
geom_line(aes(colour = Chamber_Corrected, group = Chamber_Corrected),
linewidth = 0.5, alpha = 0.25) +
# overall trend line across all chambers
geom_smooth(method = "lm", se = TRUE,
colour = "black", fill = "grey30",
linewidth = 0.9, linetype = "dashed", alpha = 0.15) +
scale_colour_manual(values = chamber_cols) +
scale_x_continuous(breaks = 1:11) +
theme_bw(base_size = 11) +
theme(
legend.position = "top",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Turning event number",
y = "Peak temperature (°C)",
colour = "Chamber",
title = "Control pile — peak post-turn temperature")
p_peak_exp <- ggplot(peak_metrics_exp %>% filter(!is.na(peak_temp)),
aes(x = turn_number, y = peak_temp)) +
geom_point(aes(colour = Chamber_Corrected),
size = 2.5, alpha = 0.35) +
geom_line(aes(colour = Chamber_Corrected, group = Chamber_Corrected),
linewidth = 0.5, alpha = 0.25) +
geom_smooth(method = "lm", se = TRUE,
colour = "black", fill = "grey30",
linewidth = 0.9, linetype = "dashed", alpha = 0.15) +
scale_colour_manual(values = chamber_cols) +
scale_x_continuous(breaks = 1:6) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Turning event number",
y = "Peak temperature (°C)",
colour = "Chamber",
title = "Experimental pile — peak post-turn temperature")
p_peak / p_peak_exp
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
# ── LINEAR MODELS — peak temperature only ────────────────────────────────────
lm_peak <- lm(peak_temp ~ turn_number,
data = peak_metrics %>% filter(!is.na(peak_temp)))
lm_peak_exp <- lm(peak_temp ~ turn_number,
data = peak_metrics_exp %>% filter(!is.na(peak_temp)))
# ── SUMMARY TABLE ─────────────────────────────────────────────────────────────
library(broom)
slopes <- bind_rows(
tidy(lm_peak) %>%
filter(term == "turn_number") %>%
mutate(pile = "Control"),
tidy(lm_peak_exp) %>%
filter(term == "turn_number") %>%
mutate(pile = "Experimental")
) %>%
select(pile, estimate, std.error)
model_stats <- bind_rows(
glance(lm_peak) %>% mutate(pile = "Control"),
glance(lm_peak_exp) %>% mutate(pile = "Experimental")
) %>%
select(pile, r.squared, statistic, p.value, df, df.residual)
left_join(slopes, model_stats, by = "pile") %>%
mutate(across(c(estimate, std.error), ~ round(.x, 2)),
across(c(r.squared, statistic), ~ round(.x, 3)),
p.value = round(p.value, 4)) %>%
gt() %>%
cols_label(
pile = "Pile",
estimate = "Slope (°C/turn)",
std.error = "SE",
r.squared = "R²",
statistic = "F",
p.value = "p-value",
df = "df",
df.residual = "df residual"
) %>%
tab_header(
title = "Linear Model Results — Peak Post-turn Temperature",
subtitle = "Change in peak temperature per successive turning event"
) %>%
tab_style(style = cell_text(weight = "bold"),
locations = cells_body(rows = p.value < 0.05)) %>%
tab_style(style = cell_text(weight = "bold"),
locations = cells_column_labels()) %>%
fmt_number(columns = p.value, decimals = 4) %>%
sub_missing(missing_text = "—") %>%
opt_table_font(font = "Times New Roman")
| Linear Model Results — Peak Post-turn Temperature | |||||||
| Change in peak temperature per successive turning event | |||||||
| Pile | Slope (°C/turn) | SE | R² | F | p-value | df | df residual |
|---|---|---|---|---|---|---|---|
| Control | -1.86 | 0.48 | 0.459 | 15.241 | 0.0010 | 1 | 18 |
| Experimental | -8.88 | 1.36 | 0.842 | 42.662 | 0.0002 | 1 | 8 |
Turning events looking at temp similar analysis to the above needs to be worked on
library(tidyverse)
# All turning events for control chambers
all_turns_sorted <- sort(c(turns_both_red, turns_control_black))
# Filter to control chambers and ALL turning events
control_turns <- post_turn_temp %>%
filter(Chamber_Corrected %in% c("C1", "C2", "C3"),
turn_event %in% all_turns_sorted)
# Check data availability per day per chamber per turn
data_availability <- control_turns %>%
mutate(day_bin = floor(days_post_turn)) %>%
filter(day_bin >= 1, day_bin <= 5) %>%
group_by(turn_event, Chamber_Corrected, day_bin) %>%
summarise(n_obs = n(), .groups = "drop")
# Flag chamber-turn combinations meeting minimum 5 obs per day
qualified_combos <- data_availability %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
days_with_enough = sum(n_obs >= 5),
all_days_covered = days_with_enough >= 5,
.groups = "drop"
) %>%
filter(all_days_covered)
qualified_combos
## # A tibble: 21 × 4
## turn_event Chamber_Corrected days_with_enough all_days_covered
## <dbl> <chr> <int> <lgl>
## 1 159 C1 5 TRUE
## 2 159 C2 5 TRUE
## 3 159 C3 5 TRUE
## 4 173 C2 5 TRUE
## 5 173 C3 5 TRUE
## 6 191 C1 5 TRUE
## 7 191 C2 5 TRUE
## 8 208 C1 5 TRUE
## 9 208 C2 5 TRUE
## 10 208 C3 5 TRUE
## # ℹ 11 more rows
# Fit cubic polynomial for each qualifying chamber-turn combination
poly_metrics <- control_turns %>%
inner_join(qualified_combos %>%
select(turn_event, Chamber_Corrected),
by = c("turn_event", "Chamber_Corrected")) %>%
group_by(turn_event, Chamber_Corrected) %>%
filter(sum(!is.na(pile_temp)) >= 10) %>% # minimum 10 observations to fit poly
group_modify(~ {
df <- .x
# Fit cubic polynomial
fit <- tryCatch(
lm(pile_temp ~ poly(days_post_turn, 3, raw = TRUE), data = df),
error = function(e) NULL
)
if (is.null(fit)) {
return(tibble(
slope_at_turn = NA,
time_to_peak = 0,
peak_temp = NA,
pre_turn_mean = NA
))
}
# Extract coefficients: a + bx + cx^2 + dx^3
coefs <- coef(fit)
a <- coefs[1]
b <- coefs[2]
c <- coefs[3]
d <- coefs[4]
# Slope (first derivative) at day 0: b + 2cx + 3dx^2 at x=0
slope_at_turn <- b
# Find turning points — solve derivative = 0
# b + 2cx + 3dx^2 = 0
# Use numerical approach across post-turn window
x_seq <- seq(0, 12, by = 0.01)
y_seq <- a + b*x_seq + c*x_seq^2 + d*x_seq^3
dy_seq <- b + 2*c*x_seq + 3*d*x_seq^2
# Find where derivative crosses zero (peak) after turn
sign_changes <- which(diff(sign(dy_seq)) < 0) # negative = peak not trough
if (length(sign_changes) == 0 || max(y_seq) <= y_seq[1]) {
# No peak found or polynomial declining throughout — no increase
time_to_peak <- 0
peak_temp <- y_seq[1]
} else {
peak_idx <- sign_changes[1]
time_to_peak <- x_seq[peak_idx]
peak_temp <- y_seq[peak_idx]
}
# Pre-turn mean
pre_turn_mean <- mean(df$pile_temp[df$days_post_turn < 0 &
df$days_post_turn >= -3],
na.rm = TRUE)
tibble(
slope_at_turn = slope_at_turn,
time_to_peak = time_to_peak,
peak_temp = peak_temp,
pre_turn_mean = pre_turn_mean
)
}) %>%
ungroup()
poly_metrics
## # A tibble: 21 × 6
## turn_event Chamber_Corrected slope_at_turn time_to_peak peak_temp
## <dbl> <chr> <dbl> <dbl> <dbl>
## 1 159 C1 0.547 1.27 59.4
## 2 159 C2 2.46 4.76 74.4
## 3 159 C3 0.340 1.93 67.9
## 4 173 C2 -0.737 7.57 51.8
## 5 173 C3 -0.110 6.95 59.6
## 6 191 C1 -0.728 8.4 65.2
## 7 191 C2 110. 0 138.
## 8 208 C1 0.391 0.74 59.4
## 9 208 C2 4.96 2.98 53.3
## 10 208 C3 1.65 1.45 61.4
## # ℹ 11 more rows
## # ℹ 1 more variable: pre_turn_mean <dbl>
# Plot 1 - slope at turn
p1 <- ggplot(poly_metrics,
aes(x = turn_event, y = slope_at_turn,
colour = Chamber_Corrected,
group = Chamber_Corrected)) +
geom_hline(yintercept = 0, linetype = "dashed",
colour = "grey50", linewidth = 0.4) +
geom_point(size = 2.5) +
geom_line(linewidth = 0.6, alpha = 0.7) +
geom_smooth(aes(group = 1), method = "lm",
se = TRUE, colour = "black",
linewidth = 0.8, linetype = "dashed") +
scale_colour_manual(values = chamber_cols) +
theme_bw(base_size = 11) +
theme(legend.position = "top",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)) +
labs(x = "Day of year (turning event)",
y = "Slope at turn (°C/day)",
colour = "Chamber",
title = "Rate of temperature change at moment of turning")
# Plot 2 - time to peak
p2 <- ggplot(poly_metrics,
aes(x = turn_event, y = time_to_peak,
colour = Chamber_Corrected,
group = Chamber_Corrected)) +
geom_point(size = 2.5) +
geom_line(linewidth = 0.6, alpha = 0.7) +
geom_smooth(aes(group = 1), method = "lm",
se = TRUE, colour = "black",
linewidth = 0.8, linetype = "dashed") +
scale_colour_manual(values = chamber_cols) +
theme_bw(base_size = 11) +
theme(legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)) +
labs(x = "Day of year (turning event)",
y = "Days to peak temperature",
colour = "Chamber",
title = "Time to peak temperature after turning")
# Plot 3 - peak temp
p3 <- ggplot(poly_metrics,
aes(x = turn_event, y = peak_temp,
colour = Chamber_Corrected,
group = Chamber_Corrected)) +
geom_point(size = 2.5) +
geom_line(linewidth = 0.6, alpha = 0.7) +
geom_smooth(aes(group = 1), method = "lm",
se = TRUE, colour = "black",
linewidth = 0.8, linetype = "dashed") +
scale_colour_manual(values = chamber_cols) +
theme_bw(base_size = 11) +
theme(legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)) +
labs(x = "Day of year (turning event)",
y = "Peak temperature (°C)",
colour = "Chamber",
title = "Peak temperature reached after turning")
library(patchwork)
p1 / p2 / p3
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Turning events looking at temp similar analysis to the above needs to be worked on
# Step 1 - Daily averages with minimum 5 obs per day
daily_avg_turns <- control_turns %>%
filter(Chamber_Corrected %in% c("C1", "C2", "C3"),
turn_event %in% all_turns_sorted,
days_post_turn >= -2) %>%
mutate(day_bin = floor(days_post_turn)) %>%
group_by(turn_event, Chamber_Corrected, day_bin) %>%
summarise(
daily_mean = mean(pile_temp, na.rm = TRUE),
n_obs = n(),
.groups = "drop"
) %>%
filter(n_obs >= 5) # minimum 5 obs per day
peak_metrics <- daily_avg_turns %>%
group_by(turn_event, Chamber_Corrected) %>%
summarise(
# Use first available post-turn day as reference
first_post_turn_day = min(day_bin[day_bin >= 1], na.rm = TRUE),
temp_at_turn = daily_mean[day_bin == first_post_turn_day],
peak_day = day_bin[which.max(ifelse(day_bin >= 1, daily_mean, NA))],
peak_temp = max(daily_mean[day_bin >= 1], na.rm = TRUE),
# Peak must be after first available day
time_to_peak = ifelse(peak_day > first_post_turn_day &
peak_temp > temp_at_turn,
peak_day - first_post_turn_day, NA_real_),
slope = (peak_temp - temp_at_turn) / time_to_peak,
spike_magnitude = peak_temp - temp_at_turn,
.groups = "drop"
)
## Warning: There was 1 warning in `summarise()`.
## ℹ In argument: `peak_temp = max(daily_mean[day_bin >= 1], na.rm = TRUE)`.
## ℹ In group 11: `turn_event = 191` `Chamber_Corrected = "C2"`.
## Caused by warning in `max()`:
## ! no non-missing arguments to max; returning -Inf
## Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
## dplyr 1.1.0.
## ℹ Please use `reframe()` instead.
## ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
## always returns an ungrouped data frame and adjust accordingly.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
peak_metrics %>%
filter(Chamber_Corrected == "C1") %>%
select(turn_event, first_post_turn_day, temp_at_turn,
peak_day, peak_temp, time_to_peak)
## # A tibble: 11 × 6
## turn_event first_post_turn_day temp_at_turn peak_day peak_temp time_to_peak
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 159 1 59.2 2 60.3 1
## 2 173 1 51.8 1 51.8 NA
## 3 186 2 58.2 11 65.8 9
## 4 191 1 50.5 6 65.8 5
## 5 208 1 57.2 2 59.5 1
## 6 222 1 49.8 3 54.3 2
## 7 234 1 45.8 8 57.1 7
## 8 249 1 55.1 2 58.2 1
## 9 262 2 56.9 9 59.3 7
## 10 276 1 49.7 4 61.8 3
## 11 290 1 48.9 6 55.2 5
peak_metrics
## # A tibble: 32 × 9
## turn_event Chamber_Corrected first_post_turn_day temp_at_turn peak_day
## <dbl> <chr> <dbl> <dbl> <dbl>
## 1 159 C1 1 59.2 2
## 2 159 C2 1 75.3 2
## 3 159 C3 1 69.6 1
## 4 173 C1 1 51.8 1
## 5 173 C2 1 45.3 3
## 6 173 C3 1 54.2 4
## 7 186 C1 2 58.2 11
## 8 186 C2 2 68.4 3
## 9 186 C3 6 61.5 6
## 10 191 C1 1 50.5 6
## # ℹ 22 more rows
## # ℹ 4 more variables: peak_temp <dbl>, time_to_peak <dbl>, slope <dbl>,
## # spike_magnitude <dbl>
c1_daily <- daily_avg_turns %>%
filter(Chamber_Corrected == "C1") %>%
left_join(peak_metrics %>%
filter(Chamber_Corrected == "C1") %>%
select(turn_event, peak_day, time_to_peak),
by = "turn_event")
ggplot(c1_daily,
aes(x = day_bin, y = daily_mean)) +
geom_vline(xintercept = 0, linetype = "dashed",
colour = "grey40", linewidth = 0.4) +
geom_point(size = 2.5,
colour = chamber_cols[["C1"]],
alpha = 0.8) +
geom_line(linewidth = 0.6,
colour = chamber_cols[["C1"]],
alpha = 0.7) +
geom_point(data = c1_daily %>%
filter(!is.na(time_to_peak) & day_bin == peak_day),
aes(x = day_bin, y = daily_mean),
colour = "black", size = 4, shape = 8) +
facet_wrap(~ turn_event,
labeller = labeller(turn_event = function(x) paste("DOY", x)),
ncol = 3) +
theme_bw(base_size = 11) +
theme(
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3),
legend.position = "none"
) +
labs(x = "Days relative to turning event",
y = expression("Temperature (°C)"),
title = "C1 — Daily mean temperature per turning event (★ = peak)")
# Add turn number
turn_order <- tibble(
turn_event = sort(c(turns_both_red, turns_control_black)),
turn_number = 1:length(sort(c(turns_both_red, turns_control_black)))
)
peak_metrics_plot <- peak_metrics %>%
left_join(turn_order, by = "turn_event") %>%
filter(Chamber_Corrected %in% c("C1", "C2", "C3"))
# Plot spike magnitude by turn number
ggplot(peak_metrics_plot %>% filter(!is.na(spike_magnitude)),
aes(x = turn_number, y = spike_magnitude,
colour = Chamber_Corrected,
group = Chamber_Corrected)) +
geom_point(size = 2.5) +
geom_line(linewidth = 0.6, alpha = 0.7) +
geom_smooth(aes(group = 1), method = "lm",
se = TRUE, colour = "black",
linewidth = 0.8, linetype = "dashed") +
scale_colour_manual(values = chamber_cols) +
scale_x_continuous(breaks = 1:11) +
theme_bw(base_size = 11) +
theme(
legend.position = "top",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Turning event number",
y = "Spike magnitude (°C)",
colour = "Chamber",
title = "Post-turn temperature spike magnitude across composting cycle")
## `geom_smooth()` using formula = 'y ~ x'
ggplot(peak_metrics_plot %>% filter(!is.na(peak_temp)),
aes(x = turn_number, y = peak_temp,
colour = Chamber_Corrected,
group = Chamber_Corrected)) +
geom_point(size = 2.5) +
geom_line(linewidth = 0.6, alpha = 0.7) +
geom_smooth(aes(group = 1), method = "lm",
se = TRUE, colour = "black",
linewidth = 0.8, linetype = "dashed") +
scale_colour_manual(values = chamber_cols) +
scale_x_continuous(breaks = 1:11) +
theme_bw(base_size = 11) +
theme(
legend.position = "top",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Turning event number",
y = "Peak temperature post-turn (°C)",
colour = "Chamber",
title = "Peak temperature reached after each turning event")
## `geom_smooth()` using formula = 'y ~ x'
# Linear model of peak temp vs turn number
lm_peak <- lm(peak_temp ~ turn_number,
data = peak_metrics_plot %>% filter(!is.na(peak_temp)))
summary(lm_peak)
##
## Call:
## lm(formula = peak_temp ~ turn_number, data = peak_metrics_plot %>%
## filter(!is.na(peak_temp)))
##
## Residuals:
## Min 1Q Median 3Q Max
## -20.1684 -3.6843 0.5645 5.1073 13.2625
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 69.2767 3.0394 22.793 < 2e-16 ***
## turn_number -2.0777 0.4436 -4.683 5.69e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8.009 on 30 degrees of freedom
## Multiple R-squared: 0.4223, Adjusted R-squared: 0.4031
## F-statistic: 21.93 on 1 and 30 DF, p-value: 5.689e-05
Plotting figures which show each chambers time in each zone and measuring this maturation phase
# Zone colours
zone_cols <- c(
"Mesophilic" = "#2166ac",
"Transition" = "#74add1",
"Thermophilic" = "#f46d43",
"Hyper-thermophilic" = "#a50026"
)
# Add zone classification to Cleaned_Data
Cleaned_Data <- Cleaned_Data %>%
mutate(
temp_zone = case_when(
pile_temp < 40 ~ "Mesophilic",
pile_temp >= 40 & pile_temp < 45 ~ "Transition",
pile_temp >= 45 & pile_temp <= 70 ~ "Thermophilic",
pile_temp > 70 ~ "Hyper-thermophilic",
TRUE ~ NA_character_
),
temp_zone = factor(temp_zone,
levels = c("Mesophilic", "Transition",
"Thermophilic", "Hyper-thermophilic"))
)
# Build individual chamber zone plots
zone_plots <- Cleaned_Data %>%
split(.$Chamber_Corrected) %>%
imap(~ ggplot(.x, aes(x = DOY, y = pile_temp, colour = temp_zone)) +
geom_point(size = 0.6, alpha = 0.4) +
geom_hline(yintercept = c(40, 45, 70),
linetype = "dashed",
colour = "grey40",
linewidth = 0.3) +
geom_vline(xintercept = turns_both_red,
colour = "#9b1c31",
linetype = "solid",
linewidth = 0.3,
alpha = 0.6) +
geom_vline(xintercept = turns_control_black,
colour = "#000000",
linetype = "dashed",
linewidth = 0.3,
alpha = 0.6) +
scale_colour_manual(values = zone_cols, na.value = "grey80") +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Day of year",
y = expression("Temperature (°C)"),
title = .y)
)
# Shared legend
legend_plot <- ggplot(Cleaned_Data %>% filter(!is.na(temp_zone)),
aes(x = DOY, y = pile_temp, colour = temp_zone)) +
geom_point() +
scale_colour_manual(values = zone_cols, name = "Zone") +
theme(legend.position = "top") +
guides(colour = guide_legend(override.aes = list(size = 3, alpha = 1)))
shared_legend <- cowplot::get_legend(legend_plot)
# Arrange
left_col <- zone_plots[["C1"]] / zone_plots[["C2"]] / zone_plots[["C3"]]
right_col <- zone_plots[["C4"]] / zone_plots[["C5"]] / zone_plots[["C6"]]
fig_zones_individual <- wrap_elements(shared_legend) /
(left_col | right_col) +
plot_layout(heights = c(0.05, 1))
fig_zones_individual
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 776 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 5 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 266 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 1368 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 62 rows containing missing values or values outside the scale range
## (`geom_point()`).
# ============================================================
# ZONE SUMMARY WITH MISSING DATA - BY CHAMBER
# ============================================================
zone_summary_chamber <- Cleaned_Data %>%
group_by(Chamber_Corrected, Pile) %>%
summarise(
n_total = n(),
n_missing = sum(is.na(temp_zone)),
n_valid = sum(!is.na(temp_zone)),
pct_missing = round(n_missing / n_total * 100, 1),
# Zone percentages as % of VALID observations
pct_meso = round(sum(temp_zone == "Mesophilic", na.rm = TRUE) / n_valid * 100, 1),
pct_trans = round(sum(temp_zone == "Transition", na.rm = TRUE) / n_valid * 100, 1),
pct_thermo = round(sum(temp_zone == "Thermophilic", na.rm = TRUE) / n_valid * 100, 1),
pct_hyper = round(sum(temp_zone == "Hyper-thermophilic", na.rm = TRUE) / n_valid * 100, 1),
.groups = "drop"
) %>%
mutate(Pile = ifelse(Pile == "C", "Control", "Experimental")) %>%
arrange(Pile, Chamber_Corrected)
# ============================================================
# ZONE SUMMARY WITH MISSING DATA - BY PILE
# ============================================================
zone_summary_pile <- Cleaned_Data %>%
mutate(Pile = ifelse(Pile == "C", "Control", "Experimental")) %>%
group_by(Pile) %>%
summarise(
n_total = n(),
n_missing = sum(is.na(temp_zone)),
n_valid = sum(!is.na(temp_zone)),
pct_missing = round(n_missing / n_total * 100, 1),
pct_meso = round(sum(temp_zone == "Mesophilic", na.rm = TRUE) / n_valid * 100, 1),
pct_trans = round(sum(temp_zone == "Transition", na.rm = TRUE) / n_valid * 100, 1),
pct_thermo = round(sum(temp_zone == "Thermophilic", na.rm = TRUE) / n_valid * 100, 1),
pct_hyper = round(sum(temp_zone == "Hyper-thermophilic", na.rm = TRUE) / n_valid * 100, 1),
.groups = "drop"
) %>%
mutate(Chamber_Corrected = Pile)
# ============================================================
# COMBINE AND RENDER
# ============================================================
combined_table <- bind_rows(
zone_summary_chamber %>%
mutate(row_type = "chamber"),
zone_summary_pile %>%
mutate(
Chamber_Corrected = "",
row_type = "pile"
)
) %>%
arrange(Pile, row_type) %>%
mutate(row_id = row_number()) # add row index before select
# Identify pile row numbers
pile_rows <- which(combined_table$row_type == "pile")
combined_table %>%
select(Pile, Chamber_Corrected, n_total, n_valid,
pct_missing, pct_meso, pct_trans, pct_thermo, pct_hyper) %>%
gt() %>%
cols_label(
Pile = "Pile",
Chamber_Corrected = "Chamber",
n_total = "Total Obs.",
n_valid = "Valid Obs.",
pct_missing = "% Missing",
pct_meso = "Mesophilic",
pct_trans = "Transition",
pct_thermo = "Thermophilic",
pct_hyper = "Hyper-therm."
) %>%
tab_spanner(
label = "Temperature Zone (% of valid obs.)",
columns = c(pct_meso, pct_trans, pct_thermo, pct_hyper)
) %>%
tab_spanner(
label = "Data Quality",
columns = c(n_total, n_valid, pct_missing)
) %>%
fmt_number(
columns = c(pct_missing, pct_meso, pct_trans, pct_thermo, pct_hyper),
decimals = 1,
suffix = "%"
) %>%
fmt_number(
columns = c(n_total, n_valid),
decimals = 0,
use_seps = TRUE
) %>%
tab_row_group(
label = "Experimental Pile (C4–C6)",
rows = Pile == "Experimental"
) %>%
tab_row_group(
label = "Control Pile (C1–C3)",
rows = Pile == "Control"
) %>%
tab_style(
style = cell_fill(color = "#f7f7f7"),
locations = cells_body(rows = pile_rows)
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_body(rows = pile_rows)
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_column_labels()
) %>%
tab_style(
style = cell_fill(color = "#ffe0e0"),
locations = cells_body(
columns = pct_missing,
rows = pct_missing > 15
)
) %>%
tab_header(
title = "Time Spent in Each Temperature Zone",
subtitle = "By chamber and pile — percentages of valid observations with missing data reported"
) %>%
tab_footnote(
footnote = "Zone percentages calculated from valid observations only. Pile rows (bold) show aggregated values across all chambers. Missing data highlighted in red where >15% of observations are missing.",
locations = cells_title()
) %>%
opt_table_font(font = "Times New Roman")
| Time Spent in Each Temperature Zone1 | ||||||||
| By chamber and pile — percentages of valid observations with missing data reported1 | ||||||||
| Pile | Chamber |
Data Quality
|
Temperature Zone (% of valid obs.)
|
|||||
|---|---|---|---|---|---|---|---|---|
| Total Obs. | Valid Obs. | % Missing | Mesophilic | Transition | Thermophilic | Hyper-therm. | ||
| Control Pile (C1–C3) | ||||||||
| Control | C1 | 3,203 | 3,202 | 0.0 | 1.0 | 2.2 | 96.7 | 0.1 |
| Control | C2 | 3,663 | 2,887 | 21.2 | 8.9 | 10.4 | 69.6 | 11.1 |
| Control | C3 | 3,108 | 3,103 | 0.2 | 9.3 | 14.1 | 75.1 | 1.6 |
| Control | 9,974 | 9,192 | 7.8 | 6.3 | 8.8 | 80.9 | 4.0 | |
| Experimental Pile (C4–C6) | ||||||||
| Experimental | C4 | 3,624 | 3,358 | 7.3 | 26.0 | 1.3 | 68.9 | 3.8 |
| Experimental | C5 | 3,695 | 2,327 | 37.0 | 0.0 | 8.2 | 84.7 | 7.0 |
| Experimental | C6 | 3,797 | 3,735 | 1.6 | 16.4 | 19.9 | 62.6 | 1.2 |
| Experimental | 11,116 | 9,420 | 15.3 | 15.8 | 10.4 | 70.3 | 3.5 | |
| 1 Zone percentages calculated from valid observations only. Pile rows (bold) show aggregated values across all chambers. Missing data highlighted in red where >15% of observations are missing. | ||||||||
# ============================================================
# STEP 1 - Daily averages per chamber
# A valid day requires >= 5 raw observations
# ============================================================
# Get study period range
study_start <- min(Cleaned_Data$DOY, na.rm = TRUE)
study_end <- max(Cleaned_Data$DOY, na.rm = TRUE)
all_days <- study_start:study_end
# Daily summary per chamber
daily_chamber <- Cleaned_Data %>%
mutate(DOY_day = floor(DOY)) %>%
group_by(Chamber_Corrected, Pile, DOY_day) %>%
summarise(
n_obs = n(),
daily_mean = mean(pile_temp, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
valid = n_obs >= 5,
daily_mean = ifelse(valid, daily_mean, NA_real_),
# Classify zone from daily mean
temp_zone = case_when(
daily_mean < 40 ~ "Mesophilic",
daily_mean >= 40 & daily_mean < 45 ~ "Transition",
daily_mean >= 45 & daily_mean <= 70 ~ "Thermophilic",
daily_mean > 70 ~ "Hyper-thermophilic",
TRUE ~ NA_character_
),
temp_zone = factor(temp_zone,
levels = c("Mesophilic", "Transition",
"Thermophilic", "Hyper-thermophilic"))
)
# Add missing days — days with no data at all
all_chamber_days <- expand.grid(
Chamber_Corrected = unique(Cleaned_Data$Chamber_Corrected),
DOY_day = all_days
) %>%
left_join(
Cleaned_Data %>%
select(Chamber_Corrected, Pile) %>%
distinct(),
by = "Chamber_Corrected"
)
daily_chamber_full <- all_chamber_days %>%
left_join(daily_chamber, by = c("Chamber_Corrected", "Pile", "DOY_day")) %>%
mutate(
valid = ifelse(is.na(valid), FALSE, valid),
n_obs = ifelse(is.na(n_obs), 0L, n_obs)
)
# ============================================================
# STEP 2 - Daily averages per pile
# Average valid chamber means per day
# ============================================================
daily_pile <- daily_chamber_full %>%
mutate(Pile = ifelse(Pile == "C", "Control", "Experimental")) %>%
group_by(Pile, DOY_day) %>%
summarise(
n_chambers_valid = sum(valid, na.rm = TRUE),
pile_daily_mean = mean(daily_mean, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
pile_daily_mean = ifelse(n_chambers_valid == 0, NA_real_, pile_daily_mean),
valid = n_chambers_valid > 0,
temp_zone = case_when(
pile_daily_mean < 40 ~ "Mesophilic",
pile_daily_mean >= 40 & pile_daily_mean < 45 ~ "Transition",
pile_daily_mean >= 45 & pile_daily_mean <= 70 ~ "Thermophilic",
pile_daily_mean > 70 ~ "Hyper-thermophilic",
TRUE ~ NA_character_
),
temp_zone = factor(temp_zone,
levels = c("Mesophilic", "Transition",
"Thermophilic", "Hyper-thermophilic"))
)
# ============================================================
# STEP 3 - Zone summary by chamber
# ============================================================
zone_summary_chamber <- daily_chamber_full %>%
mutate(Pile = ifelse(Pile == "C", "Control", "Experimental")) %>%
group_by(Chamber_Corrected, Pile) %>%
summarise(
n_days_total = n(),
n_days_missing = sum(!valid),
n_days_valid = sum(valid),
pct_missing = round(n_days_missing / n_days_total * 100, 1),
pct_meso = round(sum(temp_zone == "Mesophilic", na.rm = TRUE) / n_days_valid * 100, 1),
pct_trans = round(sum(temp_zone == "Transition", na.rm = TRUE) / n_days_valid * 100, 1),
pct_thermo = round(sum(temp_zone == "Thermophilic", na.rm = TRUE) / n_days_valid * 100, 1),
pct_hyper = round(sum(temp_zone == "Hyper-thermophilic", na.rm = TRUE) / n_days_valid * 100, 1),
.groups = "drop"
) %>%
arrange(Pile, Chamber_Corrected)
# ============================================================
# STEP 4 - Zone summary by pile
# ============================================================
zone_summary_pile <- daily_pile %>%
group_by(Pile) %>%
summarise(
n_days_total = n(),
n_days_missing = sum(!valid),
n_days_valid = sum(valid),
pct_missing = round(n_days_missing / n_days_total * 100, 1),
pct_meso = round(sum(temp_zone == "Mesophilic", na.rm = TRUE) / n_days_valid * 100, 1),
pct_trans = round(sum(temp_zone == "Transition", na.rm = TRUE) / n_days_valid * 100, 1),
pct_thermo = round(sum(temp_zone == "Thermophilic", na.rm = TRUE) / n_days_valid * 100, 1),
pct_hyper = round(sum(temp_zone == "Hyper-thermophilic", na.rm = TRUE) / n_days_valid * 100, 1),
.groups = "drop"
) %>%
mutate(Chamber_Corrected = "")
# ============================================================
# STEP 5 - Combine and render table
# ============================================================
combined_table <- bind_rows(
zone_summary_chamber %>% mutate(row_type = "chamber"),
zone_summary_pile %>% mutate(row_type = "pile")
) %>%
arrange(Pile, row_type) %>%
mutate(row_id = row_number())
pile_rows <- which(combined_table$row_type == "pile")
combined_table %>%
mutate(
# Combine pile and chamber into one label
Label = case_when(
row_type == "pile" ~ paste0(Pile, " (mean)"),
TRUE ~ Chamber_Corrected
)
) %>%
select(Label, Pile, n_days_total, n_days_valid,
pct_missing, pct_meso, pct_trans, pct_thermo, pct_hyper) %>%
gt() %>%
cols_label(
Label = "Chamber",
n_days_total = "Days",
n_days_valid = "Valid",
pct_missing = "Missing (%)",
pct_meso = "Meso.",
pct_trans = "Trans.",
pct_thermo = "Thermo.",
pct_hyper = "Hyper."
) %>%
cols_hide(Pile) %>%
tab_spanner(
label = "Zone (% valid days)",
columns = c(pct_meso, pct_trans, pct_thermo, pct_hyper)
) %>%
fmt_number(
columns = c(pct_missing, pct_meso, pct_trans, pct_thermo, pct_hyper),
decimals = 1
) %>%
fmt_number(
columns = c(n_days_total, n_days_valid),
decimals = 0,
use_seps = FALSE
) %>%
tab_row_group(
label = "Experimental (C4–C6)",
rows = Pile == "Experimental"
) %>%
tab_row_group(
label = "Control (C1–C3)",
rows = Pile == "Control"
) %>%
tab_style(
style = cell_fill(color = "#f7f7f7"),
locations = cells_body(rows = pile_rows)
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_body(rows = pile_rows)
) %>%
tab_style(
style = cell_text(weight = "bold"),
locations = cells_column_labels()
) %>%
tab_style(
style = cell_fill(color = "#ffe0e0"),
locations = cells_body(
columns = pct_missing,
rows = pct_missing > 100
)
) %>%
tab_header(
title = "Temperature Zone Classification by Chamber and Pile",
subtitle = "Daily mean temperature — days with <5 observations excluded"
) %>%
tab_footnote(
footnote = "Meso. = Mesophilic (<40°C); Trans. = Transition (40–45°C); Thermo. = Thermophilic (45–70°C); Hyper. = Hyper-thermophilic (>70°C). Bold rows show pile averages. Missing threshold raised to 20%.",
locations = cells_title()
) %>%
opt_table_font(font = "Times New Roman") %>%
tab_options(table.width = pct(100))
| Temperature Zone Classification by Chamber and Pile1 | |||||||
| Daily mean temperature — days with <5 observations excluded1 | |||||||
| Chamber | Days | Valid | Missing (%) |
Zone (% valid days)
|
|||
|---|---|---|---|---|---|---|---|
| Meso. | Trans. | Thermo. | Hyper. | ||||
| Control (C1–C3) | |||||||
| C1 | 145 | 111 | 23.4 | 1.8 | 1.8 | 96.4 | 0.0 |
| C2 | 145 | 113 | 22.1 | 10.6 | 8.0 | 60.2 | 7.1 |
| C3 | 145 | 108 | 25.5 | 9.3 | 15.7 | 74.1 | 0.9 |
| Control (mean) | 145 | 121 | 16.6 | 2.5 | 8.3 | 89.3 | 0.0 |
| Experimental (C4–C6) | |||||||
| C4 | 145 | 113 | 22.1 | 30.1 | 1.8 | 58.4 | 3.5 |
| C5 | 145 | 119 | 17.9 | 0.0 | 5.9 | 47.9 | 3.4 |
| C6 | 145 | 120 | 17.2 | 11.7 | 20.0 | 67.5 | 0.8 |
| Experimental (mean) | 145 | 121 | 16.6 | 16.5 | 9.1 | 71.1 | 3.3 |
| 1 Meso. = Mesophilic (<40°C); Trans. = Transition (40–45°C); Thermo. = Thermophilic (45–70°C); Hyper. = Hyper-thermophilic (>70°C). Bold rows show pile averages. Missing threshold raised to 20%. | |||||||
# ============================================================
# PLOT - Zone by DOY daily averages per chamber
# ============================================================
# Add pile label to daily_chamber_full
daily_chamber_plot <- daily_chamber_full %>%
mutate(Pile = ifelse(Pile == "C", "Control", "Experimental")) %>%
filter(valid) # only valid days
# Individual chamber plots
chamber_zone_plots <- daily_chamber_plot %>%
split(.$Chamber_Corrected) %>%
imap(~ ggplot(.x, aes(x = DOY_day, y = daily_mean, colour = temp_zone)) +
geom_point(size = 1.5, alpha = 0.8) +
geom_hline(yintercept = c(40, 45, 70),
linetype = "dashed",
colour = "grey40",
linewidth = 0.3) +
geom_vline(xintercept = turns_both_red,
colour = "#9b1c31",
linetype = "solid",
linewidth = 0.3,
alpha = 0.6) +
geom_vline(xintercept = turns_control_black,
colour = "#000000",
linetype = "dashed",
linewidth = 0.3,
alpha = 0.6) +
scale_colour_manual(values = zone_cols, na.value = "grey80",
drop = FALSE) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Day of year",
y = expression("Daily mean temperature (°C)"),
title = .y)
)
# Pile average plots
pile_zone_plots <- daily_pile %>%
filter(valid) %>%
split(.$Pile) %>%
imap(~ ggplot(.x, aes(x = DOY_day, y = pile_daily_mean, colour = temp_zone)) +
geom_point(size = 1.5, alpha = 0.8) +
geom_hline(yintercept = c(40, 45, 70),
linetype = "dashed",
colour = "grey40",
linewidth = 0.3) +
geom_vline(xintercept = turns_both_red,
colour = "#9b1c31",
linetype = "solid",
linewidth = 0.3,
alpha = 0.6) +
geom_vline(xintercept = turns_control_black,
colour = "#000000",
linetype = "dashed",
linewidth = 0.3,
alpha = 0.6) +
scale_colour_manual(values = zone_cols, na.value = "grey80",
drop = FALSE) +
theme_bw(base_size = 11) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "grey92", linewidth = 0.3)
) +
labs(x = "Day of year",
y = expression("Daily mean temperature (°C)"),
title = paste(.y, "pile average"))
)
# Shared legend
legend_plot <- ggplot(daily_chamber_plot %>% filter(!is.na(temp_zone)),
aes(x = DOY_day, y = daily_mean, colour = temp_zone)) +
geom_point() +
scale_colour_manual(values = zone_cols, name = "Zone", drop = FALSE) +
theme(legend.position = "top") +
guides(colour = guide_legend(override.aes = list(size = 3, alpha = 1)))
shared_legend <- cowplot::get_legend(legend_plot)
# ============================================================
# ARRANGE - chambers left/right, pile averages at bottom
# ============================================================
left_col <- chamber_zone_plots[["C1"]] /
chamber_zone_plots[["C2"]] /
chamber_zone_plots[["C3"]]
right_col <- chamber_zone_plots[["C4"]] /
chamber_zone_plots[["C5"]] /
chamber_zone_plots[["C6"]]
pile_row <- pile_zone_plots[["Control"]] |
pile_zone_plots[["Experimental"]]
fig_zones_daily <- wrap_elements(shared_legend) /
(left_col | right_col) /
pile_row +
plot_layout(heights = c(0.05, 1, 0.35))
fig_zones_daily
## Warning: Removed 16 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 7 rows containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 51 rows containing missing values or values outside the scale range
## (`geom_point()`).
Had Error as outdir not found but this was to export tables
2024 data - looking at transformations in 2024 chem data