Dette projekt er lavet i forbindelse med kurset Data Science og omhandler dataopsamling, eksplorativ dataanalyse og datakommunikation. Rapporten og forsøget blev udført af gruppen * Halvdelen af * Gruppe 4-5, der består af Anna Bastholm Mølgaard, Iben Edelbo Lauritzen & Hanne Colding Bundgaard fra studieretningerne Kemi og kemiteknologi.

**Figur 1:** Den Store Surdejskonkurrence.

Figur 1: Den Store Surdejskonkurrence.

Forsøgssetup

Forsøget er udført med en afstandsmåler (Sharp IR Distance Sensor) og en temperatur- og luftfugtighedsmåler (DHT11 Temperature Humidity Sensor) tilkoblet Arduino UNO R4 minima.

**Figur 2:** Det benyttede forsøgssetup med afstandsmåler og temperatur- og fugtighedsmåler.

Figur 2: Det benyttede forsøgssetup med afstandsmåler og temperatur- og fugtighedsmåler.

Eksplorativ dataanalyse

Før den endelige analyse udføres en eksplorativ analyse til indledende undersøgelse af dataen.

Indlæsning af data

Fuld_1 <- read.csv(file = "1_1_1_Fuldkorn.CSV")
Hvede_2 <- read.csv(file = "1_1_2_Hvede.CSV")
Rug_3 <- read.csv(file = "1_3_Rug.CSV")
Fuld1hon_4 <- read.csv(file = "1_4_Fuldkorn_1tsk_honning.CSV")
Hvede1hon_5 <- read.csv(file = "1_5_Hvede_1tsk_honning.CSV")
Rug1hon_6 <- read.csv(file = "1_6_Rug_1tsk_honning.CSV")
Rug_7 <- read.csv(file = "1_7_Rug.CSV")
FuldHvede_8 <- read.csv(file = "1_8_Fuldkorn_Hvede.CSV")
FuldRug_9 <- read.csv(file = "1_9_Fuldkorn_Rug.CSV")
RugHvede_10 <- read.csv(file = "1_10_Rug_Hvede.CSV")
Hvede2hon_11 <- read.csv(file = "1_11_Hvede_2tsk_honning.CSV")
Rug2hon_12 <- read.csv(file = "1_12_Rug_2tsk_honning.CSV")
Hvede_13 <- read.csv(file = "1_13_Hvede.CSV")
Fuld2hon_14 <- read.csv(file = "1_14_Fuldkorn_2tsk_honning.CSV")
Fuld_15 <- read.csv(file = "1_15_Fuldkorn.CSV")
RugHvede_16 <- read.csv(file = "1_16_Rug_Hvede.CSV")
RugHvede_17 <- read.csv(file = "1_17_Rug_Hvede.CSV")
RugHvede_18 <- read.csv(file = "1_18_Rug_Hvede.CSV")
RugHvede_19 <- read.csv(file = "1_19_Rug_Hvede.CSV")
RugHvede_20 <- read.csv(file = "1_20_Rug_Hvede.CSV")
RugHvede_21 <- read.csv(file = "1_21_Rug_Hvede.CSV")
RugHvede_22 <- read.csv(file = "1_22_Rug_Hvede.CSV")
RugHvede_23 <- read.csv(file = "1_23_Rug_Hvede.CSV")
RugHvede_24 <- read.csv(file = "1_24_Rug_Hvede.csv")

De indlæste filer grupperes, så forsøg med samme type mel og mængde honning kan præsenteres sammen senere.

trial_mapping <- c(
    "Fuld_1" = "Fuld",
    "Hvede_2" = "Hvede",
    "Rug_3" = "Rug",
    "Fuld1hon_4" = "Fuld + 1 Hon",
    "Hvede1hon_5" = "Hvede + 1 Hon",
    "Rug1hon_6" = "Rug + 1 Hon",
    "Rug_7" = "Rug",
    "FuldHvede_8" = "Fuld + Hvede",
    "FuldRug_9" = "Fuld + Rug",
    "RugHvede_10" = "Rug + Hvede",
    "Hvede2hon_11" = "Hvede + 2 Hon",
    "Rug2hon_12" = "Rug + 2 Hon",
    "Hvede_13" = "Hvede",
    "Fuld2hon_14" = "Fuld + 2 Hon",
    "Fuld_15" = "Fuld",
    "RugHvede_16" = "Rug + Hvede",
    "RugHvede_17" = "Rug + Hvede",
    "RugHvede_18" = "Rug + Hvede",
    "RugHvede_19" = "Rug + Hvede",
    "RugHvede_20" = "Rug + Hvede",
    "RugHvede_21" = "Rug + Hvede",
    "RugHvede_22" = "Rug + Hvede",
    "RugHvede_23" = "Rug + Hvede",
    "RugHvede_24" = "Rug + Hvede"
)

Rengøring af data

Header fra den indlæste data muteres, så værdierne angives som tal fremfor karakterer, mens overskrifterne ændres. Dette laves som en funktion, hvorefter funktionen bliver udført på hvert forsøg.

fun_clean_data <- function(rawdata) {
  clean_data <- rawdata %>%
  mutate(Temperatur = as.numeric(Temperatur..C.),
         Fugtighed = as.numeric(Fugtighed....),
         Afstand = as.numeric(Afstand..cm.),
         Sekunder = as.numeric(Tid..s.)
         ) %>%
  select(Temperatur, Fugtighed, Afstand, Sekunder)
  return(clean_data)
}

Fuld_1_nummerisk <- fun_clean_data(Fuld_1)
Hvede_2_nummerisk <- fun_clean_data(Hvede_2)
Rug_3_nummerisk <- fun_clean_data(Rug_3)
Fuld1hon_4_nummerisk <- fun_clean_data(Fuld1hon_4)
Hvede1hon_5_nummerisk <- fun_clean_data(Hvede1hon_5)
Rug1hon_6_nummerisk <- fun_clean_data(Rug1hon_6)
Rug_7_nummerisk <- fun_clean_data(Rug_7)
FuldHvede_8_nummerisk <- fun_clean_data(FuldHvede_8)
FuldRug_9_nummerisk <- fun_clean_data(FuldRug_9)
RugHvede_10_nummerisk <- fun_clean_data(RugHvede_10)
Hvede2hon_11_nummerisk <- fun_clean_data(Hvede2hon_11)
Rug2hon_12_nummerisk <- fun_clean_data(Rug2hon_12)
Hvede_13_nummerisk <- fun_clean_data(Hvede_13)
Fuld2hon_14_nummerisk <- fun_clean_data(Fuld2hon_14)
Fuld_15_nummerisk <- fun_clean_data(Fuld_15)
RugHvede_16_nummerisk <- fun_clean_data(RugHvede_16)
RugHvede_17_nummerisk <- fun_clean_data(RugHvede_17)
RugHvede_18_nummerisk <- fun_clean_data(RugHvede_18)
RugHvede_19_nummerisk <- fun_clean_data(RugHvede_19)
RugHvede_20_nummerisk <- fun_clean_data(RugHvede_20)
RugHvede_21_nummerisk <- fun_clean_data(RugHvede_21)
RugHvede_22_nummerisk <- fun_clean_data(RugHvede_22)
RugHvede_23_nummerisk <- fun_clean_data(RugHvede_23)
RugHvede_24_nummerisk <- fun_clean_data(RugHvede_24)
## Warning: There were 4 warnings in `mutate()`.
## The first warning was:
## ℹ In argument: `Temperatur = as.numeric(Temperatur..C.)`.
## Caused by warning:
## ! NAs introduced by coercion
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 3 remaining warnings.

1. eksplorative plot

For at undersøge, hvordan det kunne være relevant at filtrere dataene, laves et plot, der viser afstand over tid for samtlige forsøg.

For at gøre dette laves en samlet data frame med alle de rengjorte data, hvorefter hvert run gøres til en faktor, så rækkefølgen bevares, og data fra forskellige runs ikke bliver blandet. Herefter tilføjes grupperingen, der blev lavet tidligere, så denne kan bruges senere.

combined_df <- bind_rows(
  Fuld_1 = Fuld_1_nummerisk, Hvede_2 = Hvede_2_nummerisk, Rug_3 = Rug_3_nummerisk, Fuld1hon_4 = Fuld1hon_4_nummerisk, Hvede1hon_5 = Hvede1hon_5_nummerisk, Rug1hon_6 = Rug1hon_6_nummerisk, Rug_7 = Rug_7_nummerisk, FuldHvede_8 = FuldHvede_8_nummerisk, FuldRug_9 = FuldRug_9_nummerisk, RugHvede_10 = RugHvede_10_nummerisk, Hvede2hon_11 = Hvede2hon_11_nummerisk, Rug2hon_12 = Rug2hon_12_nummerisk, Hvede_13 = Hvede_13_nummerisk, Fuld2hon_14 = Fuld2hon_14_nummerisk, Fuld_15 = Fuld_15_nummerisk, RugHvede_16 = RugHvede_16_nummerisk, RugHvede_17 = RugHvede_17_nummerisk, RugHvede_18 = RugHvede_18_nummerisk, RugHvede_19 = RugHvede_19_nummerisk, RugHvede_20 = RugHvede_20_nummerisk, RugHvede_21 = RugHvede_21_nummerisk, RugHvede_22 = RugHvede_22_nummerisk, RugHvede_23 = RugHvede_23_nummerisk, RugHvede_24 = RugHvede_24_nummerisk, .id = "run")

combined_df$run <- factor(combined_df$run, levels = unique(combined_df$run))

combined_df$Gruppering <- factor(trial_mapping[combined_df$run])

ggplot(combined_df, aes(x = Sekunder, y = Afstand)) +
  geom_point() + facet_wrap(~ run, nrow = 6, ncol = 4) + scale_y_continuous(limits=c(7,13))
## Warning: Removed 6 rows containing missing values or values outside the scale range
## (`geom_point()`).

Filtrér og subset data

På baggrund af det første eksplorative plot kan det ses, at det kan være en fordel at fjerne de første to målepunkter, da disse blev brugt på kallibrering. Desuden muteres tiden fra sekunder til minutter, så tiden kan fremstilles i hms-format. Der filtreres desuden for de første 12 timer. Sidst gøres den målte afstand også til højde, så man kan kigge på vækst.

fun_mutate_data <- function(cleandata) {
mutate_data <- cleandata %>% 
  group_by(run) %>%
    slice(-(1:4)) %>% 
  ungroup() %>%
  filter(Sekunder %% 60 == 0) %>%
  mutate(min = Sekunder/60) %>%
  group_by(run) %>%
    mutate(Højde = Afstand - min(Afstand, na.rm = TRUE)) %>%
  ungroup() %>%
  mutate(hms = hms(minutes = min),
         date_time = as.POSIXct(hms)) %>%
  filter(min<=720)

mutate_data_long <- pivot_longer(data = mutate_data, 
                     cols = c("Højde", "Fugtighed", "Temperatur"),
                     names_to = "variable",
                     values_to = "value") %>%
  mutate(variable = factor(variable, levels = c("Højde", "Fugtighed", "Temperatur")))

return(mutate_data_long)
}
combined_df_mutated <- fun_mutate_data(combined_df)

2. eksplorative plot

Der laves nu et plot for at se, om rensningen er færdig. Højden benyttes, så maksimal vækst medtænkes i skaleringen af y-aksen. For at mindske antallet af subfigure, laves figurerne for hver gruppering.

ggplot(combined_df_mutated %>% filter(variable == "Højde"), 
       aes(x = min, y = value, color = run)) +
  geom_point() + 
  facet_wrap(~ Gruppering, nrow = 6, ncol = 4) + 
  labs(x = "Tid (minutter)", y = "Højde", title = "Højde over tid") +
  theme_minimal() + scale_y_continuous(limits=c(0,5)) +
  theme(legend.position = "none")

Plottet ser relativt rent ud nu.

Lineær regression i ggplot

geom_smooth() på subset

Der indsættes nu regressionslinje for hvert plot. Dette gøres for tidsrummet 4-6 timer, da den benyttede surdej var lidt langsom i betrækket.

dcf_subset <- combined_df_mutated %>% 
  filter(variable == "Højde", min >= 240, min <= 360) 

ggplot(data = combined_df_mutated %>% filter(variable == "Højde")) +  
  geom_point(aes(x = date_time, y = value, color = run), size = 1) +  
  geom_smooth(data = dcf_subset, 
              aes(x = date_time, y = value, group = run), 
              method = "lm", se = FALSE, color = "darkred", size = 1.5) +
  facet_wrap(~ Gruppering, nrow = 6, ncol = 4) +  
  labs(x = "Tid (minutter)", y = "Højde", title = "Højde over tid med regressionslinje") +
  theme_minimal() +
  theme(legend.position = "none")

Find hældningen på regressionslinjen

Hældningen udregnes nu og præsenteres i en tabel.

get_regression_summary <- function(data, group_name) {

  dcf_subset_group_hr <- data %>% 
    filter(variable == "Højde", Gruppering == group_name, min >= 240, min <= 360) %>%
    mutate(hour = Sekunder / 3600) 
  lm_group <- lm(data = dcf_subset_group_hr, formula = value ~ hour)
  lm_summary <- tidy(lm_group)
  lm_summary$Gruppering <- group_name
  
  return(lm_summary)
}

regression_table <- combined_df_mutated %>%
  filter(variable == "Højde") %>%
  distinct(Gruppering) %>%
  pull(Gruppering) %>%
  map_dfr(~ get_regression_summary(combined_df_mutated, .x))

print(n = 24, regression_table)

Forsøget med største hældning indenfor tidsrummet er rugmel med 2 tsk honning, der hævede 0.938 cm/h.

Plot residualer fra modellen

Residualerne findes nu og disse præsenteres i histogrammer for at undersøge for normalfordeling og dermed lineærthed i tidsrummet 4-6 timer.

lm_results <- map(unique(combined_df_mutated$Gruppering), ~ {
  model <- lm(value ~ min, data = combined_df_mutated %>% filter(Gruppering == .x, variable == "Højde"))
  get_regression_points(model = model)
})

lm_results_df <- bind_rows(lm_results, .id = "Gruppering")

run_names <- combined_df_mutated %>%
  filter(variable == "Højde") %>%
  distinct(run) %>%
  pull(run)

lm_results_df$run <- rep(run_names, length.out = nrow(lm_results_df))

ggplot(lm_results_df, aes(x = residual)) +
  geom_histogram(bins = 30, alpha = 0.5) +
  facet_wrap(~ run, scales = "free", nrow = 6, ncol = 4) + 
  labs(x = "Residualer", y = "Antal") + 
  theme_minimal() +
  theme(legend.position = "none")

map(unique(combined_df_mutated$Gruppering), ~ {
  model <- lm(value ~ min, data = combined_df_mutated %>% filter(Gruppering == .x, variable == "Højde"))
  get_regression_summaries(model = model, digits = 10)
})
## [[1]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1   0.00248       0.00178 0.167 0.408 0.409      3.55  0.0596     1  1432
## 
## [[2]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic      p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>        <dbl> <dbl> <dbl>
## 1    0.0236        0.0229 0.286 0.535 0.535      34.6 0.0000000051     1  1432
## 
## [[3]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1    0.0817        0.0811 0.522 0.723 0.723      127.       0     1  1432
## 
## [[4]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared    mse  rmse sigma statistic      p_value    df  nobs
##       <dbl>         <dbl>  <dbl> <dbl> <dbl>     <dbl>        <dbl> <dbl> <dbl>
## 1    0.0501        0.0488 0.0980 0.313 0.313      37.7 0.0000000014     1   716
## 
## [[5]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared    mse  rmse sigma statistic     p_value    df  nobs
##       <dbl>         <dbl>  <dbl> <dbl> <dbl>     <dbl>       <dbl> <dbl> <dbl>
## 1    0.0383        0.0369 0.0469 0.216 0.217      28.4 0.000000132     1   716
## 
## [[6]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1     0.165         0.163 0.314 0.560 0.561      141.       0     1   716
## 
## [[7]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic      p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>        <dbl> <dbl> <dbl>
## 1    0.0660        0.0643 0.134 0.366 0.366      39.5 0.0000000007     1   561
## 
## [[8]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared    mse  rmse sigma statistic      p_value    df  nobs
##       <dbl>         <dbl>  <dbl> <dbl> <dbl>     <dbl>        <dbl> <dbl> <dbl>
## 1    0.0614        0.0600 0.0776 0.279 0.279      42.0 0.0000000002     1   644
## 
## [[9]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1    0.0189        0.0187 0.715 0.846 0.846      152.       0     1  7880
## 
## [[10]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared    mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl>  <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1 0.0000584      -0.00134 0.0989 0.314 0.315    0.0417   0.838     1   716
## 
## [[11]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared   mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl> <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1     0.564         0.564 0.329 0.574 0.575      924.       0     1   716
## 
## [[12]]
## # A tibble: 1 × 9
##   r_squared adj_r_squared    mse  rmse sigma statistic p_value    df  nobs
##       <dbl>         <dbl>  <dbl> <dbl> <dbl>     <dbl>   <dbl> <dbl> <dbl>
## 1    0.0123        0.0106 0.0601 0.245 0.245      7.35 0.00691     1   591

Generelt ses en klokkeformet tendens, så residualerne er umiddelbart normalfordelt.

Kommunikative visualiseringer

Overbliksfigur

For at lave gode kommunikative figurer er det vigtigt at læseren fokuseres på de vigtige aspekter i dataene. Unødig tekst og farver skal fjernes for at få læseren til at fokusere på de vigtige trends og mønstre i data. I nedenstående kode har jeg implementeret forskellige modikationer der kunne være relevante.

prepare_data <- function(df) {
  df %>%
    mutate(variable = fct_relevel(variable, "Højde", "Fugtighed", "Temperatur")) %>%
    mutate(variable = recode(variable, 
                             "Højde" = "Vækst [cm]",
                             "Fugtighed" = "Fugtighed [%]",
                             "Temperatur" = "Temperatur [°C]"))
}

plot_gruppe <- function(df, gruppe, titel) {
  df_filtered <- df %>% filter(Gruppering %in% gruppe)  
  
  df_pretty <- prepare_data(df_filtered) 
  
  ggplot(df_pretty, aes(x = min, y = value, color = Gruppering)) +
    geom_point(size = 1) +
    facet_grid(rows = "variable", scales = "free_y") +
    theme_classic() +
    theme(panel.grid.major = element_line(color = "grey90"),
          axis.title = element_blank(),
          plot.title = element_text(face = "bold", size = 22),
          plot.subtitle = element_text(face = "italic", size = 8, color = "grey70"),
          plot.caption = element_text(face = "italic", size = 8, color = "grey70"),
          panel.background = element_rect(fill = "grey97"),
          strip.background = element_blank()) +
    labs(title = titel,
         subtitle = paste("Udført af", gruppemedlemmer),
         caption = "Disclaimer: Det anbefales ikke at spise surdejen uden den er bagt.\nAalborg Universitet, forår 2025")
}

gruppe_1 <- c("Fuld", "Fuld + 1 Hon", "Fuld + 2 Hon")
gruppe_2 <- c("Hvede", "Hvede + 1 Hon", "Hvede + 2 Hon")
gruppe_3 <- c("Rug", "Rug + 1 Hon", "Rug + 2 Hon")
gruppe_4 <- c("Fuld + Hvede", "Rug + Hvede", "Fuld + Rug")

plot1 <- plot_gruppe(combined_df_mutated, gruppe_1, "Fuld-gruppen")
plot2 <- plot_gruppe(combined_df_mutated, gruppe_2, "Hvede-gruppen")
plot3 <- plot_gruppe(combined_df_mutated, gruppe_3, "Rug-gruppen")
plot4 <- plot_gruppe(combined_df_mutated, gruppe_4, "Blandet gruppe")
print(plot1)

Der ses ikke en tydelig forskel i væksten afhængigt af den tilførte honningmængde. Generelt ligner det dog, at surdejen hævede mest med 1 tsk honning tilføjet. Det kan desuden ses, at der ikke har været de store udsving luftfugtigheds- og temperaturniveauet på tværs af de forskellige forsøg og indenfor det enkelte forsøg.

print(plot2)

Overordnet set ser det ud til, at surdejen hævede mest kun med hvede og uden honning. Igen har forholdene været relativt stabile.

print(plot3)

Der ses ikke en tydelig tendens i hvilken surdej, der voksede mest. Den, der ligger højest er den ene Rug, men den anden Rug ligger lavest, så det kan ikke definitet konstateres, hvilken der er hævet mest. Der ses på tværs af alle forsøg en tendens til, at luftfugtigheden steg i løbet af de første par timer, hvorefter den faldt igen.

print(plot4)

I den blandede gruppe kan det ses, at blandingen af rug og hvede generelt ligger højere i vækstkurven, mens det igen kan ses, at der ikke er nogle systematiske forskellige for luftfugtighed og temperatur.

Annoterede figure

max_Højde_time <- combined_df_mutated %>% 
  filter(Gruppering == "Rug + 2 Hon", variable == "Højde") %>% 
  filter(value == max(value, na.rm = TRUE))

ggplot(combined_df_mutated %>% filter(Gruppering == "Rug + 2 Hon", variable == "Højde"), aes(x = date_time, y = value)) +
  geom_point(size = 1) +
  annotate(geom = "segment", 
           x = max_Højde_time$date_time, 
           y = 0,
           xend = max_Højde_time$date_time,
           yend = max_Højde_time$value,
           color = "darkred") +
  annotate(geom = "text", 
           x = max_Højde_time$date_time + dminutes(40), 
           y = max_Højde_time$value / 2, 
           label = paste("Samlet\nhævning\n", round(max_Højde_time$value, 2), " cm"),
           color = "darkred") +
  scale_x_datetime(date_breaks = "3 hours", date_labels = "%H:%M") +  
  
  theme_classic() + 
  theme(panel.grid.major = element_line(color = "grey90"),
        axis.title.x = element_blank(),
        plot.title = element_text(face = "bold", size = 22),
        plot.subtitle = element_text(face = "italic", size = 8, color = "grey70"),
        plot.caption =  element_text(face = "italic", size = 8, color = "grey70"),
        panel.background = element_rect(fill = "grey97"),
        strip.background = element_blank()) +
  labs(y = "Vækst [cm]",
       title = "Se surdej vokse",
       subtitle = paste("Udført af", "Anna Bastholm Mølgaard, Iben Edelbo Lauritzen & Hanne Colding Bundgaard"),
       caption = "Disclaimer: Det anbefales ikke at spise surdejen uden den er bagt.\nAalborg Universitet, forår 2025")

Rugmel med 2 tsk honning viste sig at have den hurtigste vækstrate. Denne havde en samlet hævning 3.57 cm.

max_Højde_time <- combined_df_mutated %>% 
  filter(run == "RugHvede_24", variable == "Højde") %>% 
  filter(value == max(value, na.rm = TRUE))

# Plot
ggplot(combined_df_mutated %>% filter(run == "RugHvede_24", variable == "Højde"), aes(x = date_time, y = value)) +
  geom_point(size = 1) +
  annotate(geom = "segment", 
           x = max_Højde_time$date_time, 
           y = 0,
           xend = max_Højde_time$date_time,
           yend = max_Højde_time$value,
           color = "darkred") +
  annotate(geom = "text", 
           x = max_Højde_time$date_time + dminutes(40), 
           y = max_Højde_time$value / 2, 
           label = paste("Samlet\nhævning\n", round(max_Højde_time$value, 2), " cm"),
           color = "darkred") +
  scale_x_datetime(date_breaks = "3 hours", date_labels = "%H:%M") +  
  theme_classic() + 
  theme(panel.grid.major = element_line(color = "grey90"),
        axis.title.x = element_blank(),
        plot.title = element_text(face = "bold", size = 22),
        plot.subtitle = element_text(face = "italic", size = 8, color = "grey70"),
        plot.caption =  element_text(face = "italic", size = 8, color = "grey70"),
        panel.background = element_rect(fill = "grey97"),
        strip.background = element_blank()) +
  labs(y = "Vækst [cm]",
       title = "Se surdej vokse",
       subtitle = paste("Udført af", "Anna Bastholm Mølgaard, Iben Edelbo Lauritzen & Hanne Colding Bundgaard"),
       caption = "Disclaimer: Det anbefales ikke at spise surdejen uden den er bagt.\nAalborg Universitet, forår 2025")

Surdejen, der hævede mest, hævede til 4.01 cm.

Tabel med ekstraherede stats

rug2hon_stats <- combined_df_mutated %>%
  filter(Gruppering == "Rug + 2 Hon") %>%
  group_by(variable) %>%
  summarise(
    Max_Højde = ifelse(variable == "Højde", max(value, na.rm = TRUE), NA),
    
    R_squared_Højde = ifelse(variable == "Højde", summary(lm(value ~ min))$r.squared, NA),
    
    Gennemsnit_Temperatur = ifelse(variable == "Temperatur", mean(value, na.rm = TRUE), NA),
    
    Gennemsnit_Fugtighed = ifelse(variable == "Fugtighed", mean(value, na.rm = TRUE), NA)
  ) %>%
  ungroup()  
## 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.
## `summarise()` has grouped output by 'variable'. You can override using the
## `.groups` argument.
rug2hon_final <- rug2hon_stats %>%
  summarise(
    Max_Højde = max(Max_Højde, na.rm = TRUE), 
    Væksthastighed = "0.938",
    R_squared = "0.5642304", 
    Gennemsnit_Temperatur = "24.10573", 
    Gennemsnit_Fugtighed = "57.87989" 
  ) %>%
  mutate(
    Meltype = "Rug", 
    Forsøgsvariable = "Honning 2 tsk" 
  )
rugHvede_stats <- combined_df_mutated %>%
  filter(run == "RugHvede_24") %>%
  group_by(variable) %>%
  summarise(
    Max_Højde = ifelse(variable == "Højde", max(value, na.rm = TRUE), NA),
    R_squared_Højde = ifelse(variable == "Højde", summary(lm(value ~ min))$r.squared, NA),
    Gennemsnit_Temperatur = ifelse(variable == "Temperatur", mean(value, na.rm = TRUE), NA),
    Gennemsnit_Fugtighed = ifelse(variable == "Fugtighed", mean(value, na.rm = TRUE), NA),
    .groups = "drop"
  )  
## 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.
rugHvede_final <- rugHvede_stats %>%
  summarise(
    Max_Højde = max(Max_Højde, na.rm = TRUE), 
    Væksthastighed = "-0.0622",
    R_squared = R_squared_Højde, 
    Gennemsnit_Temperatur = mean(Gennemsnit_Temperatur, na.rm = TRUE), 
    Gennemsnit_Fugtighed = mean(Gennemsnit_Fugtighed, na.rm = TRUE),
    .groups = "drop"
    
  ) %>%
  mutate(
    Meltype = "Rug + Hvede", 
    Forsøgsvariable = "" 
  )
## 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.
rugHvede_final
## # A tibble: 4,308 × 7
##    Max_Højde Væksthastighed R_squared Gennemsnit_Temperatur Gennemsnit_Fugtighed
##        <dbl> <chr>              <dbl>                 <dbl>                <dbl>
##  1      4.01 -0.0622           0.0346                  23.8                 64.2
##  2      4.01 -0.0622           0.0346                  23.8                 64.2
##  3      4.01 -0.0622           0.0346                  23.8                 64.2
##  4      4.01 -0.0622           0.0346                  23.8                 64.2
##  5      4.01 -0.0622           0.0346                  23.8                 64.2
##  6      4.01 -0.0622           0.0346                  23.8                 64.2
##  7      4.01 -0.0622           0.0346                  23.8                 64.2
##  8      4.01 -0.0622           0.0346                  23.8                 64.2
##  9      4.01 -0.0622           0.0346                  23.8                 64.2
## 10      4.01 -0.0622           0.0346                  23.8                 64.2
## # ℹ 4,298 more rows
## # ℹ 2 more variables: Meltype <chr>, Forsøgsvariable <chr>
rug2hon_final <- rug2hon_final %>%
  mutate(
    R_squared = as.numeric(R_squared),
    Gennemsnit_Temperatur = as.numeric(Gennemsnit_Temperatur),
    Gennemsnit_Fugtighed = as.numeric(Gennemsnit_Fugtighed)
  )

rugHvede_final <- rugHvede_final %>%
  mutate(
    R_squared = as.numeric(R_squared),
    Gennemsnit_Temperatur = as.numeric(Gennemsnit_Temperatur),
    Gennemsnit_Fugtighed = as.numeric(Gennemsnit_Fugtighed)
  ) %>%
  slice(1)

final_table <- bind_rows(
  rug2hon_final %>% mutate(Navn = "Rug + 2 Hon"),
  rugHvede_final %>% mutate(Navn = "Rug + Hvede")
) %>%
  select(Navn, everything())  # Flytter 'Navn' til første kolonne

# Omdøb kolonneoverskrifter og lav en pæn tabel
library(kableExtra)

final_table %>%
  kable(format = "html", digits = 2, col.names = c(
    "Navn", "Max Højde [cm]", "Væksthastighed [cm/time]", "R²",
    "Gns. Temperatur [°C]", "Gns. Fugtighed [%]", "Meltype", "Forsøgsvariable"
  )) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"), full_width = FALSE)
Navn Max Højde [cm] Væksthastighed [cm/time] Gns. Temperatur [°C] Gns. Fugtighed [%] Meltype Forsøgsvariable
Rug + 2 Hon 3.57 0.938 0.56 24.11 57.88 Rug Honning 2 tsk
Rug + Hvede 4.01 -0.0622 0.03 23.78 64.17 Rug + Hvede

Forsøget med rugmel og 2 tsk honning havde den hurtigste væksthastighed, mens et af forsøgene med rugmel og hvede gav en max højde på 4.01 cm.

Bagværk af den kære surdej

**Figur 3:** Foccasia bagt på overskudssurdej..

Figur 3: Foccasia bagt på overskudssurdej..

**Figur 4:** Bananbrød bagt på overskudssurdej.

Figur 4: Bananbrød bagt på overskudssurdej.

**Figur 5:** Franskbrød bagt på aktiv surdej.

Figur 5: Franskbrød bagt på aktiv surdej.

**Figur 6:** Rugbrød bagt på aktiv surdej - det bedste rugbrød, man kan bage!

Figur 6: Rugbrød bagt på aktiv surdej - det bedste rugbrød, man kan bage!

Reproducering (supporting material)

Arduinokode

I det følgende er koden, der har været benyttet til at kode Arduino UNO R4 minima til dataopsamling. Denne viser de målte data på et LCD, måler hvert minut og har et eksponentialfilter med \(\alpha = 0.2\). Bemærk desuden, at de første tre målinger ignoreres i eksponentialfilteret, da disse kan være ukorrekte, eftersom Arduinoen først skal “i gang”.

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
//                        Libraries & Variabler               //
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

// Package setup
#include <Wire.h> // Kommunikation med I2C devices – specifikt LCD
#include <LiquidCrystal_I2C.h> // Udvidelse til kommunikation med LCD
#include <DHT.h> // Kommunikation med fugtigheds- og temperaturmåler
#include <SPI.h> // Low-level kommunikation med SD-kort (Håndterer lavpraktisk kommunikation med selve devicen) 
#include <SD.h> // High-level kommunikation med SD-kort (Indeholder funktioner, som open(), read(), write())

// LCD setup
LiquidCrystal_I2C lcd(0x27, 16, 2); // Opsætning med adresse: 0x27, antal kolonner: 16 og antal rækker: 2

// DHT setup
#define DHTPIN 8 // Digital udgang forbundet til DHT sensor
#define DHTTYPE DHT11 // Definerer typen af DHT-sensor til DHT11
DHT dht(DHTPIN, DHTTYPE); // Opsætning med digital udgang og type ud fra variable.

// Afstandsmåler setup
#define sensor A0 // Analog udgang til afstandsmåler

// SD-kort setup
File myFile;
#define CSPIN 10 // CS pin for SD-kortlæser

// Tidsstyring LCD
unsigned long previousMillis = 0; // Startværdi
const long interval = 2000; // 2 sekunders interval
bool showTempHumidity = true; // Starter med at vise temperatur og fugtighed

// Variabler til eksponentielt filter
float filteredDistance = -1.0; // Denne startværdi indikerer, at filteret endnu ikke anvendes
int measurementCount = 0; // Tæller antallet af målinger
const float alpha = 0.2; // Filterkoefficent

// Tidsstyring for målinger
const long measureInterval = 60000; // 1 minut (60.000 ms)
unsigned long lastMeasureTime = 0; // Tidspunkt for sidste måling

// Tidsstyring for LCD-opdatering
unsigned long lastLcdUpdate = 0; // Tidspunkt for sidste opdatering af LCD
const long lcdUpdateInterval = 2000; // LCD-opdateringsinterval (2 sekunder)

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
//                        Setup-kode                          //
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

void setup() {
  Serial.begin(9600); // Kommunikationshastighed 9600 bits pr. sekund

  // Start LCD
  lcd.init(); // Opstart LCD
  lcd.setBacklight(1); // Tænder lyset på LCD ved værdien 1 (tændt)

  // Start DHT sensor
  dht.begin(); // Tænder DHT

  // Initialisering af SD-kort
  Serial.print("Initializing SD card..."); // Printer opstart af SD-kort i Serial Monitor
  if (!SD.begin(CSPIN)) {
    Serial.println("initialization failed!"); // Printer, hvis SD-kort opstart fejler
    abort(); // Stop alt ved error
  }
  Serial.println("initialization done."); // Printer succesfuld initiering

  // Opretter header i csv-fil til data
  myFile = SD.open("DATA.csv", FILE_WRITE); // Beskriver hvilken fil, der henvises til og laver en ny , hvis den ikke allerede eksisterer
  if (myFile) {
    myFile.println("Temperatur (C),Fugtighed (%),Afstand (cm),Tid (s)"); // Navngiver hver header og bruger komma som separator
    myFile.close(); // Lukker DATA.csv for redigering
  } else {
    Serial.println("error opening DATA.csv"); // Printer fejl, hvis ikke SD-kortet kan åbnes korrekt
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
//                          Loop-kode                         //
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

void loop() { // Påbegynder og definerer loop, der skal gentages
  unsigned long currentMillis = millis(); // Starter counter på millisekunder

  // Setup af selve målingerne
  if (currentMillis - lastMeasureTime >= measureInterval) { // Målinger sker kun hvert minut
    lastMeasureTime = currentMillis; // Opdaterer sidste måletidspunkt

    // Måler distance med IR-afstandsmåler
    float volts = analogRead(sensor) * 0.0048828125; // Omregner værdi fra bit til volt ved at gange 5 V/1024 bit på den aflæste værdi
    float distance = 13 * pow(volts, -1); // Omregner volt til afstand (cm)

    // Anvend eksponentielt filter på afstand
    if (distance >= 5.0) { // Hvis afstanden er over 5 cm anvendes følgende
      if (filteredDistance == -1.0) { // Med == tjekkes det, om det er den første måling
        filteredDistance = distance; // Ved den første måling er den filtrede afstand den målte afstand
        
      } else {
        filteredDistance = alpha * distance + (1 - alpha) * filteredDistance; // Definerer en ny filtreret afstand pba. den målte afstand
      }
    }

    // Tæl målinger
    measurementCount++; // ++ indikerer, at for hver måling skal øges i counteren med 1
    if (measurementCount >= 3) {
      filteredDistance = alpha * distance + (1 - alpha) * filteredDistance; // Anvender filteret efter de første tre målinger

    } else {
      filteredDistance = distance; // Anvender den målte afstand de første tre målinger
    }

    // Måler temperatur og fugtighed
    float temperature = dht.readTemperature(); // Definerer ”temperature” som den læste værdi på DHT
    float humidity = dht.readHumidity(); // Definerer “humidity” som den læste værdi på DHT

    // Printer data i Serial Monitor
    Serial.print("Temperatur: "); // Printer parameternavn
    Serial.print(temperature); // Printer den målte værdi
    Serial.print(" C, Luftfugtighed: "); // Printer kommasepereret enhed for den forrige værdi og nyt parameter-navn
    Serial.print(humidity);
    Serial.print(" %, Afstand: ");
    Serial.print(filteredDistance);
    Serial.print(" cm, Tid: ");
    Serial.print(currentMillis / 1000); // Angiver, at der skal være sekundvise intervaller
    Serial.println(" s"); // Printer den sidste enhed og definerer, at de næste værdier skal være på en ny linje

    // Gemmer data på SD-kort
    myFile = SD.open("DATA.csv", FILE_WRITE); // Beskriver hvilken fil, der henvises til 
    if (myFile) { // Hvis det lykkes, sker følgende
      myFile.print(temperature); // Printer den målte temperatur
      myFile.print(","); // Indsætter komma i filen, så næste parameter får separator herfra
      myFile.print(humidity); // Printer den målte luftfugtighed
      myFile.print(","); // Indsætter komma i filen
      myFile.print(filteredDistance); // Printer den filtrerede afstand
      myFile.print(","); // Indsætter komma i filen
      myFile.println(currentMillis / 1000); // Printer tiden med sekundvise-intervaller 
      // println indikerer linjeskift og definerer, at de næste værdier skal være på næste linje
      myFile.close(); // Lukker filen og definerer, at al information er givet

    } else { // Hvis ikke SD-kortet ikke kan åbnes, sker følgende
      Serial.println("error opening data.csv"); // Printer error, hvis SD-kortet ikke er korrekt opsat
    } 
  }

  // Setup af displayet, der skifter hvert 2. sekund
  if (filteredDistance <= 30) { // LCD printer følgende, hvis den filtrerede afstand er under 30 cm
    if (currentMillis - lastLcdUpdate >= lcdUpdateInterval) { // LCD-opdatering sker hvert 2. sekund
      lastLcdUpdate = currentMillis; // Opdaterer tiden for den seneste LCD-opdatering
      showTempHumidity = !showTempHumidity;  // Skift mellem, at temperatur og fugtighed gælder og derfor vises og ikke vises
      lcd.clear(); // Rydder skærmen for at forhindre overlap med gamle data
      // Viser data på LCD. Kolonne og række starter ved begge med 0
      if (showTempHumidity) { // Defineret i det forrige, hvornår dette gælder
        lcd.setCursor(0, 0); // Placerer markøren ved række 1, kolonne 1
        lcd.print("Temp: "); // Printer parameternavn
        lcd.print(dht.readTemperature()); // Printer den målte temperatur
        lcd.print("C"); // Printer enhed

        lcd.setCursor(0, 1); // Placerer markøren ved række 1, kolonne 2
        lcd.print("Fugt: "); // Printer parameternavn
        lcd.print(dht.readHumidity()); // Printer den målte luftfugtighed
        lcd.print("%"); // Printer enheden

      } else { // Hvis det forrige ikke gælder, printes følgende
        lcd.setCursor(0, 0); // Placerer markøren ved række 1, kolonne 1
        lcd.print("Afstand: "); // Printer parameternavn
        lcd.print(filteredDistance); // Printer den filtrerede afstand
        lcd.print("cm"); // Printer enheden

        lcd.setCursor(0, 1); // Placerer markøren ved række 1, kolonne 2
        lcd.print("Tid: "); // Printer parameternavn
        lcd.print(currentMillis / 1000);  // Viser den nuværende tid i hele sekunder
        lcd.print("s"); // Printer enheden
      }
    }
  }
}

Session Info

Den benyttede version af R og de benyttede pakker fremgår af følgende Session Info.

sessionInfo()
## R version 4.3.2 (2023-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 11 x64 (build 26100)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Danish_Denmark.utf8  LC_CTYPE=Danish_Denmark.utf8   
## [3] LC_MONETARY=Danish_Denmark.utf8 LC_NUMERIC=C                   
## [5] LC_TIME=Danish_Denmark.utf8    
## 
## time zone: Europe/Copenhagen
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] kableExtra_1.4.0 knitr_1.49       broom_1.0.7      moderndive_0.7.0
##  [5] rmdformats_1.0.4 hms_1.1.3        lubridate_1.9.4  forcats_1.0.0   
##  [9] stringr_1.5.1    dplyr_1.1.4      purrr_1.0.4      readr_2.1.5     
## [13] tidyr_1.3.1      tibble_3.2.1     ggplot2_3.5.1    tidyverse_2.0.0 
## 
## loaded via a namespace (and not attached):
##  [1] sass_0.4.9           utf8_1.2.4           generics_0.1.3      
##  [4] xml2_1.3.7           stringi_1.8.4        digest_0.6.37       
##  [7] magrittr_2.0.3       evaluate_1.0.3       grid_4.3.2          
## [10] timechange_0.3.0     bookdown_0.42        fastmap_1.2.0       
## [13] operator.tools_1.6.3 jsonlite_1.9.0       backports_1.5.0     
## [16] fansi_1.0.6          viridisLite_0.4.2    scales_1.3.0        
## [19] infer_1.0.7          jquerylib_0.1.4      cli_3.6.4           
## [22] crayon_1.5.2         rlang_1.1.5          munsell_0.5.1       
## [25] withr_3.0.2          cachem_1.1.0         yaml_2.3.10         
## [28] tools_4.3.2          tzdb_0.4.0           colorspace_2.1-1    
## [31] vctrs_0.6.5          R6_2.5.1             lifecycle_1.0.4     
## [34] snakecase_0.11.1     janitor_2.2.1        pkgconfig_2.0.3     
## [37] pillar_1.9.0         bslib_0.9.0          gtable_0.3.6        
## [40] glue_1.7.0           systemfonts_1.2.1    xfun_0.51           
## [43] tidyselect_1.2.1     rstudioapi_0.17.1    farver_2.1.2        
## [46] htmltools_0.5.8.1    labeling_0.4.3       svglite_2.1.3       
## [49] rmarkdown_2.29       formula.tools_1.7.1  compiler_4.3.2