Back to Article
Research Question 2
Download Source

Is there a relationship between personal light exposure and participants’ behaviour and environment?

Author

Johannes Zauner

Last modified:

May 20, 2026

Preface

This document contains the analysis for Research Question 2, as defined in the preregistration.

Research Question 2:
Is there a relationship between personal light exposure and participants’ behaviour and environment?

H3: There is a significant relationship between hourly self-reported light exposure categories and hourly geometric mean of melanopic EDI.

H4: There is a significant relationship between hourly self-reported activities and hourly geometric mean of melanopic EDI.

H5: There is a significant relationship between LEBA-factors and pre-selected personal light exposure metrics.

H6: There is a significant relationship between day type, daily exercise, sleep, and the hourly geometric mean of light exposure.

H7: There is a ceiling effect of (absolute) latitude and photoperiod with level, duration, and exposure-history-based metrics

Setup

In this section, we load in the preprocessed and prepared data, site data, and load required packages.

In [1]:
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(tidyverse)
Warning: package 'ggplot2' was built under R version 4.5.2
Warning: package 'tibble' was built under R version 4.5.2
Warning: package 'tidyr' was built under R version 4.5.2
Warning: package 'readr' was built under R version 4.5.2
Warning: package 'purrr' was built under R version 4.5.2
Warning: package 'dplyr' was built under R version 4.5.2
Warning: package 'lubridate' was built under R version 4.5.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.2     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(melidosData)
library(LightLogR)
library(mgcv)
Loading required package: nlme

Attaching package: 'nlme'

The following object is masked from 'package:dplyr':

    collapse

This is mgcv 1.9-4. For overview type '?mgcv'.
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(lme4)
Warning: package 'lme4' was built under R version 4.5.2
Loading required package: Matrix

Attaching package: 'Matrix'

The following objects are masked from 'package:tidyr':

    expand, pack, unpack


Attaching package: 'lme4'

The following object is masked from 'package:nlme':

    lmList
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(performance)
Warning: package 'performance' was built under R version 4.5.2
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(cowplot)

Attaching package: 'cowplot'

The following object is masked from 'package:lubridate':

    stamp
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(ggridges)
library(ggtext)
library(sjPlot)

Attaching package: 'sjPlot'

The following objects are masked from 'package:cowplot':

    plot_grid, save_plot

The following object is masked from 'package:ggplot2':

    set_theme
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(broom)
Warning: package 'broom' was built under R version 4.5.2
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(itsadug)
Warning: package 'itsadug' was built under R version 4.5.2
Loading required package: plotfunctions
Warning: package 'plotfunctions' was built under R version 4.5.2

Attaching package: 'plotfunctions'

The following object is masked from 'package:ggplot2':

    alpha

Loaded package itsadug 2.5 (see 'help("itsadug")' ).
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(gratia)
Warning: package 'gratia' was built under R version 4.5.2

Attaching package: 'gratia'

The following object is masked from 'package:itsadug':

    dispersion

The following object is masked from 'package:stringr':

    boundary
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
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'
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(patchwork)

Attaching package: 'patchwork'

The following object is masked from 'package:cowplot':

    align_plots
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(rlang)
Warning: package 'rlang' was built under R version 4.5.2

Attaching package: 'rlang'

The following objects are masked from 'package:purrr':

    flatten, flatten_chr, flatten_dbl, flatten_int, flatten_lgl,
    flatten_raw, invoke, splice
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(broom.mixed)
Warning: package 'broom.mixed' was built under R version 4.5.2
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(gt)
Warning: package 'gt' was built under R version 4.5.2

Attaching package: 'gt'

The following object is masked from 'package:cowplot':

    as_gtable
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(performance)
library(glmmTMB)
Warning: package 'glmmTMB' was built under R version 4.5.2
Warning in check_dep_version(dep_pkg = "TMB"): package version mismatch: 
glmmTMB was built with TMB package version 1.9.19
Current TMB package version is 1.9.21
Please re-install glmmTMB from source or restore original 'TMB' package (see '?reinstalling' for more information)
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
library(gghighlight)
library(gtsummary)
Warning: package 'gtsummary' was built under R version 4.5.2
Code cell - Setup
#| message: false
#| warning: false
#| code-summary: Code cell - Setup
source("scripts/fitting.R")
source("scripts/summaries.R")
source("scripts/RQ2_specific.R")
In [2]:
Code
#| message: false
#| file-name: Load data
load("data/metrics_glasses.RData")
load("data/preprocessed_glasses_2.RData")
metrics <- metrics_glasses
hourly_data <-  light_glasses_processed2
H3 <- "Hypothesis: There is a significant relationship between hourly self-reported light exposure categories and hourly geometric mean of melanopic EDI."
H4 <- "Hypothesis: There is a significant relationship between hourly self-reported activities and hourly geometric mean of melanopic EDI."
H5 <- "Hypothesis: There is a significant relationship between LEBA-factors and pre-selected personal light exposure metrics."
H6 <- "Hypothesis: There is a significant relationship between the day type, daily exercise, and sleep with the hourly geometric mean of light exposure"
H7 <- "Hypothesis: There is a ceiling effect of photoperiod with level, duration, and exposure-history-based metrics."
sig.level <- 0.05

#for chest:
# load("data/metrics_chest.RData")
# metrics <- metrics_chest
# melidos_cities <- melidos_cities[names(melidos_cities)!="MPI"]

Hypothesis 3

Details

Hypothesis 3 states:

There is a significant relationship between hourly self-reported light exposure categories and hourly geometric mean of melanopic EDI.

Type of model: GLMM

Base formula: \text{mel EDI} \sim \text{mH-LEA}*\text{Site} + (1|\text{Site}:\text{Participant})

Notes: - site as a random effect with random slopes for mH-LEA was unstable. Thus, site will be used as an interaction - mH-LEA data was used to collect the primary_lightsource, which is the variable used in the model - The confirmatory test looks at the base model compared to the Null model for mH-LEA, the other comparisons are exploratory - Site was sum-coded, so as to calculate an average effect of site - Error family is Tweedie

Specific formulas: - \text{mel EDI} \sim \text{Site} + (1|\text{Site}:\text{Participant}) (Null-model, mH-LEA) - \text{mel EDI} \sim \text{mH-LEA} + \text{Site} + (1|\text{Site}:\text{Participant}) (Null-model interaction) - \text{mel EDI} \sim \text{mH-LEA} + (1|\text{Site}:\text{Participant}) (Null-model, Site)

False-discovery-rate correction: - non for confirmatory test (1 comparison) - 9 for site-effects - 4 for mH-LEA

Preparation

In [3]:
Code
Load hourly self-reported light exposure
#| filename: Load hourly self-reported light exposure
mHLEA <- load_data("lightexposurediary") |> flatten_data()
mHLEA_labels <- 
  mHLEA|> extract_labels() |> 
  enframe() |> 
  filter(str_detect(name, "act_")) |> 
  mutate(name = name |> str_remove("act_"))
In [4]:
Code
hourly_data <- 
  hourly_data |> 
  aggregate_Datetime(
    "1 hour",
    type = "floor",
    numeric.handler = \(x) x |> mean(na.rm = TRUE),
    geo.MEDI = MEDI |> log_zero_inflated() |> mean(na.rm = TRUE) |> exp_zero_inflated()
  )|>
  add_Date_col(group.by = TRUE) |> 
  mutate(static = all(MEDI == MEDI[1])) |> 
  filter_out(static) |> 
  select(-static) |> 
  ungroup(Date)
In [5]:
Code
Merge light data with self-reports
#| filename: Merge light data with self-reports
hourly_data <- 
  hourly_data |> 
  group_by(site, Id, Date) |> 
  add_states(mHLEA)
In [6]:
Code
Plot overview of light against self-reports
#| filename: Plot overview of light against self-reports
#| fig-width: 12
#| fig-height: 5

addline_format <- function(x,...){
    gsub('\\s','\n',x)
}

hourly_data |> 
  filter_out(is.na(lightsource_primary)) |> 
  ggplot(aes(x=lightsource_primary, y = MEDI)) +
  geom_boxplot() +
  scale_x_discrete(labels = addline_format) +
  scale_y_continuous(trans = "symlog", 
                     breaks = c(0,1,10,100,1000,10000,10^5),
                     labels = expression(0,1,10,10^2,10^3,10^4,10^5)) +
  theme_cowplot() +
  labs(y = "Melanopic EDI (lx)", x = "Primary lightsource")
Warning: Removed 577 rows containing non-finite outside the scale range
(`stat_boxplot()`).

In [7]:
Code
hourly_data |> 
  ungroup() |> 
  count(lightsource_primary) |> 
  mutate(pct = (n / sum(n))) |> 
  gt() |> fmt_percent(pct, decimals = 0)
Primary lightsource (mH-LEA) n pct
Electric light source indoors 4866 25%
Electric light source outdoors 222 1%
Daylight indoors 4927 25%
Daylight outdoors (including shade) 1642 8%
Emissive display light 681 4%
Darkness during sleep 5210 27%
Light entering from outside during sleep 858 4%
NA 1010 5%

We will remove any subgroup that has less than 0.1 % of the total datapoints, i.e., 20 observations within a site, and then 1% less than overall, i.e., 200 observations. We will also drop “Light entering from outside during sleep”, as it is very unevenly populated and dropped site levels will lead to issues later.

In [8]:
Code
hourly_data2 <-
  hourly_data |> 
  group_by(site, lightsource_primary) |> 
  mutate(n = n(),
         lightsource_primary = lightsource_primary |> 
           replace_when((n < 20 ) ~ NA)
           ) |> 
  group_by(lightsource_primary) |> 
  mutate(n = n(),
         lightsource_primary = lightsource_primary |> 
           replace_when((n < 200 ) ~ NA,
                        lightsource_primary == "Light entering from outside during sleep" ~ NA),
         site = factor(site)
           )
In [9]:
Code
library(nnet)

Attaching package: 'nnet'
The following object is masked from 'package:mgcv':

    multinom
Code
hourly_data3 <- hourly_data2 |> mutate(lightsource_primary = fct_drop(lightsource_primary))
model_lightsource_site <- multinom(
  lightsource_primary ~ site,
  data = hourly_data3
)
# weights:  50 (36 variable)
initial  value 27885.121271 
iter  10 value 24572.228146
iter  20 value 24309.089361
iter  30 value 24235.202330
iter  40 value 24213.472274
final  value 24213.045527 
converged
Code
model_lightsource_site0 <- multinom(
  lightsource_primary ~ 1,
  data = hourly_data3
)
# weights:  10 (4 variable)
initial  value 27885.121271 
final  value 24708.609443 
converged
Code
anova(model_lightsource_site, model_lightsource_site0, test = "Chisq")
Likelihood ratio tests of Multinomial Models

Response: lightsource_primary
  Model Resid. df Resid. Dev   Test    Df LR stat. Pr(Chi)
1     1     69300   49417.22                              
2  site     69268   48426.09 1 vs 2    32 991.1278       0
Code
prop.table(
  table(hourly_data3$site, hourly_data3$lightsource_primary),
  margin = 1)
         
          Electric light source indoors Daylight indoors
  BAUA                       0.15759438       0.34591747
  FUSPCEU                    0.34988272       0.19898358
  IZTECH                     0.34740883       0.28646833
  KNUST                      0.34945652       0.14619565
  MPI                        0.27360248       0.33788820
  RISE                       0.22641509       0.31560892
  THUAS                      0.23693587       0.28087886
  TUM                        0.27475058       0.34075211
  UCR                        0.34590164       0.33934426
         
          Daylight outdoors (including shade) Emissive display light
  BAUA                             0.15188762             0.04345917
  FUSPCEU                          0.08639562             0.03752932
  IZTECH                           0.04654511             0.03214971
  KNUST                            0.13369565             0.03206522
  MPI                              0.08074534             0.01459627
  RISE                             0.10177244             0.06232133
  THUAS                            0.10391924             0.07363420
  TUM                              0.07290867             0.02609363
  UCR                              0.03934426             0.07540984
         
          Darkness during sleep
  BAUA               0.30114135
  FUSPCEU            0.32720876
  IZTECH             0.28742802
  KNUST              0.33858696
  MPI                0.29316770
  RISE               0.29388222
  THUAS              0.30463183
  TUM                0.28549501
  UCR                0.20000000
Code
chisq.test(table(hourly_data3$site, hourly_data3$lightsource_primary))

    Pearson's Chi-squared test

data:  table(hourly_data3$site, hourly_data3$lightsource_primary)
X-squared = 940.65, df = 32, p-value < 2.2e-16

Analysis

In [10]:
Code
Fit the model for H3
#| filename: Fit the model for H3
#| fig-height: 10
#| fig-width: 8
H3_formula_1 <- geo.MEDI ~ site*lightsource_primary + (1|Id)
H3_formula_0 <- geo.MEDI ~ site + (1|Id)
H3_formula_1_ni <- geo.MEDI ~ site + lightsource_primary + (1|Id)
H3_formula_1_ns <- geo.MEDI ~ lightsource_primary + (1|Id)

#confirmatory
H3_model_1 <- 
  glmmTMB(H3_formula_1, 
          data = hourly_data2 |> drop_na(lightsource_primary), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H3_model_0 <- 
  glmmTMB(H3_formula_0, 
          data = hourly_data2 |> drop_na(lightsource_primary), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
#exploratory:
H3_model_1_ni <- 
  glmmTMB(H3_formula_1_ni, 
          data = hourly_data2 |> drop_na(lightsource_primary), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H3_model_1_ns <- 
  glmmTMB(H3_formula_1_ns, 
          data = hourly_data2 |> drop_na(lightsource_primary), 
          REML = FALSE, 
          family = tweedie(),
          )

comp_confirmatory <- 
anova(H3_model_0, H3_model_1)
comp_confirmatory
Data: drop_na(hourly_data2, lightsource_primary)
Models:
H3_model_0: geo.MEDI ~ site + (1 | Id), zi=~0, disp=~1
H3_model_1: geo.MEDI ~ site * lightsource_primary + (1 | Id), zi=~0, disp=~1
           Df    AIC    BIC logLik deviance Chisq Chi Df Pr(>Chisq)    
H3_model_0 12 156906 156999 -78441   156882                            
H3_model_1 48 145714 146085 -72809   145618 11264     36  < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H3
#| fig-height: 10
#| fig-width: 8
comp_interaction <- 
anova(H3_model_1, H3_model_1_ni)
comp_interaction
Data: drop_na(hourly_data2, lightsource_primary)
Models:
H3_model_1_ni: geo.MEDI ~ site + lightsource_primary + (1 | Id), zi=~0, disp=~1
H3_model_1: geo.MEDI ~ site * lightsource_primary + (1 | Id), zi=~0, disp=~1
              Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H3_model_1_ni 16 146303 146427 -73136   146271                             
H3_model_1    48 145714 146085 -72809   145618 653.26     32  < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H3
#| fig-height: 10
#| fig-width: 8
comp_site <- 
anova(H3_model_1_ns, H3_model_1_ni)
comp_site
Data: drop_na(hourly_data2, lightsource_primary)
Models:
H3_model_1_ns: geo.MEDI ~ lightsource_primary + (1 | Id), zi=~0, disp=~1
H3_model_1_ni: geo.MEDI ~ site + lightsource_primary + (1 | Id), zi=~0, disp=~1
              Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H3_model_1_ns  8 146324 146385 -73154   146308                             
H3_model_1_ni 16 146303 146427 -73136   146271 36.081      8  1.697e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H3
#| fig-height: 10
#| fig-width: 8
AIC(H3_model_1_ni, H3_model_1, H3_model_0, H3_model_1_ns) |> 
  arrange(AIC) |> 
  rownames_to_column("model") |> gt()
model df AIC
H3_model_1 48 145714.2
H3_model_1_ni 16 146303.5
H3_model_1_ns 8 146323.6
H3_model_0 12 156905.9

Both AIC and ∆loglik comparisons suggest that the interaction model is the most relevant one.

In [11]:
Code
check_model(H3_model_1)
`check_outliers()` does not yet support models of class `glmmTMB`.

In [12]:
Code
tab_model(H3_model_1, CSS = css_theme("cells"))
  geo.MEDI
Predictors Estimates CI p
(Intercept) 74.61 64.54 – 86.26 <0.001
site1 1.12 0.77 – 1.63 0.563
site2 1.48 1.08 – 2.04 0.015
site3 0.87 0.60 – 1.26 0.471
site4 0.50 0.34 – 0.73 <0.001
site5 0.56 0.41 – 0.76 <0.001
site6 2.04 1.35 – 3.08 0.001
site7 2.16 1.43 – 3.25 <0.001
site8 0.96 0.61 – 1.52 0.860
lightsource_primary:
Daylight outdoors
(including shade)
2.31 2.13 – 2.49 <0.001
lightsource_primary:
Emissive display light
9.47 8.52 – 10.53 <0.001
lightsource_primary:
Darkness during sleep
0.28 0.24 – 0.34 <0.001
lightsource_primary:
Light entering from
outside during sleep
0.02 0.02 – 0.02 <0.001
site1:lightsource_primaryDaylight indoors 1.14 0.92 – 1.41 0.242
site2:lightsource_primaryDaylight indoors 0.74 0.62 – 0.88 0.001
site3:lightsource_primaryDaylight indoors 0.99 0.82 – 1.19 0.889
site4:lightsource_primaryDaylight indoors 0.79 0.63 – 1.01 0.056
site5:lightsource_primaryDaylight indoors 1.74 1.47 – 2.06 <0.001
site6:lightsource_primaryDaylight indoors 0.68 0.55 – 0.83 <0.001
site7:lightsource_primaryDaylight indoors 0.70 0.57 – 0.87 0.001
site8:lightsource_primaryDaylight indoors 1.21 0.96 – 1.52 0.113
site1:lightsource_primaryDaylight outdoors (including shade) 1.00 0.78 – 1.28 0.992
site2:lightsource_primaryDaylight outdoors (including shade) 0.51 0.40 – 0.64 <0.001
site3:lightsource_primaryDaylight outdoors (including shade) 1.56 1.18 – 2.07 0.002
site4:lightsource_primaryDaylight outdoors (including shade) 1.05 0.84 – 1.32 0.662
site5:lightsource_primaryDaylight outdoors (including shade) 1.30 1.04 – 1.62 0.023
site6:lightsource_primaryDaylight outdoors (including shade) 1.50 1.18 – 1.91 0.001
site7:lightsource_primaryDaylight outdoors (including shade) 0.91 0.71 – 1.16 0.429
site8:lightsource_primaryDaylight outdoors (including shade) 0.95 0.70 – 1.29 0.758
site1:lightsource_primaryEmissive display light 3.06 2.06 – 4.54 <0.001
site2:lightsource_primaryEmissive display light 3.44 2.37 – 4.98 <0.001
site3:lightsource_primaryEmissive display light 2.28 1.46 – 3.57 <0.001
site4:lightsource_primaryEmissive display light 1.35 0.80 – 2.30 0.264
site5:lightsource_primaryEmissive display light 0.28 0.15 – 0.51 <0.001
site6:lightsource_primaryEmissive display light 0.26 0.17 – 0.41 <0.001
site7:lightsource_primaryEmissive display light 2.92 2.08 – 4.11 <0.001
site8:lightsource_primaryEmissive display light 0.08 0.04 – 0.20 <0.001
site1:lightsource_primaryDarkness during sleep 0.99 0.75 – 1.31 0.928
site2:lightsource_primaryDarkness during sleep 0.36 0.28 – 0.47 <0.001
site3:lightsource_primaryDarkness during sleep 1.37 1.05 – 1.79 0.022
site4:lightsource_primaryDarkness during sleep 0.29 0.21 – 0.39 <0.001
site5:lightsource_primaryDarkness during sleep 1.44 1.14 – 1.81 0.002
site6:lightsource_primaryDarkness during sleep 0.67 0.50 – 0.91 0.010
site7:lightsource_primaryDarkness during sleep 0.61 0.47 – 0.80 <0.001
site8:lightsource_primaryDarkness during sleep 6.70 4.98 – 9.01 <0.001
Random Effects
σ2 1.08
τ00Id 0.55
ICC 0.34
N Id 140
Observations 16774
Marginal R2 / Conditional R2 0.778 / 0.853

Basic table

In [13]:
Code
H3_tab_ran <- 
  H3_model_1 |> 
  tidy() |> 
  mutate(signif = p.value <= sig.level,
         estimate = exp(estimate) |> round(2)) |> 
  filter(effect == "ran_pars") |> 
  select(term, estimate)

H3_tab_reference <-
  H3_model_1 |>
  emmeans(~ lightsource_primary, type = "response",
          at = list(lightsource_primary = "Electric light source indoors")) |> 
  as_tibble() |> 
  rename(estimate = response)
NOTE: Results may be misleading due to involvement in interactions
Code
H3_tab_light <- 
  H3_model_1 |> 
  emmeans(~ lightsource_primary, type = "response") |> 
  contrast(method = "trt.vs.ctrl") |> 
  as_tibble() |> 
  rename(estimate = ratio)
NOTE: Results may be misleading due to involvement in interactions
Code
H3_tab_interaction <-
  H3_model_1 |> 
  emmeans(~ site | lightsource_primary, type = "response") |> 
  contrast() |> 
  as_tibble() |> 
  rename(estimate = ratio)

H3_tab_fix <-
  bind_rows(H3_tab_reference, H3_tab_light, H3_tab_interaction) |> 
  mutate(signif = p.value <= sig.level) |> 
  select(-c(df, asymp.LCL, asymp.UCL, null, z.ratio)) |> 
  relocate(contrast, .before = 1) |> 
  mutate(site = contrast |> as.character(),
         lightsource_primary = 
           lightsource_primary |> as.character() |> 
           replace_when(
             str_detect(contrast, "Electric light source indoors") ~ str_remove(contrast, " / Electric light source indoors")
           ),
         site = replace_when(site, 
                             is.na(site) ~ "Overall",
                             str_detect(site, " effect") ~ str_remove(site, " effect"),
                             str_detect(contrast, "Electric light source indoors") ~ "Overall"),
         .before = 1
                              
  ) |> 
  select(-contrast) |> 
  pivot_wider(id_cols = site, names_from = lightsource_primary, values_from = estimate:signif)
  # mutate(
         # across(3:6,
                # \(x) c(x[1], x[-1]*x[1]))
         # )
In [14]:
Code
H3_r2_1 <- r2_helper(H3_model_1, hourly_data2 |> drop_na(lightsource_primary), tweedie(), "geo.MEDI", "(1 | Id)")
H3_r2_0 <- r2_helper(H3_model_0, hourly_data2 |> drop_na(lightsource_primary), tweedie(), "geo.MEDI", "(1 | Id)")
H3_r2_interaction<- r2_helper(H3_model_1_ni, hourly_data2 |> drop_na(lightsource_primary), tweedie(), "geo.MEDI", "(1 | Id)")
H3_r2_site<- r2_helper(H3_model_1_ns, hourly_data2 |> drop_na(lightsource_primary), tweedie(), "geo.MEDI", "(1 | Id)")
H3_tab_r2 <-
  tribble(
    ~type, ~r2,
    "Model", H3_r2_1$R2_conditional,
    "All fixed", H3_r2_1$R2_marginal,
    "Residual", 1-H3_r2_1$R2_conditional,
    "Lightsource", H3_r2_1$R2_marginal - H3_r2_0$R2_marginal,
    "Site", H3_r2_1$R2_marginal - H3_r2_site$R2_marginal,
    "Random", H3_r2_1$R2_conditional - H3_r2_1$R2_marginal,
    "Interaction", H3_r2_1$R2_marginal - H3_r2_interaction$R2_marginal,
  ) |> 
  mutate(r2= r2 |> round(2))
In [15]:
Code
H3_tab <-
H3_tab_fix |> 
  site_conv_mutate(rev = FALSE, other.levels = "Overall") |> 
  arrange(site) |> 
  gt() |> 
  cols_label_with(fn = \(x) str_remove(x, "estimate_") |> str_remove("lightsource_primary")) |> 
  tab_spanner(
    md(
      paste0("Primary lightsource ($p = ",
             comp_confirmatory$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H3_tab_r2$r2[4], 
             "$)")
      ), 
    columns = -site) |> 
  cols_hide((contains(c("p.value", "signif", "SE_")))) |> 
  sub_missing(missing_text = "") |> 
  tab_style_body(
    style = cell_borders(weight = px(2)),
    rows = 1,
    columns = 2,
    fn = \(x) TRUE
  ) |> 
  gt::rows_add(site = NA, .after = 1) |> 
  tab_style(
    style = list(css(`padding-top` = "0px",
            `padding-bottom` = "0px"),
            cell_fill("lightgrey")
            ),
locations = cells_body(rows = 2)) |>
  fmt(starts_with("estimate"), 
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, "×")}
      ) |> 
  fmt(columns = 2, 
      rows = site == "Overall",
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, "lx")}
      ) |> 
  gt_multiple(
    names(H3_tab_fix) |> 
      subset(str_detect(names(H3_tab_fix), "estimate_")) |> 
      str_remove("estimate_"),
    bold_p.) |> 
  gt_multiple(
    melidos_colors |> names(),
    style_rows
  ) |> 
  tab_style(
    style = cell_text(weight = "bold"),
    locations = list(
      cells_column_labels(),
      cells_column_spanners(),
      cells_body(columns = site),
      cells_body(2,1)
    )
  ) |> 
  cols_label(
    site = md(paste0("Site<br>($p = ",
             comp_site$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H3_tab_r2$r2[5], 
             "$)"))
    ) |> 
  tab_header(
    title = "Model results for Hypothesis 3",
    subtitle = H3
  ) |> 
  tab_footnote(
    "Reference value, for electric light indoors and a site average. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.", locations = cells_body(2,1)
  ) |> 
  tab_footnote(
    md(paste0("Conditional (Model) $R^2=", 
              H3_tab_r2$r2[1],
              "$, Marginal (Fixed effects) $R^2=",
              H3_tab_r2$r2[2],
              "$, Random effect $R^2=",
              H3_tab_r2$r2[6],
              "$, Residual Variance $R^2=",
              H3_tab_r2$r2[3],
              "$; Random effect of participant: $×",
              H3_tab_ran$estimate[1],
              "$; Model based on ",
              hourly_data2 |> drop_na(geo.MEDI, lightsource_primary) |> nrow() |> vec_fmt_integer(sep_mark = "'"),
              " participant-hours."
              ))
  ) |> 
  tab_footnote(
    md(paste0("Interaction effect of primary lightsource with site is significant ($p=", 
              comp_interaction$`Pr(>Chisq)`[2] |> style_pvalue(),
              "$, $R^2 = ",
              H3_tab_r2$r2[7],
              "$)"))
  ) |> 
  tab_footnote(
    md("Values shown in **bold** are significant at $p<0.05$. False-discovery-rate adjustment for n=4 in primary lightsource and n=9 in site")
  ) |> 
  cols_width(everything() ~ px(150)) 
In [16]:
Code
H3_tab
Model results for Hypothesis 3
Hypothesis: There is a significant relationship between hourly self-reported light exposure categories and hourly geometric mean of melanopic EDI.
Site
(p = <0.001, R^2=0.05)
Primary lightsource (p = <0.001, R^2=0.71)
Electric light source indoors Daylight indoors Daylight outdoors (including shade) Emissive display light Darkness during sleep
Overall 1 74.61lx 2.31× 9.47× 0.28× 0.02×






Borås (SE) 2.04× 1.39× 3.06× 0.54× 1.37×
Delft (NL) 2.16× 1.51× 1.95× 6.30× 1.32×
Dortmund (DE) 1.12× 1.27× 1.12× 3.42× 1.10×
Tübingen (DE) 0.56× 0.97× 0.72× 0.15× 0.80×
Munich (DE) 0.96× 1.16× 0.91× 0.08× 6.42×
Madrid (ES) 1.48× 1.09× 0.75× 5.10× 0.54×
Izmir (TR) 0.87× 0.86× 1.36× 1.99× 1.20×
San José (CR) 0.59× 0.90× 0.42× 1.01× 1.06×
Kumasi (GH) 0.50× 0.40× 0.52× 0.67× 0.14×
Conditional (Model) R^2=0.85, Marginal (Fixed effects) R^2=0.78, Random effect R^2=0.07, Residual Variance R^2=0.15; Random effect of participant: ×2.1; Model based on 16’774 participant-hours.
Interaction effect of primary lightsource with site is significant (p=<0.001, R^2 = 0.02)
Values shown in bold are significant at p<0.05. False-discovery-rate adjustment for n=4 in primary lightsource and n=9 in site
1 Reference value, for electric light indoors and a site average. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.
Code
gtsave(H3_tab, "tables/H3.png")
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpGsFdJh/file6fba726cbf88.html screenshot completed
Code
gtsave(H3_tab, "tables/H3.docx")

Visualization

In [17]:
Code
visualize H3 result
#| filename: visualize H3 result
#| fig-width: 10
#| fig-height: 12
H3_plot <- 
H3_model_1 |> 
  emmeans(~ site | lightsource_primary, type = "response") |> 
  plot()

H3_plot <-
  H3_plot$data |> 
  as.tibble() |> 
  site_conv_mutate(site = pri.fac) |> 
    ggplot(
      aes(x = the.emmean, y = pri.fac)
    ) +
    geom_crossbar(aes(xmin = lcl, xmax = ucl, fill = pri.fac), col = NA, alpha = 0.7) +
    geom_point() +
  theme_cowplot() +
  theme(panel.grid.major = element_line(color = "grey")) +
  scale_fill_manual(values = melidos_colors) +
  facet_wrap(~lightsource_primary, ncol = 1) +
  scale_x_continuous(trans = "symlog", breaks = c(0, 10^(0:5), 250, 5000)) +
  coord_cartesian(xlim = c(0, 5000)) +
  labs(y = NULL, x = "Melanopic EDI (lx), based on hourly geometric mean") +
    guides(fill = "none")
Warning: `as.tibble()` was deprecated in tibble 2.0.0.
ℹ Please use `as_tibble()` instead.
ℹ The signature and semantics have changed, see `?as_tibble`.
Code
#| filename: visualize H3 result
#| fig-width: 10
#| fig-height: 12
H3_plot

Code
#| filename: visualize H3 result
#| fig-width: 10
#| fig-height: 12
hourly_data2 |> 
    filter_out(is.na(lightsource_primary)) |> 
    site_conv_mutate(rev = FALSE) |> 
    ggplot(aes(x=geo.MEDI)) +
    geom_density_ridges(aes(y=lightsource_primary),
                        linewidth = 1, colour = NA,
                        alpha = 0.5,
                        from = 0,
    ) +
    geom_density_ridges(aes(y=lightsource_primary), fill = NA,
                        alpha = 1, linewidth = 1,
                        quantile_lines = TRUE, quantiles = 2, vline_color = "red",
                        linetype = 1,
                        from = 0,
    ) +
    # scale_fill_manual(values = melidos_colors) +
    scale_alpha_continuous(range = c(0, 1)) +
    scale_x_continuous(trans = "symlog", breaks = c(0, 10^(0:5)),
                       labels = expression(0, 1, 10, 10^2, 10^3, 10^4, 10^5)) +
    # scale_color_manual(values = melidos_colors) +
    guides(fill = "none", color = "none") +
    labs(y = NULL, x = NULL) +
    theme_ridges() +
    coord_cartesian(xlim = c(0, 10^5)) +
    theme_sub_plot(margin = margin(r = 20, t = 20)) +
    theme_sub_axis_left(text = element_text(vjust = 0))
Picking joint bandwidth of 0.18
Warning: Removed 552 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 0.18
Warning: Removed 552 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

Interpretation

  • Overall, an average mel EDI of 74.1 lx (based on geometric mean) is reached with electric light indoors. This value somewhat varies between sites, with Sweden, The Netherlands, and Madrid achieving substantially higher values (factor 2.06, 2.17, and 1.49, respectively). They nevertheless do not reach recommended values of 250lx during the day. Tübingen, Germany, and Ghana reach significantly lower levels (factor 0.56 and 0.5, respectively).
  • When the primary lightsource is Daylight indoors, about 2.34 times as much mel EDI can be achieved on average (173.4 lx). The only significant deviation is Ghana, which scores 61% less than the other sites (factor 0.39).
  • When people are receiving Daylight outdoors (including shade), they achieve about 9.81 times more light compared to indoor electric light (727 lx). Sweden and the Netherlands achieve still higher values (times 2.98 and 1.9, respectively), while Ghana has only about half (0.51)
  • Emissive display light as the primary lightsource leads to values of melanopic EDI of 29% of indoor electric light (21.5 lx). There is very high variance between the different sites, with factor ranging between 8% (Munich, Germany) to 623% (The Netherlands).
  • Darkness during sleep translates to about 2% of light levels compared to indoor electric light (~1.5 lx). Values are similar across sites, with three notable exceptions: Munich, Germany has over 6 times the level compared to other sites (6.61), while Spain and Ghana have considerably lower values (0.55 and 0.15, respectively).
  • Overall, the effect of self-reported primary lightsource is astounding - 71% of variance can be explained simply through those categories. While site and interindividual differences are significant, they contribute considerably less (5% and 7%, respectively).

Exploration: GAM of lightsource x time

In this exploratory analysis, we study the nonlinear effect of time on the type of primary lightsource, based on the final model from RQ1:

In [18]:
Code
Prepare GAM data
#| filename: Prepare GAM data
GAM_data <- 
  hourly_data2 |>
  add_Time_col() |> 
  drop_na(lightsource_primary) |> 
  ungroup() |> 
  mutate(Time = as.numeric(Time)/3600 + 0.5,
         across(c(site, Id, sleep, State.Brown, wear, photoperiod.state,
                  lightsource_primary, Date),
                factor),
         Id_date = interaction(Id, Date),
         lzMEDI = log_zero_inflated(MEDI),
         photoperiod = (dusk - dawn) |> as.numeric()
  ) |> 
  group_by(Id) |> 
  mutate(AR.start = ifelse(row_number() == 1, TRUE, FALSE))

H3_form_gam <- lzMEDI ~ s(Time, k = 12) + s(Time, lightsource_primary, bs = "sz", k = 12) + s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, bs = "sz", k=12) + s(Time, Id, bs = "fs") + s(Id_date, bs = "re")
H2_form_gam <- lzMEDI ~ s(Time, k = 12) + s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, bs = "sz", k=12) + s(Time, Id, bs = "fs") + s(Id_date, bs = "re")
In [19]:
Code
Main model
#| filename: Main model
H3_model_gam <- 
  bam(H3_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10
      )

r1 <- start_value_rho(H3_model_gam, 
                      plot=TRUE)

Code
#| filename: Main model
H3_model_gam <- 
  bam(H3_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10,
      rho = r1,
      AR.start = AR.start
      )

acf_resid(H3_model_gam, split_pred = "AR.start")

Code
#| filename: Main model
H3_summary_gam <- summary(H3_model_gam)
H3_summary_gam

Family: gaussian 
Link function: identity 

Formula:
lzMEDI ~ s(Time, k = 12) + s(Time, lightsource_primary, bs = "sz", 
    k = 12) + s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, 
    bs = "sz", k = 12) + s(Time, Id, bs = "fs") + s(Id_date, 
    bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.93970    0.05415   17.35   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                                edf   Ref.df       F  p-value    
s(Time)                      10.320   10.607  62.517  < 2e-16 ***
s(Time,lightsource_primary)  20.112   22.348 109.693  < 2e-16 ***
s(Time,site)                 61.214   69.602   5.099  < 2e-16 ***
s(Time,photoperiod.state)     6.584    7.544   3.509 0.000413 ***
s(Time,Id)                  569.445 1382.000   3.041  < 2e-16 ***
s(Id_date)                  343.837  785.000   0.884  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.821   Deviance explained = 83.2%
fREML =  16453  Scale est. = 0.40603   n = 16774
Code
#| filename: Main model
H3_model_gam |> glance()
# A tibble: 1 × 9
     df  logLik    AIC    BIC deviance df.residual  nobs adj.r.squared  npar
  <dbl>   <dbl>  <dbl>  <dbl>    <dbl>       <dbl> <int>         <dbl> <int>
1 1013. -14830. 31710. 39634.    6655.      15761. 16774         0.821  2362
Code
#| filename: Main model
H3_model_gam |> appraise()

In [20]:
Code
Variance explained
#| filename: Variance explained
Xp <- predict(H3_model_gam, type = "terms")
variance <- apply(Xp, 2, var)
cor(Xp)
                               s(Time) s(Time,lightsource_primary) s(Time,site)
s(Time)                     1.00000000                  0.64818658  0.040341286
s(Time,lightsource_primary) 0.64818658                  1.00000000  0.095527124
s(Time,site)                0.04034129                  0.09552712  1.000000000
s(Time,photoperiod.state)   0.22569356                  0.06179673  0.115731014
s(Time,Id)                  0.02528643                  0.13142176 -0.014679858
s(Id_date)                  0.00666594                  0.03008012 -0.002836419
                            s(Time,photoperiod.state)  s(Time,Id)   s(Id_date)
s(Time)                                    0.22569356  0.02528643  0.006665940
s(Time,lightsource_primary)                0.06179673  0.13142176  0.030080119
s(Time,site)                               0.11573101 -0.01467986 -0.002836419
s(Time,photoperiod.state)                  1.00000000  0.04929679  0.012598470
s(Time,Id)                                 0.04929679  1.00000000  0.122249040
s(Id_date)                                 0.01259847  0.12224904  1.000000000
Code
#| filename: Variance explained
H3_R2 <- (variance/sum(variance)) |> enframe(value = "R2", name = "parameter")
H3_R2
# A tibble: 6 × 2
  parameter                        R2
  <chr>                         <dbl>
1 s(Time)                     0.681  
2 s(Time,lightsource_primary) 0.153  
3 s(Time,site)                0.0504 
4 s(Time,photoperiod.state)   0.00603
5 s(Time,Id)                  0.0963 
6 s(Id_date)                  0.0133 
In [21]:
Code
Null model
#| filename: Null model
H2_model_gam <- 
  bam(H2_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10
      )

r1 <- start_value_rho(H2_model_gam, 
                      plot=TRUE)

Code
#| filename: Null model
H2_model_gam <- 
  bam(H2_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10,
      rho = r1,
      AR.start = AR.start
      )

acf_resid(H2_model_gam, split_pred = "AR.start")

In [22]:
Code
AIC(H3_model_gam, H2_model_gam)
                   df      AIC
H3_model_gam 1025.376 31710.08
H2_model_gam 1015.007 33613.90
In [23]:
Code
H3_model_gam |> draw(select = "s(Time,lightsource_primary)")

Code
H3_model_gam |> draw(select = "s(Time)")

####Visualization

In [24]:
Code
prepare lightsource model H3 plot
#| filename: prepare lightsource model H3 plot
H3_ls_data <- 
H3_model_gam |> 
  conditional_values(
    condition = list(Time = seq(0.5, 23.5, by = 1), "lightsource_primary", "photoperiod.state"),
    exclude = c("s(Time,Id)", "s(Id_date)", "s(Time,site)")
    ) |> 
  mutate(across(c(.fitted, .lower_ci, .upper_ci),
                exp_zero_inflated)
  )

H3_ls_data <-
GAM_data |> 
  Datetime2Time() |> 
  group_by(lightsource_primary, Time) |> 
  summarize(
    .groups = "drop_last",
    dawn = dawn |> as.numeric() |> mean(na.rm = TRUE)/3600,
    dusk = dusk |> as.numeric() |> mean(na.rm = TRUE)/3600,
    n = n()
  ) |> 
  mutate(
    photoperiod.state = ifelse(between(Time, dawn, dusk), "day", "night")
  ) |> 
  select(-c(dawn, dusk)) |> 
  left_join(H3_ls_data, by = c("lightsource_primary", "Time", "photoperiod.state"))

H3_ls_data_phot <- 
H3_ls_data |> 
  number_states(photoperiod.state) |> 
  filter(photoperiod.state == "night") |> 
  group_by(lightsource_primary, photoperiod.state.count) |> 
  summarize(xmin = min(Time),
            xmax = max(Time),
            .groups = "drop_last") |> 
  mutate(across(c(xmin, xmax), \(x) replace_values(x,
                                                   0.5 ~ 0,
                                                   23.5 ~ 24)))
In [25]:
Code
Visualize lightsource model H3
#| filename: Visualize lightsource model H3
#| fig-height: 5
#| fig-width: 14
source("scripts/Brown_bracket.R")
Loading required package: legendry
Code
#| filename: Visualize lightsource model H3
#| fig-height: 5
#| fig-width: 14
ls_colors <- 
  c(
    `Darkness during sleep` = "#1B1F3B",  # deep navy
    `Daylight indoors` = "#E6C229",       # warm yellow
    `Daylight outdoors (including shade)` = "#4DAF4A", # medium leaf green
    `Electric light source indoors` = "#D55E00", # amber/orange
    `Emissive display light` = "#3B5BDB" # medium purple
  )

H3_pred <- 
H3_ls_data |> 
  ggplot(aes()) +
  map(c(1,10,250, 10^4), 
          \(x) geom_hline(aes(yintercept = x), 
                          col = "grey", linetype = "dashed")
      ) +
  geom_rect(data = H3_ls_data_phot ,
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = lightsource_primary), alpha = 0.5) +
  geom_line(aes(x = Time, y = .fitted, 
                col = lightsource_primary), linewidth = 0.1) +
  geom_line(aes(x = Time, y = .fitted), linewidth = 1) +
  scale_fill_manual(values = ls_colors) +
  scale_color_manual(values = ls_colors) +
  theme_cowplot() +
  scale_y_continuous(trans = "symlog", breaks = c(0,1,10,100,250, 1000)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), ylim = c(0,10^4), expand = FALSE) +
  labs(y = "Melanopic EDI (lx)", x = "Local time (hrs)", fill = "Primary lightsource", col = "Primary lightsource") +
  facet_wrap(~lightsource_primary, nrow = 1) +
  guides( y = guide_axis_stack(Brown_bracket, "axis")) +
  gghighlight(
    label_key = lightsource_primary,
    label_params = list(x = 12, y = 4.5, force = 0, hjust = 0.5)
  ) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
  # labs(caption = Brown_caption)
H3_pred
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_ribbon()`).

In [26]:
Code
Visualize model terms
#| filename: Visualize model terms
Term_data <- 
  H3_ls_data |> select(Time, lightsource_primary) |> 
  left_join(
    H3_model_gam |> 
  smooth_estimates(select = "s(Time,lightsource_primary)", n=24),
  by = c("Time", "lightsource_primary")
  ) |> 
  ungroup() |> 
  complete(Time, lightsource_primary) |> 
  mutate(.lower_ci = .estimate -1.96*.se,
         .upper_ci = .estimate +1.96*.se,
         .fitted = .estimate,
         signif = .lower_ci > 0 | .upper_ci < 0,
         signif_id = consecutive_id(signif),
         .by = lightsource_primary)

P_terms <-
Term_data |> 
  ggplot(aes()) +
  map(c(log10(c(0.1, 0.2, 0.5, 2, 5, 10))),
      \(x) geom_hline(yintercept = x, linewidth = 0.5, col = "grey", linetype = 2)) +
  geom_rect(data = H3_ls_data_phot ,
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = lightsource_primary), 
              alpha = 0.5) +
  geom_hline(yintercept = 0, linewidth = 1, linetype = 2) +
  geom_line(aes(x = Time, y = .fitted), 
            linewidth = 1) +
  geom_line(data = \(x) filter(x, signif),
            aes(x = Time, y = .fitted, group = signif_id), 
            col = "red", linewidth = 1) +
  scale_fill_manual(values = ls_colors) +
  # scale_color_manual(values = c(melidos_colors)) +
  theme_cowplot() +
  scale_y_continuous(labels = expression(10^-1, 5^-1, 2^-1, "1  ", "2  ", "5  ", "10  "), 
                     breaks = log10(c(0.1, 0.2, 0.5, 1, 2, 5, 10)))+
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), expand = FALSE) +
  guides(fill = "none") +
  labs(y = "Factor", x = "Local time (hrs)", fill = "Primary lightsource") +
  facet_wrap(~lightsource_primary, nrow = 1) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
P_terms
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_ribbon()`).

In [27]:
Code
n_plot_H3 <-
H3_ls_data |> 
  ungroup() |> 
  ggplot(aes(x=Time)) + 
  geom_col(aes(y=n, fill = lightsource_primary)) + 
  facet_wrap(~lightsource_primary, nrow = 1) +
  theme_cowplot() +
  scale_fill_manual(values = ls_colors) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple()) +
  guides(fill = "none") +
  # scale_y_continuous(trans = "symlog", breaks = c(0, 1, 10, 100, 500)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6))
In [28]:
Code
Combination of partial plots H3
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H3

(H3_pred / P_terms / n_plot_H3) + plot_layout(heights = c(2,2,1)) +
  plot_annotation(tag_levels = "A", 
                  caption = paste(Brown_caption, 
                                  "Panel B: Significant deviations from the average are shown in red", sep = "<br>"),
                  theme = 
                    theme(plot.caption = element_textbox_simple(size = rel(1.5))
                          )
                  )
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_ribbon()`).

Code
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H3

ggsave("figures/Fig9.pdf", height = 10, width = 15)
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Code
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H3

ggsave("figures/Fig9.png", height = 10, width = 15)
Warning: Removed 5 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 3 rows containing missing values or values outside the scale range
(`geom_ribbon()`).

Hypothesis 4

Details

Hypothesis 4 states:

There is a significant relationship between hourly self-reported activities and hourly geometric mean of melanopic EDI.

Type of model: GLMM

Base formula: \text{mel EDI} \sim \text{activity}*\text{Site} + (1|\text{Site}:\text{Participant})

Notes: - site as a random effect with random slopes for activity was unstable. Thus, site will be used as an interaction - data was used to collect various, which is the variable used in the model - The confirmatory test looks at the base model compared to the Null model for mH-LEA, the other comparisons are exploratory - Site was sum-coded, so as to calculate an average effect of site - Error family is Tweedie - We will drop the level “other” and combine outdoors levels

Specific formulas: - \text{mel EDI} \sim \text{Site} + (1|\text{Site}:\text{Participant}) (Null-model, mH-LEA) - \text{mel EDI} \sim \text{mH-LEA} + \text{Site} + (1|\text{Site}:\text{Participant}) (Null-model interaction) - \text{mel EDI} \sim \text{mH-LEA} + (1|\text{Site}:\text{Participant}) (Null-model, Site)

False-discovery-rate correction: - non for confirmatory test (1 comparison) - 9 for site-effects - 4 for activity

Preparation

In [29]:
Code
Prepare activity data
#| filename: Prepare activity data
activity_data <- 
hourly_data2 |> 
  ungroup() |> 
  select(site, Id, geo.MEDI, starts_with("act_"), Datetime, lightsource_primary) |> 
  pivot_longer(cols = starts_with("act_"), names_to = "activity", names_prefix = "act_", values_to = "value") |> 
  filter(value, !is.na(geo.MEDI)) |>
  distinct(.keep_all = TRUE, site, Id, Datetime, activity) |>
  select(-value)
In [30]:
Code
Plot overview of light against self-reported activity
#| filename: Plot overview of light against self-reported activity
#| fig-width: 12
#| fig-height: 5
activity_data |> 
  ggplot(aes(x=activity, y = geo.MEDI)) +
  geom_boxplot() +
  scale_x_discrete(labels = addline_format) +
  scale_y_continuous(trans = "symlog", 
                     breaks = c(0,1,10,100,1000,10000,10^5),
                     labels = expression(0,1,10,10^2,10^3,10^4,10^5)) +
  theme_cowplot() +
  labs(y = "Melanopic EDI (lx)", x = "Activity")

In [31]:
Code
activity_data |> count(activity)
# A tibble: 8 × 2
  activity            n
  <chr>           <int>
1 free_outdoor      664
2 home             5080
3 other             455
4 road_open         592
5 road_vehicle      811
6 sleep            5851
7 working_indoor   3595
8 working_outdoor   208

We will drop the other group, and we will combine working_outdoor with free_outdoor and road_open, as they are small or non-existant in in some sites.

In [32]:
Code
activity_data <-
  activity_data |> 
  mutate(activity = 
           fct(activity) |> 
           fct_collapse(outdoor = c("free_outdoor", "working_outdoor", "road_open")) |> 
           fct_recode(NULL = "other") |> 
           fct_relevel("home")
         )
In [33]:
Code
activity_data |> count(activity, sort = TRUE) |> mutate(pct = n / sum(n)) |> gt() |> fmt_percent(pct, decimals = 0)
activity n pct
sleep 5851 34%
home 5080 29%
working_indoor 3595 21%
outdoor 1464 8%
road_vehicle 811 5%
NA 455 3%
Code
activity_data <- 
  activity_data |> 
  drop_na(activity)
In [34]:
Code
library(nnet)
activity_data2 <- activity_data |> mutate(activity = fct_drop(lightsource_primary))
model_activity_site <- multinom(
  activity ~ site,
  data = activity_data2 
)
# weights:  50 (36 variable)
initial  value 24959.163146 
iter  10 value 21965.110780
iter  20 value 21694.712337
iter  30 value 21628.850452
iter  40 value 21607.493791
final  value 21607.232028 
converged
Code
model_activity_site0 <- multinom(
  activity ~ 1,
  data = activity_data2 
)
# weights:  10 (4 variable)
initial  value 24959.163146 
iter  10 value 22149.014391
iter  10 value 22149.014371
final  value 22149.014371 
converged
Code
anova(model_activity_site, model_activity_site0, test = "Chisq")
Likelihood ratio tests of Multinomial Models

Response: activity
  Model Resid. df Resid. Dev   Test    Df LR stat. Pr(Chi)
1     1     62028   44298.03                              
2  site     61996   43214.46 1 vs 2    32 1083.565       0
Code
prop.table(
  table(activity_data2$site, activity_data2$activity),
  margin = 1)
         
          Electric light source indoors Daylight indoors
  BAUA                       0.14452949       0.34759118
  FUSPCEU                    0.33915637       0.20622071
  IZTECH                     0.34602727       0.28255759
  KNUST                      0.33906071       0.14375716
  MPI                        0.33680343       0.29699939
  RISE                       0.22096774       0.32419355
  THUAS                      0.21415929       0.28967552
  TUM                        0.26484751       0.34510433
  UCR                        0.33649289       0.36176935
         
          Daylight outdoors (including shade) Emissive display light
  BAUA                             0.16839262             0.03016659
  FUSPCEU                          0.09288453             0.03323392
  IZTECH                           0.05406676             0.03244006
  KNUST                            0.13631157             0.03035510
  MPI                              0.04286589             0.01041029
  RISE                             0.10752688             0.06666667
  THUAS                            0.11150442             0.07138643
  TUM                              0.06500803             0.02728732
  UCR                              0.03949447             0.06951027
         
          Darkness during sleep
  BAUA               0.30932013
  FUSPCEU            0.32850447
  IZTECH             0.28490832
  KNUST              0.35051546
  MPI                0.31292100
  RISE               0.28064516
  THUAS              0.31327434
  TUM                0.29775281
  UCR                0.19273302
Code
chisq.test(table(activity_data2$site, activity_data2$activity))

    Pearson's Chi-squared test

data:  table(activity_data2$site, activity_data2$activity)
X-squared = 1026.1, df = 32, p-value < 2.2e-16

Analysis

In [35]:
Code
Fit the model for H4
#| filename: Fit the model for H4
#| fig-height: 10
#| fig-width: 8
H4_formula_1 <- geo.MEDI ~ site*activity + (1|Id)
H4_formula_0 <- geo.MEDI ~ site + (1|Id)
H4_formula_1_ni <- geo.MEDI ~ site + activity + (1|Id)
H4_formula_1_ns <- geo.MEDI ~ activity + (1|Id)

#confirmatory
H4_model_1 <- 
  glmmTMB(H4_formula_1, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H4_model_0 <- 
  glmmTMB(H4_formula_0, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
#exploratory:
H4_model_1_ni <- 
  glmmTMB(H4_formula_1_ni, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H4_model_1_ns <- 
  glmmTMB(H4_formula_1_ns, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          )

comp_confirmatory <- 
anova(H4_model_0, H4_model_1)
comp_confirmatory
Data: activity_data
Models:
H4_model_0: geo.MEDI ~ site + (1 | Id), zi=~0, disp=~1
H4_model_1: geo.MEDI ~ site * activity + (1 | Id), zi=~0, disp=~1
           Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H4_model_0 12 152942 153035 -76459   152918                             
H4_model_1 48 143055 143426 -71480   142959 9958.9     36  < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H4
#| fig-height: 10
#| fig-width: 8
comp_interaction <- 
anova(H4_model_1, H4_model_1_ni)
comp_interaction
Data: activity_data
Models:
H4_model_1_ni: geo.MEDI ~ site + activity + (1 | Id), zi=~0, disp=~1
H4_model_1: geo.MEDI ~ site * activity + (1 | Id), zi=~0, disp=~1
              Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H4_model_1_ni 16 143721 143845 -71845   143689                             
H4_model_1    48 143055 143426 -71480   142959 730.19     32  < 2.2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H4
#| fig-height: 10
#| fig-width: 8
comp_site <- 
anova(H4_model_1_ns, H4_model_1_ni)
comp_site
Data: activity_data
Models:
H4_model_1_ns: geo.MEDI ~ activity + (1 | Id), zi=~0, disp=~1
H4_model_1_ni: geo.MEDI ~ site + activity + (1 | Id), zi=~0, disp=~1
              Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H4_model_1_ns  8 143752 143813 -71868   143736                             
H4_model_1_ni 16 143721 143845 -71845   143689 46.165      8  2.211e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H4
#| fig-height: 10
#| fig-width: 8
AIC(H4_model_1_ni, H4_model_1, H4_model_0, H4_model_1_ns) |> 
  arrange(AIC) |> 
  rownames_to_column("model") |> gt()
model df AIC
H4_model_1 48 143055.2
H4_model_1_ni 16 143721.4
H4_model_1_ns 8 143751.5
H4_model_0 12 152942.1

Both AIC and ∆loglik comparisons suggest that the interaction model is the most relevant one.

In [36]:
Code
check_model(H4_model_1)
`check_outliers()` does not yet support models of class `glmmTMB`.

In [37]:
Code
tab_model(H4_model_1, CSS = css_theme("cells"))
  geo.MEDI
Predictors Estimates CI p
(Intercept) 59.43 50.94 – 69.35 <0.001
site1 2.04 1.41 – 2.96 <0.001
site2 0.79 0.56 – 1.12 0.178
site3 0.83 0.57 – 1.23 0.357
site4 0.33 0.22 – 0.50 <0.001
site5 0.46 0.30 – 0.71 <0.001
site6 1.77 1.17 – 2.70 0.007
site7 2.37 1.55 – 3.61 <0.001
site8 1.39 0.87 – 2.22 0.174
activity: road_vehicle 0.04 0.04 – 0.05 <0.001
activity: working_indoor 4.90 4.34 – 5.54 <0.001
activity: outdoor 3.25 3.01 – 3.50 <0.001
NA 8.91 7.92 – 10.03 <0.001
site1:activitysleep 0.90 0.72 – 1.11 0.316
site2:activitysleep 0.59 0.47 – 0.75 <0.001
site3:activitysleep 2.54 2.06 – 3.14 <0.001
site4:activitysleep 0.47 0.35 – 0.63 <0.001
site5:activitysleep 0.93 0.72 – 1.21 0.595
site6:activitysleep 0.47 0.37 – 0.59 <0.001
site7:activitysleep 0.69 0.54 – 0.88 0.002
site8:activitysleep 5.35 4.24 – 6.76 <0.001
site1:activityroad_vehicle 1.01 0.78 – 1.30 0.968
site2:activityroad_vehicle 0.80 0.61 – 1.06 0.118
site3:activityroad_vehicle 1.37 1.00 – 1.88 0.048
site4:activityroad_vehicle 3.10 2.16 – 4.44 <0.001
site5:activityroad_vehicle 0.76 0.51 – 1.13 0.179
site6:activityroad_vehicle 0.95 0.73 – 1.25 0.727
site7:activityroad_vehicle 0.62 0.45 – 0.84 0.002
site8:activityroad_vehicle 0.48 0.33 – 0.70 <0.001
site1:activityworking_indoor 0.65 0.53 – 0.78 <0.001
site2:activityworking_indoor 1.62 1.35 – 1.93 <0.001
site3:activityworking_indoor 1.05 0.87 – 1.26 0.616
site4:activityworking_indoor 1.88 1.47 – 2.42 <0.001
site5:activityworking_indoor 1.24 1.00 – 1.54 0.050
site6:activityworking_indoor 1.01 0.84 – 1.22 0.913
site7:activityworking_indoor 0.76 0.63 – 0.93 0.007
site8:activityworking_indoor 0.83 0.66 – 1.04 0.104
site1:activityoutdoor 0.66 0.53 – 0.83 <0.001
site2:activityoutdoor 1.25 0.97 – 1.61 0.079
site3:activityoutdoor 1.13 0.87 – 1.45 0.357
site4:activityoutdoor 1.88 1.44 – 2.46 <0.001
site5:activityoutdoor 0.49 0.36 – 0.67 <0.001
site6:activityoutdoor 1.87 1.49 – 2.34 <0.001
site7:activityoutdoor 0.94 0.75 – 1.19 0.634
site8:activityoutdoor 0.91 0.67 – 1.23 0.531
Random Effects
σ2 1.16
τ00Id 0.61
ICC 0.34
N Id 126
Observations 16801
Marginal R2 / Conditional R2 0.728 / 0.821

Basic table

In [38]:
Code
H4_tab_ran <- 
  H4_model_1 |> 
  tidy() |> 
  mutate(signif = p.value <= sig.level,
         estimate = exp(estimate) |> round(2)) |> 
  filter(effect == "ran_pars") |> 
  select(term, estimate)

H4_tab_reference <-
  H4_model_1 |>
  emmeans(~ activity, type = "response",
          at = list(activity = "home")) |> 
  as.tibble() |> 
  rename(estimate = response)
NOTE: Results may be misleading due to involvement in interactions
Code
H4_tab_act <- 
  H4_model_1 |> 
  emmeans(~ activity, type = "response") |> 
  contrast(method = "trt.vs.ctrl") |> 
  as.tibble() |> 
  rename(estimate = ratio)
NOTE: Results may be misleading due to involvement in interactions
Code
H4_tab_interaction <-
  H4_model_1 |> 
  emmeans(~ site | activity, type = "response") |> 
  contrast() |> 
  as.tibble() |> 
  rename(estimate = ratio)

H4_tab_fix <-
  bind_rows(H4_tab_reference, H4_tab_act, H4_tab_interaction) |> 
  mutate(signif = p.value <= sig.level) |> 
  select(-c(df, asymp.LCL, asymp.UCL, null, z.ratio)) |> 
  relocate(contrast, .before = 1) |> 
  mutate(site = contrast |> as.character(),
         activity = 
           activity |> as.character() |> 
           replace_when(
             str_detect(contrast, "home") ~ str_remove(contrast, " / home")
           ),
         site = replace_when(site, 
                             is.na(site) ~ "Overall",
                             str_detect(site, " effect") ~ str_remove(site, " effect"),
                             str_detect(contrast, "home") ~ "Overall"),
         .before = 1
                              
  ) |> 
  select(-contrast) |> 
  mutate(activity = replace_values(activity, from = mHLEA_labels$name, to = mHLEA_labels$value),
         activity = replace_values(activity, 
                                   "outdoor" ~ "Outdoor (free, working, on the road)")) |> 
  pivot_wider(id_cols = site, names_from = activity, values_from = estimate:signif)
  # mutate(
         # across(3:6,
                # \(x) c(x[1], x[-1]*x[1]))
         # )
In [39]:
Code
H4_r2_1 <- r2_helper(H4_model_1, activity_data, tweedie(), "geo.MEDI", "(1 | Id)")
H4_r2_0 <- r2_helper(H4_model_0, activity_data, tweedie(), "geo.MEDI", "(1 | Id)")
H4_r2_interaction<- r2_helper(H4_model_1_ni, activity_data, tweedie(), "geo.MEDI", "(1 | Id)")
H4_r2_site<- r2_helper(H4_model_1_ns, activity_data, tweedie(), "geo.MEDI", "(1 | Id)")
H4_tab_r2 <-
  tribble(
    ~type, ~r2,
    "Model", H4_r2_1$R2_conditional,
    "All fixed", H4_r2_1$R2_marginal,
    "Residual", 1-H4_r2_1$R2_conditional,
    "Activity", H4_r2_1$R2_marginal - H4_r2_0$R2_marginal,
    "Site", H4_r2_1$R2_marginal - H4_r2_site$R2_marginal,
    "Random", H4_r2_1$R2_conditional - H4_r2_1$R2_marginal,
    "Interaction", H4_r2_1$R2_marginal - H4_r2_interaction$R2_marginal,
  ) |> 
  mutate(r2= r2 |> round(2))
In [40]:
Code
H4_tab <-
H4_tab_fix |> 
  site_conv_mutate(rev = FALSE, other.levels = "Overall") |> 
  arrange(site) |> 
  gt() |> 
  cols_label_with(fn = \(x) str_remove(x, "estimate_") |> str_remove("\\(activity\\)")) |> 
  tab_spanner(
    md(
      paste0("Activity ($p = ",
             comp_confirmatory$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H4_tab_r2$r2[4], 
             "$)")
      ), 
    columns = -site) |> 
  cols_hide((contains(c("p.value", "signif", "SE_")))) |> 
  sub_missing(missing_text = "") |> 
  tab_style_body(
    style = cell_borders(weight = px(2)),
    rows = 1,
    columns = 2,
    fn = \(x) TRUE
  ) |> 
  gt::rows_add(site = NA, .after = 1) |> 
  tab_style(
    style = list(css(`padding-top` = "0px",
            `padding-bottom` = "0px"),
            cell_fill("lightgrey")
            ),
locations = cells_body(rows = 2)) |> 
  gt_multiple(
    names(H4_tab_fix) |> 
      subset(str_detect(names(H4_tab_fix), "estimate_")) |> 
      str_remove("estimate_"),
    bold_p.) |> 
  fmt(starts_with("estimate"), 
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, "×")}
      ) |> 
  fmt(columns = 2, 
      rows = site == "Overall",
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, "lx")}
      ) |> 
  gt_multiple(
    melidos_colors |> names(),
    style_rows
  ) |> 
  tab_style(
    style = cell_text(weight = "bold"),
    locations = list(
      cells_column_labels(),
      cells_column_spanners(),
      cells_body(columns = site),
      cells_body(2,1)
    )
  ) |> 
  cols_label(
    site = md(paste0("Site<br>($p = ",
             comp_site$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H4_tab_r2$r2[5], 
             "$)"))
    ) |> 
  tab_header(
    title = "Model results for Hypothesis 4",
    subtitle = H4
  ) |> 
  tab_footnote(
    "Reference value, for `Awake at home`. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.", locations = cells_body(2,1)
  ) |> 
  tab_footnote(
    md(paste0("Conditional (Model) $R^2=", 
              H4_tab_r2$r2[1],
              "$, Marginal (Fixed effects) $R^2=",
              H4_tab_r2$r2[2],
              "$, Random effect $R^2=",
              H4_tab_r2$r2[6],
              "$, Residual Variance $R^2=",
              H4_tab_r2$r2[3],
              "$; Random effect of participant: $×",
              H4_tab_ran$estimate[1],
              "$; Model based on ",
              hourly_data2 |> drop_na(geo.MEDI, lightsource_primary) |> nrow() |> vec_fmt_integer(sep_mark = "'"),
              " participant-hours."
              ))
  ) |> 
  tab_footnote(
    md(paste0("Interaction effect of activity with site is significant ($p=", 
              comp_interaction$`Pr(>Chisq)`[2] |> style_pvalue(),
              "$, $R^2 = ",
              H4_tab_r2$r2[7],
              "$)"))
  ) |> 
  tab_footnote(
    md("Values shown in **bold** are significant at $p<0.05$. False-discovery-rate adjustment for n=4 in activity and n=9 in site")
  ) |> 
  cols_width(everything() ~ px(150))
In [41]:
Code
H4_tab
Model results for Hypothesis 4
Hypothesis: There is a significant relationship between hourly self-reported activities and hourly geometric mean of melanopic EDI.
Site
(p = <0.001, R^2=0.09)
Activity (p = <0.001, R^2=0.62)
Awake at home Sleeping in bed On the road with public transport/car Working in the office/from home Outdoor (free, working, on the road)
Overall 1 59.43lx 0.04× 4.90× 3.25× 8.91×






Borås (SE) 1.77× 0.83× 1.69× 1.79× 3.32×
Delft (NL) 2.37× 1.63× 1.46× 1.81× 2.24×
Dortmund (DE) 2.04× 1.83× 2.05× 1.32× 1.36×
Tübingen (DE) 0.46× 0.43× 0.35× 0.57× 0.23×
Munich (DE) 1.39× 7.42× 0.66× 1.15× 1.26×
Madrid (ES) 0.79× 0.47× 0.63× 1.27× 0.99×
Izmir (TR) 0.83× 2.12× 1.14× 0.87× 0.94×
San José (CR) 0.85× 0.83× 1.15× 0.52× 0.61×
Kumasi (GH) 0.33× 0.15× 1.01× 0.62× 0.62×
Conditional (Model) R^2=0.82, Marginal (Fixed effects) R^2=0.73, Random effect R^2=0.09, Residual Variance R^2=0.18; Random effect of participant: ×2.18; Model based on 16’774 participant-hours.
Interaction effect of activity with site is significant (p=<0.001, R^2 = 0.04)
Values shown in bold are significant at p<0.05. False-discovery-rate adjustment for n=4 in activity and n=9 in site
1 Reference value, for `Awake at home`. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.
Code
gtsave(H4_tab, "tables/H4.png")
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpGsFdJh/file6fba53adad6d.html screenshot completed
Code
gtsave(H4_tab, "tables/H4.docx")

Visualization

In [42]:
Code
visualize H4 result
#| filename: visualize H4 result
#| fig-width: 15
#| fig-height: 12
H4_plot <- 
H4_model_1 |> 
  emmeans(~ site | activity, type = "response") |> 
  plot()

H4_plot <-
  H4_plot$data |> 
  as.tibble() |> 
  site_conv_mutate(site = pri.fac) |> 
  mutate(activity = replace_values(activity |> as.character(), 
                                   from = mHLEA_labels$name, to = mHLEA_labels$value |> str_remove(" \\(activity\\)")),
         activity = replace_values(activity, 
                                   "outdoor" ~ "Outdoor (free, working, on the road)")) |> 
    ggplot(
      aes(x = the.emmean, y = pri.fac)
    ) +
    geom_crossbar(aes(xmin = lcl, xmax = ucl, fill = pri.fac), col = NA, alpha = 0.7) +
    geom_point() +
  theme_cowplot() +
  theme(panel.grid.major = element_line(color = "grey")) +
  scale_fill_manual(values = melidos_colors) +
  facet_wrap(~activity, ncol = 1) +
  scale_x_continuous(trans = "symlog", breaks = c(0, 10^(0:5), 250, 5000)) +
  coord_cartesian(xlim = c(0, 5000)) +
  labs(y = NULL, x = "Melanopic EDI (lx), based on hourly geometric mean") +
    guides(fill = "none")

H3_plot + H4_plot

Code
#| filename: visualize H4 result
#| fig-width: 15
#| fig-height: 12
ggsave("figures/Fig5.pdf", width = 15, height = 12)
ggsave("figures/Fig5.png", width = 15, height = 12)

activity_data |> 
    filter_out(is.na(activity)) |> 
    site_conv_mutate(rev = FALSE) |> 
    ggplot(aes(x=geo.MEDI)) +
    geom_density_ridges(aes(y=activity),
                        linewidth = 1, colour = NA,
                        alpha = 0.5,
                        from = 0,
    ) +
    geom_density_ridges(aes(y=activity), fill = NA,
                        alpha = 1, linewidth = 1,
                        quantile_lines = TRUE, quantiles = 2, vline_color = "red",
                        linetype = 1,
                        from = 0,
    ) +
    # scale_fill_manual(values = melidos_colors) +
    scale_alpha_continuous(range = c(0, 1)) +
    scale_x_continuous(trans = "symlog", breaks = c(0, 10^(0:5)),
                       labels = expression(0, 1, 10, 10^2, 10^3, 10^4, 10^5)) +
    # scale_color_manual(values = melidos_colors) +
    guides(fill = "none", color = "none") +
    labs(y = NULL, x = NULL) +
    theme_ridges() +
    coord_cartesian(xlim = c(0, 10^5)) +
    theme_sub_plot(margin = margin(r = 20, t = 20)) +
    theme_sub_axis_left(text = element_text(vjust = 0))
Picking joint bandwidth of 0.152
Picking joint bandwidth of 0.152

Interpretation

  • Overall, an average mel EDI of about 60 lx (based on geometric mean) is reached with awake at home. This value somewhat varies between sites, with The Netherlands, Dortmund, and Sweden, achieving substantially higher values (factor 2.35, 2.03, and 1.76, respectively). They nevertheless do not reach recommended values of 250lx during the day. Tübingen, Germany, and Ghana reach substantially lower values (factor 0.46 and 0.32 respectively).
  • When Sleeping in bed, about 4% of the mel EDI can be achieved on average (2.3936). There is very high variance between the different sites, with factor ranging between 44% of that value (Tübingen, Germany) to 757% (The Munich).
  • When people are On the road, they achieve about 4.88 times more light compared to at home (292 lx). Dortmund, Germany, achieves still higher values (times 2.04), while Tübingen, Germany has only about one third (0.35).
  • Working indoors increases the melanopic EDI by a factor of 3.23 (193 lx). The Netherlands and Sweden reach higher levels still (1.8 and 1.79, respectively), while Tübingen, Germany, and Ghana see lower levels of increase (0.57 and 0.62, respectively).
  • Outdoors translates to about x8.82 the amount of light levels compared to at home (~528 lx). Values are similar across sites, with three notable exceptions: Sweden and The Netherlands have significantly higher values still (3.33 and 2.24, respectively), while Munich, Germany sees much lower values (0.23).
  • Overall, the effect of self-reported activity is large - 62% of variance can be explained simply through those categories. While site and interindividual differences are significant, they contribute considerably less (9% each). In summary, activity is a good, albeit worse predictor compared to self-reported light source

Co-occurance of light and activity

In [43]:
Code
activity_data |> 
  count(activity, lightsource_primary) |> 
  pivot_wider(names_from = activity, values_from = n) |> 
  drop_na() |> 
  gt()
lightsource_primary home sleep road_vehicle working_indoor outdoor
Electric light source indoors 2166 98 185 1737 126
Daylight indoors 2230 110 245 1576 185
Daylight outdoors (including shade) 106 13 318 76 997
Emissive display light 350 39 9 192 17
Darkness during sleep 145 4565 8 5 10

As can be seen by the cross-table, the two variables are strongly correlated. However, some differentiating potential lies e.g., in the home and working indoor activity, that provides a very even split between electric light indoors, daylight indoors, and emissive display light. Similarly, daylight outdoors can be split between outdoor and on the road activities.

Exploration: GAM of activity x time

In this exploratory analysis, we study the nonlinear effect of time on the type of activity, based on the final model from RQ1:

In [44]:
Code
Prepare GAM data
#| filename: Prepare GAM data
GAM_data <- 
  hourly_data2 |> 
  ungroup() |> 
  select(site, Id, Datetime, MEDI, photoperiod.state, Date, dawn, dusk, starts_with("act_")) |> 
  pivot_longer(cols = starts_with("act_"), names_to = "activity", names_prefix = "act_", values_to = "value") |> 
  add_Time_col() |> 
  mutate(activity = 
           fct(activity) |> 
           fct_collapse(outdoor = c("free_outdoor", "working_outdoor", "road_open")) |>
           fct_recode(NULL = "other") |> 
           fct_relevel("home")
         ) |> 
  drop_na(activity) |> 
  filter(value) |> 
  distinct(.keep_all = TRUE, site, Id, Datetime, activity) |> 
  ungroup() |> 
  mutate(Time = as.numeric(Time)/3600 + 0.5,
         across(c(site, Id, photoperiod.state,
                  activity, Date),
                factor),
         Id_date = interaction(Id, Date),
         lzMEDI = log_zero_inflated(MEDI),
         photoperiod = (dusk - dawn) |> as.numeric()
  ) |> 
  group_by(Id) |> 
  mutate(AR.start = ifelse(row_number() == 1, TRUE, FALSE))

H4_form_gam <- lzMEDI ~ s(Time, k = 12) + s(Time, activity, bs = "sz", k = 12) + s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, bs = "sz", k=12) + s(Time, Id, bs = "fs") + s(Id_date, bs = "re")
In [45]:
Code
Main model
#| filename: Main model
H4_model_gam <- 
  bam(H4_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10
      )

r1 <- start_value_rho(H4_model_gam, 
                      plot=TRUE)

Code
#| filename: Main model
H4_model_gam <- 
  bam(H4_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10,
      rho = r1,
      AR.start = AR.start
      )

acf_resid(H4_model_gam, split_pred = "AR.start")

Code
#| filename: Main model
H4_summary_gam <- summary(H4_model_gam)
H4_summary_gam

Family: gaussian 
Link function: identity 

Formula:
lzMEDI ~ s(Time, k = 12) + s(Time, activity, bs = "sz", k = 12) + 
    s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, 
    bs = "sz", k = 12) + s(Time, Id, bs = "fs") + s(Id_date, 
    bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  1.05530    0.04423   23.86   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                              edf   Ref.df      F p-value    
s(Time)                    10.178   10.563 50.836  <2e-16 ***
s(Time,activity)           28.451   33.250 76.984  <2e-16 ***
s(Time,site)               64.732   72.472  5.616  <2e-16 ***
s(Time,photoperiod.state)   5.683    6.587  7.843  <2e-16 ***
s(Time,Id)                567.726 1242.000  3.306  <2e-16 ***
s(Id_date)                319.115  710.000  0.929  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.826   Deviance explained = 83.6%
fREML =  16058  Scale est. = 0.39444   n = 16751
Code
#| filename: Main model
H4_model_gam |> glance()
# A tibble: 1 × 9
     df  logLik    AIC    BIC deviance df.residual  nobs adj.r.squared  npar
  <dbl>   <dbl>  <dbl>  <dbl>    <dbl>       <dbl> <int>         <dbl> <int>
1  997. -14427. 30873. 38672.    6554.      15754. 16751         0.826  2146
Code
#| filename: Main model
H4_model_gam |> appraise()

In [46]:
Code
Variance explained
#| filename: Variance explained
Xp <- predict(H4_model_gam, type = "terms")
variance <- apply(Xp, 2, var)
cor(Xp)
                               s(Time) s(Time,activity) s(Time,site)
s(Time)                    1.000000000       0.57405113  0.044399374
s(Time,activity)           0.574051127       1.00000000  0.096875679
s(Time,site)               0.044399374       0.09687568  1.000000000
s(Time,photoperiod.state)  0.129488517       0.12994002  0.131127907
s(Time,Id)                 0.013945079       0.10351105  0.004652873
s(Id_date)                -0.000198144       0.04032426 -0.003293687
                          s(Time,photoperiod.state)  s(Time,Id)    s(Id_date)
s(Time)                                0.1294885173 0.013945079 -0.0001981440
s(Time,activity)                       0.1299400236 0.103511048  0.0403242591
s(Time,site)                           0.1311279069 0.004652873 -0.0032936866
s(Time,photoperiod.state)              1.0000000000 0.041669286  0.0004991903
s(Time,Id)                             0.0416692860 1.000000000  0.1076393979
s(Id_date)                             0.0004991903 0.107639398  1.0000000000
Code
#| filename: Variance explained
H4_R2 <- (variance/sum(variance)) |> enframe(value = "R2", name = "parameter")
H4_R2
# A tibble: 6 × 2
  parameter                      R2
  <chr>                       <dbl>
1 s(Time)                   0.683  
2 s(Time,activity)          0.148  
3 s(Time,site)              0.0574 
4 s(Time,photoperiod.state) 0.00795
5 s(Time,Id)                0.0906 
6 s(Id_date)                0.0126 
In [47]:
Code
Null model
#| filename: Null model
H2_model_gam <- 
  bam(H2_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10
      )

r1 <- start_value_rho(H2_model_gam, 
                      plot=TRUE)

Code
#| filename: Null model
H2_model_gam <- 
  bam(H2_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10,
      rho = r1,
      AR.start = AR.start
      )

acf_resid(H2_model_gam, split_pred = "AR.start")

In [48]:
Code
AIC(H4_model_gam, H2_model_gam)
                    df      AIC
H4_model_gam 1009.4252 30873.45
H2_model_gam  918.5399 32776.68
In [49]:
Code
H4_model_gam |> draw(select = "s(Time,activity)")

Code
H4_model_gam |> draw(select = "s(Time)")

####Visualization

In [50]:
Code
prepare lightsource model H4 plot
#| filename: prepare lightsource model H4 plot
H4_act_data <- 
H4_model_gam |> 
  conditional_values(
    condition = list(Time = seq(0.5, 23.5, by = 1), "activity", "photoperiod.state"),
    exclude = c("s(Time,Id)", "s(Id_date)", "s(Time,site)")
    ) |> 
  mutate(across(c(.fitted, .lower_ci, .upper_ci),
                exp_zero_inflated)
  )

H4_act_data <-
GAM_data |> 
  Datetime2Time() |> 
  group_by(activity, Time) |> 
  summarize(
    .groups = "drop_last",
    dawn = dawn |> as.numeric() |> mean(na.rm = TRUE)/3600,
    dusk = dusk |> as.numeric() |> mean(na.rm = TRUE)/3600,
    n = n()
  ) |> 
  mutate(
    photoperiod.state = ifelse(between(Time, dawn, dusk), "day", "night")
  ) |> 
  select(-c(dawn, dusk)) |> 
  left_join(H4_act_data, by = c("activity", "Time", "photoperiod.state"))

H4_act_data_phot <- 
H4_act_data |> 
  number_states(photoperiod.state) |> 
  filter(photoperiod.state == "night") |> 
  group_by(activity, photoperiod.state.count) |> 
  summarize(xmin = min(Time),
            xmax = max(Time),
            .groups = "drop_last") |> 
  mutate(across(c(xmin, xmax), \(x) replace_values(x,
                                                   0.5 ~ 0,
                                                   23.5 ~ 24)))
In [51]:
Code
Visualize lightsource model H4
#| filename: Visualize lightsource model H4
#| fig-height: 5
#| fig-width: 14

act_labels <- 
  tibble(
    level = c("home", "working_indoor", "outdoor", "road_vehicle", "sleep"),
    label = c("At home", "Working indoors", "Outdoors", "In a vehicle", "Sleeping")
  )

act_colors <- 
  c(
    `Working indoors` = "#6C757D", # slate gray
    `At home` = "#C97B84",         # muted rose
    `In a vehicle` = "#8C6D31",    # brown/bronze
    Outdoors = "#4F8A5B",          # muted forest green
    Sleeping = "#1B1F3B"           # dusty indigo
  )

H4_pred <- 
H4_act_data |> 
  mutate(activity = factor(activity, levels = act_labels$level, labels = act_labels$label)) |> 
  ggplot(aes()) +
  map(c(1,10,250, 10^4), 
          \(x) geom_hline(aes(yintercept = x), 
                          col = "grey", linetype = "dashed")
      ) +
  geom_rect(data = H4_act_data_phot |> 
  mutate(activity = factor(activity, levels = act_labels$level, labels = act_labels$label)) ,
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = activity), alpha = 0.5) +
  
  geom_line(aes(x = Time, y = .fitted, 
                col = activity), linewidth = 0.1) +
  geom_line(aes(x = Time, y = .fitted), linewidth = 1) +
  scale_fill_manual(values = act_colors) +
  scale_color_manual(values = act_colors) +
  theme_cowplot() +
  scale_y_continuous(trans = "symlog", breaks = c(0,1,10,100,250, 1000)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), ylim = c(0,10^4), expand = FALSE) +
  labs(y = "Melanopic EDI (lx)", x = "Local time (hrs)", fill = "Primary lightsource", col = "Primary lightsource") +
  facet_wrap(~activity, nrow = 1) +
  guides( y = guide_axis_stack(Brown_bracket, "axis")) +
  gghighlight(
    label_key = activity,
    label_params = list(x = 12, y = 4.5, force = 0, hjust = 0.5)
  ) +
  # geom_text(aes(label = paste0("|", n, "|"), x = Time, y = 7000), size = rel(1.5)) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
  # labs(caption = Brown_caption)
H4_pred

In [52]:
Code
Visualize model terms
#| filename: Visualize model terms
Term_data <- 
  H4_act_data |> select(Time, activity) |> 
  left_join(
    H4_model_gam |> 
  smooth_estimates(select = "s(Time,activity)", n=24),
  by = c("Time", "activity")
  ) |> 
  ungroup() |> 
  complete(Time, activity) |> 
  mutate(.lower_ci = .estimate -1.96*.se,
         .upper_ci = .estimate +1.96*.se,
         .fitted = .estimate,
         signif = .lower_ci > 0 | .upper_ci < 0,
         signif_id = consecutive_id(signif),
         .by = activity) |> 
    mutate(activity = factor(activity, levels = act_labels$level, labels = act_labels$label))

P_terms <-
Term_data |> 
  ggplot(aes()) +
  map(c(log10(c(0.1, 0.2, 0.5, 2, 5, 10))),
      \(x) geom_hline(yintercept = x, linewidth = 0.5, col = "grey", linetype = 2)) +
  geom_rect(data = H4_act_data_phot |>   
              mutate(activity = factor(activity, levels = act_labels$level, labels = act_labels$label)),
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = activity), 
              alpha = 0.5) +
  geom_hline(yintercept = 0, linewidth = 1, linetype = 2) +
  geom_line(aes(x = Time, y = .fitted), 
            linewidth = 1) +
  geom_line(data = \(x) filter(x, signif),
            aes(x = Time, y = .fitted, group = signif_id), 
            col = "red", linewidth = 1) +
  scale_fill_manual(values = act_colors) +
  # scale_color_manual(values = c(melidos_colors)) +
  theme_cowplot() +
  scale_y_continuous(labels = expression(10^-1, 5^-1, 2^-1, "1  ", "2  ", "5  ", "10  "), 
                     breaks = log10(c(0.1, 0.2, 0.5, 1, 2, 5, 10)))+
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), expand = FALSE) +
  guides(fill = "none") +
  labs(y = "Factor", x = "Local time (hrs)", fill = "Primary lightsource") +
  facet_wrap(~activity, nrow = 1) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
P_terms

In [53]:
Code
n_plot_H4 <-
GAM_data |> 
  ungroup() |> 
  count(Time, activity) |> 
      mutate(activity = factor(activity, levels = act_labels$level, labels = act_labels$label)) |> 
  ggplot(aes(x=Time)) + 
  geom_col(aes(y=n, fill = activity)) + 
  facet_wrap(~activity, nrow = 1) +
  theme_cowplot() +
  scale_fill_manual(values = act_colors) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple()) +
  guides(fill = "none") +
  # scale_y_continuous(trans = "symlog", breaks = c(0, 1, 10, 100, 500)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6))
In [54]:
Code
Combination of partial plots H4
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H4

H4_pred / P_terms / n_plot_H4 + plot_layout(heights = c(2,2,1)) +
  plot_annotation(tag_levels = "A", 
                  caption = paste(Brown_caption, 
                                  "Panel B: Significant deviations from the average are shown in red", sep = "<br>"),
                  theme = 
                    theme(plot.caption = element_textbox_simple(size = rel(1.5))
                          )
                  )

Code
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H4

ggsave("figures/Fig10.pdf", height = 10, width = 15)
ggsave("figures/Fig10.png", height = 10, width = 15)

Hypothesis 5

Hypothesis 5 states:

There is a significant relationship between LEBA-factors and pre-selected personal light exposure metrics.

Type of model: Correlation matrix + LM

Base formula: \text{LEBA factor} \sim \text{Site})

False-discovery-rate correction: - 4 leba factors x 17 metrics: n = 68

Preparation

In [55]:
Code
Collect LEBA data
#| filename: Collect LEBA data
leba <- load_data("leba") |> flatten_data() |> select(site, Id, leba_f2:leba_f5)
loading modality: leba ■■■■■■■■■■■■■■■■■■■■■■■■■■■■      89% |  ETA:  0s
Code
#| filename: Collect LEBA data
leba_labels <-
  leba |> extract_labels() |> enframe() |> filter(str_detect(name, "leba_f.$"))
In [56]:
Code
#| filenmae: Collect light exposure metrics
load("data/H1_results.RData")
metrics <- metrics |> select(name, type, metric_type, data, engine, response, family, H1_1_sig, H1_lat_sig, H1_phot_sig)
metrics_leba <-
  metrics |> 
  filter_out(is.na(response)) |> 
  mutate(
    data = 
      map(data, 
          \(x) x |> 
            group_by(site, Id) |> 
            summarize(metric = mean(metric, na.rm = TRUE), .groups = "drop_last") |> 
            left_join(leba, by = c("site", "Id")) |> 
            pivot_longer(leba_f2:leba_f5, names_to = "leba")
          )
  ) |> 
  select(name, type, metric_type, data) |> 
  unnest(data)

Analaysis

In [57]:
Code
Test whether sites vary in their leba factors
#| filename: Test whether sites vary in their leba factors

# test <-
leba |> 
  mutate(Id = fct(Id),
         site = fct(site)) |> 
  summarize(across(leba_f2:leba_f5,
                    \(x) {
                      data <- pick(cur_column(), site)
                      lm(x~ site, data = data) |> list()
                         }
                    )
                ) |> 
  pivot_longer(everything()) |> 
  mutate(marginals = map(value, \(x) emmeans(x, ~ site)),
         test = map(marginals, \(x)  joint_tests(x) |> as.tibble()),
         contrasts = map(marginals, contrast)) |> 
  unnest(test) |> 
  mutate(p.value = p.adjust(p.value, "fdr", n=4),
         signif = p.value <= sig.level) |> 
  filter(signif) |> 
  select(-p.value) |> 
  mutate(contrasts = map(contrasts, as.tibble)) |> 
  unnest(contrasts) |> 
  filter(p.value <= sig.level) |> 
  select(name, contrast, estimate, SE, df, p.value, signif) |> 
  gt() |> 
  fmt_number() |> 
  fmt(p.value, fns= style_pvalue) |> 
  tab_header("Significant differences in LEBA factors dependent on sites")
Significant differences in LEBA factors dependent on sites
name contrast estimate SE df p.value signif
leba_f2 UCR effect −2.70 0.62 175.00 <0.001 TRUE
leba_f5 BAUA effect −2.24 0.74 175.00 0.025 TRUE

Only two factors are significantly different: F2 and F5. Of those, only UCR for F2 and BAUA for F5 show significant differences from the mean, both being 2-3 points lower (post-hoc test). Thus, we deem that the factors can be analysed independent of site.

In [58]:
Code
create correlation data
#| filename: create correlation data
H5_corr_matrix <-
  metrics_leba |> 
  group_by(name, leba) |> 
  drop_na(metric, value)

H5_n <- n_groups(H5_corr_matrix)

H5_corr_matrix <- 
H5_corr_matrix |> 
  summarize(
      correlation = cor(metric, value, method = "spearman"),
              p_value = cor.test(metric, value)$p.value |> 
                        p.adjust(method = "fdr", n = H5_n),
              significant = p_value <= sig.level,
              .groups = "drop") |> 
  mutate(
    name = name |> str_replace_all("_", " ") |> str_to_sentence(),
    labels = replace_values(leba, from = leba_labels$name, to = leba_labels$value) |> 
      fct() |> fct_rev()
  )

Visualization

In [59]:
Code
Visualize correlation matrix
#| filename: Visualize correlation matrix
#| fig-width: 12
#| fig-height: 5

H5_corr_matrix |> 
  ggplot(aes(x = name, y = labels, fill = abs(correlation))) +
  geom_blank() +
  geom_tile(data = H5_corr_matrix |> filter(significant), linewidth = 0.25) + 
  geom_text(
    aes(label = 
          paste(
            paste("r=",round(correlation, 2)), 
            p_value %>% style_pvalue(prepend_p = TRUE), 
            sep = "\n"),
        color = significant
        ),
    fontface = "bold",
    size = 1.8) +
  coord_fixed() +
  scale_fill_viridis_c(limits = c(0,1))+
  guides(color = "none", fill = "none")+
  scale_color_manual(values = c("red2", "white"), drop = FALSE)+
  labs(y = "LEBA factors", x = "Metrics",
       fill = "(abs) Correlation")+
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(caption = paste0("p-values are fdr-corrected for n= ", H5_n, " comparisons"))

Code
#| filename: Visualize correlation matrix
#| fig-width: 12
#| fig-height: 5

ggsave("figures/Fig6.pdf", width = 12, height = 5)
ggsave("figures/Fig6.png", width = 12, height = 5)

Interpretation

  • There is no overarching corelation between light exposure metrics and LEBA factors. We do find a significant correlation of Dose and Duration above 1000 for F2: Spending time outdoors, which makes sense from a theoretical point of view (spending more time outside would yield in a higher dose and duration in daylight levels). These correlations are small in size and only explain a small amount of variance. Dose: r=0.27, R^2=0.07, p=0.013; Duration above 250 lx melanopic EDI: r=0.29, R^2=0.08, p=0.032

Hypothesis 6

Details

Hypothesis 6 states:

There is a significant relationship between the day type, daily exercise, and sleep with the hourly geometric mean of light exposure

Type of model: GLMM

Base formula: - \text{mel EDI} \sim \text{day type}*\text{Site} + \text{daily exercise}*\text{Site} + \text{sleep duration}*\text{Site} +(1|\text{Participant})

Notes: - The confirmatory tests look at the base model compared to the Null models for each covariate - Site was sum-coded, so as to calculate an average effect of site - Sleep duration is based on the previous night

Specific formulas: - \text{Metric} \sim \text{daily exercise}*\text{Site} + \text{sleep duration}*\text{Site}+ (1|\text{Participant}) (Null model day type) - \text{Metric} \sim \text{day type}*\text{Site} + \text{sleep duration}*\text{Site}+ (1|\text{Participant}) (Null model exercise) - \text{Metric} \sim \text{day type}*\text{Site} + \text{daily exercise}*\text{Site} + (1|\text{Participant}) (Null model sleep duration)

False-discovery-rate correction: - 3 main effects: n=3; site: n=9

Preparation

In [60]:
Code
Load other data types
#| filename: Load other data types
sleep <- load_data("sleepdiaries") |> flatten_data()
Warning in flatten_data(load_data("sleepdiaries")): Not all labels across all
sites were identical. Labels used from: BAUA; sites that have other labels:
RISE
Code
#| filename: Load other data types
exercise <- load_data("exercisediary") |> flatten_data()
loading modality: exercisediary ■■■■■■■■■■■■■■                    44% |  ETA:  …
Code
#| filename: Load other data types
sleep <-
  sleep |> 
    mutate(Date = date(wake),
           daytype = daytype2 |> fct_relevel("a work day"),
           sleep_duration = sleep_duration |> as.numeric()) |> 
    select(site, Id, Date, sleep_duration, daytype)

exercise <- 
exercise |> 
  select(site, Id, Date, intensity) |> 
  mutate(intensity = fct_relabel(intensity, \(x) str_extract(x, "^(\\S+)")) |> 
           fct_relevel("None", "Light", "Moderate")) |> 
  rename(exercise = intensity)

H6_contextdata <- 
  full_join(sleep, exercise, by = c("site", "Id", "Date")) 
In [61]:
Code
Merge data
#| filename: Merge data
H6_data <-
  hourly_data2 |> 
  left_join(H6_contextdata, by = c("site", "Id", "Date")) |> 
  add_Date_col(Weekday, as.wday = TRUE) |> 
  mutate(Weekend = ifelse(as.numeric(Weekday) >5, "Weekend", "Weekday"))
In [62]:
Code
H6_contextdata <- 
  H6_contextdata |> 
  mutate(Date = factor(Date))

Testing site-specific differences

In [63]:
Code
by.day.data <- H6_contextdata |> mutate(work = daytype == "a work day")
In [64]:
Code
model_daytype <- glm(work ~ site, data = by.day.data, family = binomial())
summary(model_daytype)

Call:
glm(formula = work ~ site, family = binomial(), data = by.day.data)

Coefficients:
              Estimate Std. Error z value Pr(>|z|)    
(Intercept)  5.649e-01  1.704e-01   3.315 0.000918 ***
siteFUSPCEU -6.939e-15  2.410e-01   0.000 1.000000    
siteIZTECH   3.926e-01  2.559e-01   1.535 0.124876    
siteKNUST   -4.494e-01  2.601e-01  -1.728 0.083999 .  
siteMPI     -9.615e-03  2.300e-01  -0.042 0.966651    
siteRISE     9.003e-02  2.589e-01   0.348 0.728031    
siteTHUAS    2.496e-01  2.749e-01   0.908 0.363926    
siteTUM     -3.930e-01  2.943e-01  -1.336 0.181703    
siteUCR      1.622e-01  2.147e-01   0.755 0.450081    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 1656.8  on 1274  degrees of freedom
Residual deviance: 1641.8  on 1266  degrees of freedom
  (174 observations deleted due to missingness)
AIC: 1659.8

Number of Fisher Scoring iterations: 4
Code
drop1(model_daytype, test = "Chisq")
Single term deletions

Model:
work ~ site
       Df Deviance    AIC LRT Pr(>Chi)  
<none>      1641.8 1659.8               
site    8   1656.8 1658.8  15  0.05915 .
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Daytype is not significantly different between sites.

In [65]:
Code
model_sleep <- lmer(sleep_duration ~ site + (1|Id), data = by.day.data)
summary(model_sleep)
Linear mixed model fit by REML ['lmerMod']
Formula: sleep_duration ~ site + (1 | Id)
   Data: by.day.data

REML criterion at convergence: 4559.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-4.7160 -0.5370 -0.0079  0.5036  4.2928 

Random effects:
 Groups   Name        Variance Std.Dev.
 Id       (Intercept) 0.7645   0.8744  
 Residual             1.7061   1.3062  
Number of obs: 1275, groups:  Id, 184

Fixed effects:
            Estimate Std. Error t value
(Intercept)  7.81050    0.21525  36.286
siteFUSPCEU  0.41288    0.30201   1.367
siteIZTECH   0.03573    0.32215   0.111
siteKNUST   -0.06143    0.33723  -0.182
siteMPI     -0.57835    0.29184  -1.982
siteRISE    -0.27851    0.32554  -0.856
siteTHUAS    0.08165    0.33969   0.240
siteTUM      0.02427    0.38361   0.063
siteUCR     -0.72011    0.26897  -2.677

Correlation of Fixed Effects:
            (Intr) sFUSPC sIZTEC sKNUST sitMPI stRISE sTHUAS sitTUM
siteFUSPCEU -0.713                                                 
siteIZTECH  -0.668  0.476                                          
siteKNUST   -0.638  0.455  0.426                                   
siteMPI     -0.738  0.526  0.493  0.471                            
siteRISE    -0.661  0.471  0.442  0.422  0.488                     
siteTHUAS   -0.634  0.452  0.423  0.404  0.467  0.419              
siteTUM     -0.561  0.400  0.375  0.358  0.414  0.371  0.356       
siteUCR     -0.800  0.570  0.535  0.511  0.590  0.529  0.507  0.449
Code
drop1(model_sleep, test = "Chisq")
Single term deletions

Model:
sleep_duration ~ site + (1 | Id)
       npar    AIC    LRT  Pr(Chi)   
<none>      4571.3                   
site      8 4581.1 25.861 0.001109 **
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Sleep duration is not significantly different between sites

In [66]:
Code
#check for site differences of exercise:
library(ordinal)
Warning: package 'ordinal' was built under R version 4.5.2

Attaching package: 'ordinal'
The following object is masked from 'package:dplyr':

    slice
Code
model_exercise_site <- clmm(
  exercise ~ site + (1 | Id),
  data = H6_contextdata
)

summary(model_exercise_site)
Cumulative Link Mixed Model fitted with the Laplace approximation

formula: exercise ~ site + (1 | Id)
data:    H6_contextdata

 link  threshold nobs logLik   AIC     niter     max.grad cond.H 
 logit flexible  1174 -1450.14 2924.28 807(2425) 8.27e-04 3.6e+02

Random effects:
 Groups Name        Variance Std.Dev.
 Id     (Intercept) 1.09     1.044   
Number of groups:  Id 181 

Coefficients:
            Estimate Std. Error z value Pr(>|z|)  
siteFUSPCEU  -0.6758     0.4117  -1.642   0.1007  
siteIZTECH   -0.6153     0.4220  -1.458   0.1448  
siteKNUST    -0.2927     0.4275  -0.685   0.4935  
siteMPI       0.1043     0.3766   0.277   0.7818  
siteRISE     -0.5508     0.4270  -1.290   0.1971  
siteTHUAS     0.8981     0.4389   2.046   0.0407 *
siteTUM      -0.5883     0.4841  -1.215   0.2243  
siteUCR      -0.6376     0.3552  -1.795   0.0726 .
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Threshold coefficients:
                  Estimate Std. Error z value
None|Light         -1.1857     0.2890  -4.103
Light|Moderate      0.7035     0.2869   2.452
Moderate|Vigorous   2.1537     0.2960   7.276
(275 observations deleted due to missingness)
Code
drop1(model_exercise_site)
Single term deletions

Model:
exercise ~ site + (1 | Id)
       Df    AIC
<none>    2924.3
site    8 2929.7
In [67]:
Code
Plot overview of light against self-reported activity
#| filename: Plot overview of light against self-reported activity
#| fig-width: 6
#| fig-height: 5
H6_data |> 
  drop_na(daytype) |> 
  ggplot(aes(x=daytype, y = geo.MEDI)) +
  geom_boxplot() +
  scale_y_continuous(trans = "symlog", 
                     breaks = c(0,1,10,100,1000,10000,10^5),
                     labels = expression(0,1,10,10^2,10^3,10^4,10^5)) +
  theme_cowplot() +
  labs(y = "Melanopic EDI (lx)", x = "Day type")
Warning: Removed 595 rows containing non-finite outside the scale range
(`stat_boxplot()`).

Code
#| filename: Plot overview of light against self-reported activity
#| fig-width: 6
#| fig-height: 5
H6_data |> 
  drop_na(exercise) |> 
  ggplot(aes(x=exercise, y = geo.MEDI)) +
  geom_boxplot() +
  scale_y_continuous(trans = "symlog", 
                     breaks = c(0,1,10,100,1000,10000,10^5),
                     labels = expression(0,1,10,10^2,10^3,10^4,10^5)) +
  theme_cowplot() +
  labs(y = "Melanopic EDI (lx)", x = "Exercise")
Warning: Removed 540 rows containing non-finite outside the scale range
(`stat_boxplot()`).

Code
#| filename: Plot overview of light against self-reported activity
#| fig-width: 6
#| fig-height: 5
H6_data |> 
  drop_na(sleep) |> 
  ggplot(aes(x=sleep_duration, y = geo.MEDI)) +
  geom_point() +
  geom_smooth() +
  scale_y_continuous(trans = "symlog", 
                     breaks = c(0,1,10,100,1000,10000,10^5),
                     labels = expression(0,1,10,10^2,10^3,10^4,10^5)) +
  theme_cowplot() +
  labs(y = "Melanopic EDI (lx)", x = "Sleep duration (hrs)")
`geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
Warning: Removed 603 rows containing non-finite outside the scale range
(`stat_smooth()`).
Warning: Removed 603 rows containing missing values or values outside the scale range
(`geom_point()`).

Analysis

In [68]:
Code
Fit the model for H6
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
H6_formula_1 <- geo.MEDI ~ site*daytype + site*exercise + site*sleep_duration + (1| Id)
H6_formula_0_daytype <- geo.MEDI ~ site*exercise + site*sleep_duration + (1| Id)
H6_formula_0_exercise <- geo.MEDI ~ site*daytype + site*sleep_duration + (1|Id)
H6_formula_0_sleep <- geo.MEDI ~ site*daytype + site*exercise + (1|Id)
H6_formula_0_site <- geo.MEDI ~ daytype + exercise + sleep_duration + (1|Id)

#confirmatory
H6_model_1 <- 
  glmmTMB(H6_formula_1, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_daytype <- 
  glmmTMB(H6_formula_0_daytype, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_exercise <- 
  glmmTMB(H6_formula_0_exercise, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_sleep <- 
  glmmTMB(H6_formula_0_sleep, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration),
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_site <- 
  glmmTMB(H6_formula_0_site, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration),
          REML = FALSE, 
          family = tweedie(),
          # contrasts = list(site = "contr.sum")
          )

comp_daytype <- 
anova(H6_model_0_daytype, H6_model_1)
comp_daytype
Data: drop_na(H6_data, geo.MEDI, daytype, exercise, sleep_duration)
Models:
H6_model_0_daytype: geo.MEDI ~ site * exercise + site * sleep_duration + (1 | Id), zi=~0, disp=~1
H6_model_1: geo.MEDI ~ site * daytype + site * exercise + site * sleep_duration + , zi=~0, disp=~1
H6_model_1:     (1 | Id), zi=~0, disp=~1
                   Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)
H6_model_0_daytype 48 148397 148767 -74150   148301                         
H6_model_1         57 148316 148755 -74101   148202 99.081      9  < 2.2e-16
                      
H6_model_0_daytype    
H6_model_1         ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
comp_exercise <- 
anova(H6_model_0_exercise, H6_model_1)
comp_exercise
Data: drop_na(H6_data, geo.MEDI, daytype, exercise, sleep_duration)
Models:
H6_model_0_exercise: geo.MEDI ~ site * daytype + site * sleep_duration + (1 | Id), zi=~0, disp=~1
H6_model_1: geo.MEDI ~ site * daytype + site * exercise + site * sleep_duration + , zi=~0, disp=~1
H6_model_1:     (1 | Id), zi=~0, disp=~1
                    Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)
H6_model_0_exercise 30 148499 148730 -74219   148439                         
H6_model_1          57 148316 148755 -74101   148202 236.83     27  < 2.2e-16
                       
H6_model_0_exercise    
H6_model_1          ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
comp_sleep <- 
anova(H6_model_0_sleep, H6_model_1)
comp_sleep
Data: drop_na(H6_data, geo.MEDI, daytype, exercise, sleep_duration)
Models:
H6_model_0_sleep: geo.MEDI ~ site * daytype + site * exercise + (1 | Id), zi=~0, disp=~1
H6_model_1: geo.MEDI ~ site * daytype + site * exercise + site * sleep_duration + , zi=~0, disp=~1
H6_model_1:     (1 | Id), zi=~0, disp=~1
                 Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H6_model_0_sleep 48 148340 148710 -74122   148244                             
H6_model_1       57 148316 148755 -74101   148202 42.297      9    2.9e-06 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
AIC(H6_model_0_sleep, H6_model_0_exercise, H6_model_0_daytype, H6_model_1) |> 
  arrange(AIC) |> 
  rownames_to_column("model") |> gt()
model df AIC
H6_model_1 57 148315.7
H6_model_0_sleep 48 148340.0
H6_model_0_daytype 48 148396.8
H6_model_0_exercise 30 148498.5

Both AIC and ∆loglik comparisons suggest that the full interaction model is the most relevant one.

In [69]:
Code
#| fig-width: 12
#| fig-height: 12
check_model(H6_model_1)
`check_outliers()` does not yet support models of class `glmmTMB`.

In [70]:
Code
tab_model(H6_model_1, CSS = css_theme("cells"))
  geo.MEDI
Predictors Estimates CI p
(Intercept) 144.12 105.16 – 197.52 <0.001
site1 1.07 0.46 – 2.45 0.878
site2 2.49 1.09 – 5.71 0.030
site3 1.19 0.62 – 2.28 0.604
site4 0.28 0.12 – 0.67 0.004
site5 0.44 0.24 – 0.81 0.009
site6 1.46 0.60 – 3.52 0.404
site7 0.38 0.13 – 1.13 0.082
site8 4.05 1.49 – 11.03 0.006
My day involved the
following type of
physical activity: None
0.92 0.84 – 1.01 0.073
My day involved the
following type of
physical activity:
Moderate
1.85 1.66 – 2.08 <0.001
My day involved the
following type of
physical activity:
Vigorous
1.74 1.47 – 2.07 <0.001
NA 1.50 1.25 – 1.79 <0.001
sleep_duration 0.92 0.89 – 0.95 <0.001
site1:daytypea free day 1.96 1.56 – 2.46 <0.001
site2:daytypea free day 0.62 0.49 – 0.80 <0.001
site3:daytypea free day 0.76 0.61 – 0.95 0.015
site4:daytypea free day 0.67 0.53 – 0.87 0.002
site5:daytypea free day 0.93 0.78 – 1.10 0.398
site6:daytypea free day 1.38 1.08 – 1.76 0.009
site7:daytypea free day 1.97 1.56 – 2.49 <0.001
site8:daytypea free day 1.05 0.83 – 1.33 0.687
site1:exerciseLight 0.65 0.49 – 0.85 0.001
site2:exerciseLight 0.60 0.46 – 0.79 <0.001
site3:exerciseLight 0.61 0.48 – 0.79 <0.001
site4:exerciseLight 3.10 2.16 – 4.46 <0.001
site5:exerciseLight 0.81 0.63 – 1.03 0.086
site6:exerciseLight 1.11 0.86 – 1.44 0.423
site7:exerciseLight 1.95 1.30 – 2.91 0.001
site8:exerciseLight 0.59 0.44 – 0.81 0.001
site1:exerciseModerate 0.77 0.56 – 1.06 0.104
site2:exerciseModerate 1.03 0.72 – 1.47 0.870
site3:exerciseModerate 0.77 0.50 – 1.21 0.260
site4:exerciseModerate 8.47 5.22 – 13.76 <0.001
site5:exerciseModerate 0.79 0.58 – 1.07 0.130
site6:exerciseModerate 0.78 0.53 – 1.14 0.193
site7:exerciseModerate 1.40 0.89 – 2.18 0.143
site8:exerciseModerate 0.61 0.36 – 1.01 0.056
site1:exerciseVigorous 0.71 0.48 – 1.05 0.085
site2:exerciseVigorous 0.53 0.34 – 0.83 0.006
site3:exerciseVigorous 0.70 0.48 – 1.04 0.075
site4:exerciseVigorous 14.98 7.28 – 30.85 <0.001
site5:exerciseVigorous 1.13 0.81 – 1.57 0.462
site6:exerciseVigorous 0.72 0.46 – 1.11 0.139
site7:exerciseVigorous 0.72 0.47 – 1.09 0.117
site8:exerciseVigorous 0.59 0.35 – 1.01 0.055
site1:sleep_duration 1.04 0.95 – 1.15 0.390
site2:sleep_duration 0.93 0.85 – 1.02 0.120
site3:sleep_duration 1.00 0.94 – 1.07 0.962
site4:sleep_duration 0.94 0.85 – 1.04 0.254
site5:sleep_duration 1.10 1.03 – 1.17 0.003
site6:sleep_duration 1.03 0.93 – 1.14 0.587
site7:sleep_duration 1.15 1.03 – 1.28 0.015
site8:sleep_duration 0.88 0.79 – 0.98 0.016
Random Effects
σ2 1.59
τ00Id 0.74
ICC 0.32
N Id 137
Observations 16406
Marginal R2 / Conditional R2 0.150 / 0.420
In [71]:
Code
H6_r2_1 <- r2_helper(H6_model_1, H6_data, tweedie(), "geo.MEDI", "(1 | Id)")
H6_r2_0_daytime <- r2_helper(H6_model_0_daytype, H6_data, tweedie(), "geo.MEDI", "(1 | Id)")
H6_r2_0_exercise <- r2_helper(H6_model_0_exercise, H6_data, tweedie(), "geo.MEDI", "(1 | Id)")
H6_r2_0_sleep <- r2_helper(H6_model_0_sleep, H6_data, tweedie(), "geo.MEDI", "(1 | Id)")
H6_r2_0_site <- r2_helper(H6_model_0_site, H6_data, tweedie(), "geo.MEDI", "(1 | Id)")
H6_tab_r2 <-
  tribble(
    ~type, ~r2,
    "Model", H6_r2_1$R2_conditional,
    "All fixed", H6_r2_1$R2_marginal,
    "Residual", 1-H6_r2_1$R2_conditional,
    "Daytype", H6_r2_1$R2_marginal - H6_r2_0_daytime$R2_marginal,
    "Exercise", H6_r2_1$R2_marginal - H6_r2_0_exercise$R2_marginal,
    "Sleep", H6_r2_1$R2_marginal - H6_r2_0_sleep$R2_marginal,
    "Random", H6_r2_1$R2_conditional - H6_r2_1$R2_marginal,
    "Site", H6_r2_1$R2_marginal - H6_r2_0_site$R2_marginal,
  ) |> 
  mutate(r2= r2 |> round(2))

Basic table

In [72]:
Code
H6_tab_ran <- 
  H6_model_1 |> 
  tidy() |> 
  mutate(signif = p.value <= sig.level,
         estimate = exp(estimate) |> round(2)) |> 
  filter(effect == "ran_pars") |> 
  select(term, estimate)

H6_tab_reference <-
  H6_model_1 |>
  emmeans(~ daytype + exercise + sleep_duration, type = "response",
          at = list(daytype = "a work day",
                    exercise = "None"
                    )) |> 
  as.tibble() |> 
  rename(estimate = response)

H6_tab_daytype <- 
  H6_model_1 |> 
  emmeans(~ daytype, type = "response") |> 
  contrast(method = "trt.vs.ctrl") |> 
  as.tibble() |> 
  rename(estimate = ratio)
NOTE: Results may be misleading due to involvement in interactions
Code
H6_tab_exercise <- 
  H6_model_1 |> 
  emmeans(~ exercise, type = "response") |> 
  contrast(method = "trt.vs.ctrl") |> 
  as.tibble() |> 
  rename(estimate = ratio)
NOTE: Results may be misleading due to involvement in interactions
Code
H6_tab_sleep <-
  H6_model_1 |> 
  emtrends(~ sleep_duration, var = "sleep_duration", type = "response") |> 
  as.tibble() |> 
  rename(estimate = sleep_duration.trend) |> 
  mutate(p.value = comp_sleep$`Pr(>Chisq)`[2],
         estimate = exp(estimate))
NOTE: Results may be misleading due to involvement in interactions
Code
H6_tab_daytype_interaction <-
  H6_model_1 |> 
  emmeans(~ site | daytype, type = "response") |> 
  contrast() |> 
  as.tibble() |> 
  rename(estimate = ratio)

H6_tab_exercise_interaction <-
  H6_model_1 |> 
  emmeans(~ site | exercise, type = "response") |> 
  contrast() |> 
  as.tibble() |> 
  rename(estimate = ratio)

H6_tab_sleep_interaction <-
  H6_model_1 |> 
  emtrends(~ site | sleep_duration, var = "sleep_duration", type = "response") |> 
  contrast() |> 
  as.tibble() |> 
  mutate(estimate = exp(estimate))

H6_tab_fix <-
  bind_rows(H6_tab_reference, 
            H6_tab_daytype, 
            H6_tab_exercise, 
            H6_tab_sleep, 
            H6_tab_daytype_interaction,
            H6_tab_exercise_interaction,
            H6_tab_sleep_interaction
            ) |> 
  mutate(signif = p.value <= sig.level) |> 
  select(-c(df, asymp.LCL, asymp.UCL, null, z.ratio)) |> 
  relocate(contrast, .before = 1) |> 
  mutate(site = contrast |> as.character(),
         daytype = 
           daytype |> as.character() |> 
           replace_when(
             str_detect(contrast, "a work day") ~ str_remove(contrast, " / a work day")
           ),
         exercise = 
           exercise |> as.character() |> 
           replace_when(
             str_detect(contrast, "None") ~ str_remove(contrast, " / None")
           ),
         site = replace_when(site, 
                             is.na(site) ~ "Overall",
                             str_detect(site, " effect") ~ str_remove(site, " effect"),
                             str_detect(contrast, "a work day") ~ "Overall",
                             str_detect(contrast, "None") ~ "Overall"),
         sleep = ifelse(is.na(sleep_duration), NA, "sleep"),
         .before = 1
                              
  ) |> 
  select(-contrast, -sleep_duration) |> 
  pivot_longer(sleep:exercise, names_to = "covariate") |> 
    drop_na(value) |> 
    filter_out(site == "Overall", value == "sleep", is.na(p.value)) |> 
  pivot_wider(id_cols = c(site), names_from = c(covariate, value), values_from = estimate:signif)
In [73]:
Code
H6_tab <-
H6_tab_fix |> 
  site_conv_mutate(rev = FALSE, other.levels = "Overall") |> 
  arrange(site) |> 
  gt() |> 
  cols_label_with(
    fn = 
      \(x) str_remove(x, "estimate_") |> 
            str_remove("daytype_") |> 
            str_remove("sleep_") |> 
            str_remove("exercise_") |> 
            str_to_sentence()
    )|> 
  tab_spanner(
    md(
      paste0("Day type ($p = ",
             comp_daytype$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H6_tab_r2$r2[4], 
             "$)")
      ), 
    columns = contains("daytype_")) |> 
  tab_spanner(
    md(
      paste0("Exercise ($p = ",
             comp_exercise$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H6_tab_r2$r2[5], 
             "$)")
      ), 
    columns = contains("exercise_"))|> 
      cols_hide((contains(c("p.value_", "signif_")))) |>
      cols_hide((starts_with(c("SE_")))) |>
  sub_missing(missing_text = "") |>
  gt::rows_add(site = NA, .after = 1) |> 
  tab_style(
    style = list(css(`padding-top` = "0px",
            `padding-bottom` = "0px"),
            cell_fill("lightgrey")
            ),
locations = cells_body(rows = 2)) |> 
  gt_multiple(
    names(H6_tab_fix) |> 
      subset(str_detect(names(H6_tab_fix), "estimate_")) |> 
      str_remove("estimate_"),
    bold_p.) |> 
  tab_style_body(
    style = cell_borders(weight = px(2)),
    rows = 1,
    columns = 2:3,
    fn = \(x) TRUE
  ) |> 
    fmt(starts_with("estimate"), 
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, "×")}
      ) |> 
  fmt(columns = 2:3, 
      rows = site == "Overall",
      fns = \(x) 
      {x <- x |> gt::vec_fmt_number()
      str_c(x, " lx")}
      ) |> 
  gt_multiple(
    melidos_colors |> names(),
    style_rows
  ) |> 
  tab_style(
    style = cell_text(weight = "bold"),
    locations = list(
      cells_column_labels(),
      cells_column_spanners(),
      cells_body(columns = site),
      cells_body(2:3,1)
    )
  ) |> 
  cols_label(
    site = md(paste0("Site<br>($p = ",
             comp_site$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H6_tab_r2$r2[8], 
             "$)")),
    estimate_sleep_sleep =  md(
      paste0("Sleep<br>($p = ",
             comp_sleep$`Pr(>Chisq)`[2] |> style_pvalue(),
             "$, $R^2=", H6_tab_r2$r2[6], 
             "$)")
      )
    ) |> 
  tab_header(
    title = "Model results for Hypothesis 6",
    subtitle = H6
  ) |> 
  tab_footnote(
    md("Values shown in **bold** are significant at $p<0.05$. False-discovery-rate adjustment for n=3 overall, n=3 in exercise, and n=9 in site")
  ) |> 
  cols_width(
             site ~ px(150),
             estimate_sleep_sleep ~ px(150),
             everything() ~ px(100)) |> 
    tab_footnote(
    "Reference value, for `A work day` with an average of `7.9 hours of sleep`, and exercise `None`. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.", locations = cells_body(2:3,1)
  ) |> 
  tab_footnote(
    md(paste0("Conditional (Model) $R^2=", 
              H6_tab_r2$r2[1],
              "$, Marginal (Fixed effects) $R^2=",
              H6_tab_r2$r2[2],
              "$, Random effect $R^2=",
              H6_tab_r2$r2[7],
              "$, Residual Variance $R^2=",
              H6_tab_r2$r2[3],
              "$; Random effect of participant: $×",
              H6_tab_ran$estimate[1],
              "$; Model based on ",
              H6_data |> drop_na(geo.MEDI, exercise, daytype, sleep_duration) |> nrow() |> vec_fmt_integer(sep_mark = "'"),
              " participant-hours."
              ))
  ) 
In [74]:
Code
H6_tab
Model results for Hypothesis 6
Hypothesis: There is a significant relationship between the day type, daily exercise, and sleep with the hourly geometric mean of light exposure
Site
(p = <0.001, R^2=0.13)
Day type (p = <0.001, R^2=0.01)
Exercise (p = <0.001, R^2=0.05)
Sleep
(p = <0.001, R^2=0.02)
A work day A free day None Light Moderate Vigorous
Overall 1 75.79 lx 0.92× 1 75.79 lx 1.85× 1.74× 1.50× 0.92×








Borås (SE) 1.61× 2.23× 2.14× 2.38× 1.66× 1.53× 1.03×
Delft (NL) 1.33× 2.62× 1.58× 3.07× 2.20× 1.13× 1.15×
Dortmund (DE) 1.15× 2.24× 2.08× 1.34× 1.60× 1.47× 1.04×
Tübingen (DE) 0.85× 0.79× 0.89× 0.72× 0.70× 1.01× 1.10×
Munich (DE) 0.99× 1.04× 1.49× 0.88× 0.90× 0.89× 0.88×
Madrid (ES) 1.08× 0.67× 1.12× 0.68× 1.16× 0.60× 0.93×
Izmir (TR) 0.91× 0.70× 1.05× 0.65× 0.81× 0.74× 1.00×
San José (CR) 0.62× 0.37× 0.62× 0.81× 0.23× 0.45× 0.96×
Kumasi (GH) 0.79× 0.54× 0.15× 0.45× 1.24× 2.19× 0.94×
Values shown in bold are significant at p<0.05. False-discovery-rate adjustment for n=3 overall, n=3 in exercise, and n=9 in site
Conditional (Model) R^2=0.42, Marginal (Fixed effects) R^2=0.15, Random effect R^2=0.27, Residual Variance R^2=0.58; Random effect of participant: ×2.36; Model based on 16’406 participant-hours.
1 Reference value, for `A work day` with an average of `7.9 hours of sleep`, and exercise `None`. All other values have to be multiplied by the reference value, their respective `Overall` multiplier, and optionally the site interaction to get the estimated mel EDI for a given combination.
Code
gtsave(H6_tab, "tables/H6.png", vwidth = 1200)
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpGsFdJh/file6fba4671c824.html screenshot completed
Code
gtsave(H6_tab, "tables/H6.docx", vwidth = 1200)

Visualization

In [75]:
Code
H6_plot <- 
H6_model_1 |> 
  emmeans(~ site | daytype, type = "response") |> 
  plot()

H6_plot <-
  H6_plot$data |> 
  as.tibble() |> 
  site_conv_mutate(site = pri.fac) |> 
    ggplot(
      aes(x = the.emmean, y = pri.fac)
    ) +
    geom_crossbar(aes(xmin = lcl, xmax = ucl, fill = pri.fac), col = NA, alpha = 0.7) +
    geom_point() +
  theme_cowplot() +
  theme(panel.grid.major = element_line(color = "grey")) +
  scale_fill_manual(values = melidos_colors) +
  facet_wrap(~daytype, ncol = 1) +
  scale_x_continuous(breaks = seq(0, 600, by = 50)) +
  coord_cartesian(xlim = c(0, 500), ylim = c(0, 10), expand = FALSE, clip = "off") +
  labs(y = NULL, x = "Melanopic EDI (lx), based on hourly geometric mean") +
    guides(fill = "none") +
  theme(plot.margin = margin(10, 20, 10, 10))

H6_plot

Code
ggsave("figures/Fig_H6.pdf", width = 7, height = 7)
ggsave("figures/Fig_H6.png", width = 7, height = 7)
In [76]:
Code
H6_model_1 |> 
  emmeans(~ site | exercise, type = "response") |> 
  plot()

In [77]:
Code
H6_model_1 |> 
  emtrends(~ site | sleep_duration, var = "sleep_duration", type = "response",
           at = list(sleep_duration = 8)) |> 
  plot() +
  labs(caption = "Trend is on a log-scale")

Interpretation

  • While there is no overall difference between work days and free days, Sweden, The Netherlands, and Dortmund, Germany show substantially higher melanopic EDI on free days (factors of 2.22, 2.60, and 2.23, respectively), while Costa Rica and Ghana have substantiall lower values (0.39 and 0.53, respectively).
  • For exercise, the overall effect is a substantially higher melanopic EDI as long as there is some amount of exercise (>1.50). Sweden and The Netherlands show partially higher values for activity levels below Vigorous (>2.14), while Ghana, have very low values for No or light exercise (0.15 and 0.45, respectively), and Costarica has lower values at moderate exercise (0.23)
  • There is an overall trend of -8% lower melanopic EDI per additional hour of sleep. Only Tübingen, Germany, shows a flat curve (1.1 x 0.92 = 1.01)
  • While all of the effects are significant (at least with their interaction of site), none are very relevant, with unique contributions to R^2 values between 0.01 and 0.05

Weekend instead of free day

This is a quick test to check whether weekend is a better predictor for melanopic EDI.

In [78]:
Code
H6_formula_weekend <- geo.MEDI ~ site*Weekend + site*exercise + site*sleep_duration + (1| Id)

H6_model_1_weekend <- 
  glmmTMB(H6_formula_weekend, 
          data = H6_data |> drop_na(geo.MEDI, daytype, exercise, sleep_duration), 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )

AIC(H6_model_1_weekend, H6_model_1)
                   df      AIC
H6_model_1_weekend 57 148348.9
H6_model_1         57 148315.7
In [79]:
Code
H6_model_1_weekend |> 
  emmeans(~ site | Weekend, type = "response") |> 
  plot()

The quick test shows that daytype is the better predictor compared to weekend (∆AIC), and that the results are broadly the same.

Exploration: GAM of activity x time

In this exploratory analysis, we study the nonlinear effect of time on the type of activity, based on the final model from RQ1:

In [80]:
Code
Prepare GAM data
#| filename: Prepare GAM data
GAM_data <- 
  H6_data |> 
  ungroup() |> 
  select(site, Id, Datetime, MEDI, photoperiod.state, Date, dawn, dusk, exercise, daytype) |> 
  add_Time_col() |> 
  drop_na(exercise, daytype) |> 
  ungroup() |> 
  mutate(Time = as.numeric(Time)/3600 + 0.5,
         across(c(site, Id, photoperiod.state,
                  Date),
                factor),
         Id_date = interaction(Id, Date),
         lzMEDI = log_zero_inflated(MEDI),
         photoperiod = (dusk - dawn) |> as.numeric()
  ) |> 
  group_by(Id) |> 
  mutate(AR.start = ifelse(row_number() == 1, TRUE, FALSE))

H6_form_gam <- lzMEDI ~ s(Time, k = 12) + s(Time, daytype, bs = "sz", k = 12) + s(Time, exercise, bs = "sz", k = 12) + s(Time, site, bs = "sz", k = 12) + s(Time, photoperiod.state, bs = "sz", k=12) + s(Time, Id, bs = "fs") + s(Id_date, bs = "re")
In [81]:
Code
Main model
#| filename: Main model
H6_model_gam <- 
  bam(H6_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10
      )

r1 <- start_value_rho(H6_model_gam, 
                      plot=TRUE)

Code
#| filename: Main model
H6_model_gam <- 
  bam(H6_form_gam, 
      data = GAM_data, 
      discrete = TRUE,
      method = "fREML",
      nthreads = 10,
      rho = r1,
      AR.start = AR.start
      )

acf_resid(H6_model_gam, split_pred = "AR.start")

Code
#| filename: Main model
H6_summary_gam <- summary(H6_model_gam)
H6_summary_gam

Family: gaussian 
Link function: identity 

Formula:
lzMEDI ~ s(Time, k = 12) + s(Time, daytype, bs = "sz", k = 12) + 
    s(Time, exercise, bs = "sz", k = 12) + s(Time, site, bs = "sz", 
    k = 12) + s(Time, photoperiod.state, bs = "sz", k = 12) + 
    s(Time, Id, bs = "fs") + s(Id_date, bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.81001    0.05415   14.96   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                              edf   Ref.df       F p-value    
s(Time)                    10.491   10.692 214.342  <2e-16 ***
s(Time,daytype)            11.447   11.931  34.612  <2e-16 ***
s(Time,exercise)           10.170   11.751   4.484  <2e-16 ***
s(Time,site)               61.649   68.754   5.659  <2e-16 ***
s(Time,photoperiod.state)   6.824    7.835   6.931  <2e-16 ***
s(Time,Id)                642.083 1352.000   3.049  <2e-16 ***
s(Id_date)                227.536  693.000   0.532  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.804   Deviance explained = 81.6%
fREML =  16614  Scale est. = 0.44162   n = 16406
Code
#| filename: Main model
H6_model_gam |> glance()
# A tibble: 1 × 9
     df  logLik    AIC    BIC deviance df.residual  nobs adj.r.squared  npar
  <dbl>   <dbl>  <dbl>  <dbl>    <dbl>       <dbl> <int>         <dbl> <int>
1  971. -14999. 31964. 39537.    7103.      15435. 16406         0.804  2244
Code
#| filename: Main model
H6_model_gam |> appraise()

In [82]:
Code
Variance explained
#| filename: Variance explained
Xp <- predict(H6_model_gam, type = "terms")
variance <- apply(Xp, 2, var)
cor(Xp)
                               s(Time) s(Time,daytype) s(Time,exercise)
s(Time)                    1.000000000    0.0904033963     -0.017535485
s(Time,daytype)            0.090403396    1.0000000000      0.008685571
s(Time,exercise)          -0.017535485    0.0086855713      1.000000000
s(Time,site)               0.016824371   -0.0017351900     -0.057791883
s(Time,photoperiod.state)  0.325413181    0.0252820880      0.001718186
s(Time,Id)                 0.013024154    0.0306209865      0.025624987
s(Id_date)                 0.001073479   -0.0001628992     -0.001435591
                          s(Time,site) s(Time,photoperiod.state) s(Time,Id)
s(Time)                    0.016824371               0.325413181 0.01302415
s(Time,daytype)           -0.001735190               0.025282088 0.03062099
s(Time,exercise)          -0.057791883               0.001718186 0.02562499
s(Time,site)               1.000000000               0.134263142 0.01065395
s(Time,photoperiod.state)  0.134263142               1.000000000 0.04548833
s(Time,Id)                 0.010653947               0.045488331 1.00000000
s(Id_date)                -0.001070312               0.009319037 0.10626352
                             s(Id_date)
s(Time)                    0.0010734792
s(Time,daytype)           -0.0001628992
s(Time,exercise)          -0.0014355907
s(Time,site)              -0.0010703120
s(Time,photoperiod.state)  0.0093190374
s(Time,Id)                 0.1062635228
s(Id_date)                 1.0000000000
Code
#| filename: Variance explained
H6_R2 <- (variance/sum(variance)) |> enframe(value = "R2", name = "parameter")
H6_R2
# A tibble: 7 × 2
  parameter                      R2
  <chr>                       <dbl>
1 s(Time)                   0.834  
2 s(Time,daytype)           0.0147 
3 s(Time,exercise)          0.00282
4 s(Time,site)              0.0450 
5 s(Time,photoperiod.state) 0.00805
6 s(Time,Id)                0.0905 
7 s(Id_date)                0.00459
In [83]:
Code
H6_model_gam |> draw(select = "s(Time,daytype)")

Code
H6_model_gam |> draw(select = "s(Time)")

Code
H6_model_gam |> draw(select = "s(Time,exercise)")

####Visualization for exercise

In [84]:
Code
prepare lightsource model H6 plot
#| filename: prepare lightsource model H6 plot
H6_ex_data <- 
H6_model_gam |> 
  conditional_values(
    condition = list(Time = seq(0.5, 23.5, by = 1), "exercise", "photoperiod.state"),
    exclude = c("s(Time,Id)", "s(Id_date)", "s(Time,site)", "s(Time,daytype)")
    ) |> 
  mutate(across(c(.fitted, .lower_ci, .upper_ci),
                exp_zero_inflated)
  )

H6_ex_data <-
GAM_data |> 
  Datetime2Time() |> 
  group_by(exercise, Time) |> 
  summarize(
    .groups = "drop_last",
    dawn = dawn |> as.numeric() |> mean(na.rm = TRUE)/3600,
    dusk = dusk |> as.numeric() |> mean(na.rm = TRUE)/3600,
    n = n()
  ) |> 
  mutate(
    photoperiod.state = ifelse(between(Time, dawn, dusk), "day", "night")
  ) |> 
  select(-c(dawn, dusk)) |> 
  left_join(H6_ex_data, by = c("exercise", "Time", "photoperiod.state"))

H6_ex_data_phot <- 
H6_ex_data |> 
  number_states(photoperiod.state) |> 
  filter(photoperiod.state == "night") |> 
  group_by(exercise, photoperiod.state.count) |> 
  summarize(xmin = min(Time),
            xmax = max(Time),
            .groups = "drop_last") |> 
  mutate(across(c(xmin, xmax), \(x) replace_values(x,
                                                   0.5 ~ 0,
                                                   23.5 ~ 24)))
In [85]:
Code
Visualize lightsource model H6
#| filename: Visualize lightsource model H6
#| fig-height: 5
#| fig-width: 14

H6_pred <- 
H6_ex_data |> 
  ggplot(aes()) +
  map(c(1,10,250, 10^4), 
          \(x) geom_hline(aes(yintercept = x), 
                          col = "grey", linetype = "dashed")
      ) +
  geom_rect(data = H6_ex_data_phot,
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = exercise), alpha = 0.5) +
  
  geom_line(aes(x = Time, y = .fitted, 
                col = exercise), linewidth = 0.1) +
  geom_line(aes(x = Time, y = .fitted), linewidth = 1) +
  # scale_fill_manual(values = act_colors) +
  # scale_color_manual(values = act_colors) +
  theme_cowplot() +
  scale_y_continuous(trans = "symlog", breaks = c(0,1,10,100,250, 1000)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), ylim = c(0,10^4), expand = FALSE) +
  labs(y = "Melanopic EDI (lx)", x = "Local time (hrs)", fill = "Exercise", col = "Exercise") +
  facet_wrap(~exercise, nrow = 1) +
  guides( y = guide_axis_stack(Brown_bracket, "axis")) +
  gghighlight(
    label_key = exercise,
    label_params = list(x = 12, y = 4.5, force = 0, hjust = 0.5)
  ) +
  # geom_text(aes(label = paste0("|", n, "|"), x = Time, y = 7000), size = rel(1.5)) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
  # labs(caption = Brown_caption)
H6_pred

In [86]:
Code
Visualize model terms
#| filename: Visualize model terms
Term_data <- 
  H6_ex_data |> select(Time, exercise) |> 
  left_join(
    H6_model_gam |> 
  smooth_estimates(select = "s(Time,exercise)", n=24),
  by = c("Time", "exercise")
  ) |> 
  ungroup() |> 
  complete(Time, exercise) |> 
  mutate(.lower_ci = .estimate -1.96*.se,
         .upper_ci = .estimate +1.96*.se,
         .fitted = .estimate,
         signif = .lower_ci > 0 | .upper_ci < 0,
         signif_id = consecutive_id(signif),
         .by = exercise)

P_terms <-
Term_data |> 
  ggplot(aes()) +
  map(c(log10(c(0.1, 0.2, 0.5, 2, 5, 10))),
      \(x) geom_hline(yintercept = x, linewidth = 0.5, col = "grey", linetype = 2)) +
  geom_rect(data = H6_ex_data_phot,
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = exercise), 
              alpha = 0.5) +
  geom_hline(yintercept = 0, linewidth = 1, linetype = 2) +
  geom_line(aes(x = Time, y = .fitted), 
            linewidth = 1) +
  geom_line(data = \(x) filter(x, signif),
            aes(x = Time, y = .fitted, group = signif_id), 
            col = "red", linewidth = 1) +
  # scale_fill_manual(values = act_colors) +
  # scale_color_manual(values = c(melidos_colors)) +
  theme_cowplot() +
  scale_y_continuous(labels = expression(10^-1, 5^-1, 2^-1, "1  ", "2  ", "5  ", "10  "), 
                     breaks = log10(c(0.1, 0.2, 0.5, 1, 2, 5, 10)))+
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), expand = FALSE) +
  guides(fill = "none") +
  labs(y = "Factor", x = "Local time (hrs)", fill = "Exercise") +
  facet_wrap(~exercise, nrow = 1) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
P_terms

In [87]:
Code
n_plot_H6 <-
GAM_data |> 
  ungroup() |> 
  count(Time, exercise) |>  
  ggplot(aes(x=Time)) + 
  geom_col(aes(y=n, fill = exercise)) + 
  facet_wrap(~exercise, nrow = 1) +
  theme_cowplot() +
  scale_fill_manual(values = act_colors) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple()) +
  guides(fill = "none") +
  # scale_y_continuous(trans = "symlog", breaks = c(0, 1, 10, 100, 500)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6))
In [88]:
Code
Combination of partial plots H6
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H6

H6_pred / P_terms / n_plot_H6 + plot_layout(heights = c(2,2,1)) +
  plot_annotation(tag_levels = "A", 
                  caption = paste(Brown_caption, 
                                  "Panel B: Significant deviations from the average are shown in red", sep = "<br>"),
                  theme = 
                    theme(plot.caption = element_textbox_simple(size = rel(1.5))
                          )
                  )
Warning: No shared levels found between `names(values)` of the manual scale and the
data's fill values.

Code
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H6

ggsave("figures/Fig11.pdf", height = 10, width = 15)
Warning: No shared levels found between `names(values)` of the manual scale and the
data's fill values.
Code
#| fig-height: 10
#| fig-width: 15
#| filename: Combination of partial plots H6

ggsave("figures/Fig11.png", height = 10, width = 15)
Warning: No shared levels found between `names(values)` of the manual scale and the
data's fill values.

Visualization for daytype

In [89]:
Code
prepare lightsource model H6 plot
#| filename: prepare lightsource model H6 plot
H6_dt_data <- 
H6_model_gam |> 
  conditional_values(
    condition = list(Time = seq(0.5, 23.5, by = 1), "daytype", "photoperiod.state"),
    exclude = c("s(Time,Id)", "s(Id_date)", "s(Time,site)", "s(Time,exercise)")
    ) |> 
  mutate(across(c(.fitted, .lower_ci, .upper_ci),
                exp_zero_inflated)
  )

H6_dt_data <-
GAM_data |> 
  Datetime2Time() |> 
  group_by(daytype, Time) |> 
  summarize(
    .groups = "drop_last",
    dawn = dawn |> as.numeric() |> mean(na.rm = TRUE)/3600,
    dusk = dusk |> as.numeric() |> mean(na.rm = TRUE)/3600,
    n = n()
  ) |> 
  mutate(
    photoperiod.state = ifelse(between(Time, dawn, dusk), "day", "night")
  ) |> 
  select(-c(dawn, dusk)) |> 
  left_join(H6_dt_data, by = c("daytype", "Time", "photoperiod.state"))

H6_dt_data_phot <- 
H6_dt_data |> 
  number_states(photoperiod.state) |> 
  filter(photoperiod.state == "night") |> 
  group_by(daytype, photoperiod.state.count) |> 
  summarize(xmin = min(Time),
            xmax = max(Time),
            .groups = "drop_last") |> 
  mutate(across(c(xmin, xmax), \(x) replace_values(x,
                                                   0.5 ~ 0,
                                                   23.5 ~ 24)))
In [90]:
Code
Visualize lightsource model H6
#| filename: Visualize lightsource model H6
#| fig-height: 5
#| fig-width: 14

H6_dt_pred <-
H6_dt_data |> 
  ggplot(aes()) +
  map(c(1,10,250, 10^4), 
          \(x) geom_hline(aes(yintercept = x), 
                          col = "grey", linetype = "dashed")
      ) +
  geom_rect(data = H6_dt_data_phot |> filter(daytype == "a free day"),
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = .lower_ci, 
                  ymax = .upper_ci, fill = daytype), alpha = 0.5) +
  geom_line(aes(x = Time, y = .fitted, 
                col = daytype), linewidth = 1) +
  # scale_fill_manual(values = act_colors) +
  # scale_color_manual(values = act_colors) +
  theme_cowplot() +
  scale_y_continuous(trans = "symlog", breaks = c(0,1,10,100,250, 1000)) +
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), ylim = c(0,10^4), expand = FALSE) +
  labs(y = "Melanopic EDI (lx)", x = "Local time (hrs)", fill = "Daytype", col = "Daytype") +
  # facet_wrap(~daytype, nrow = 1) +
  guides( y = guide_axis_stack(Brown_bracket, "axis")) +
  # gghighlight(
    # label_key = daytype,
    # label_params = list(x = 12, y = 4.5, force = 0, hjust = 0.5)
  # ) +
  # geom_text(aes(label = paste0("|", n, "|"), x = Time, y = 7000), size = rel(1.5)) +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple(),
        legend.position = "inside",
        legend.position.inside = c(0.45, 0.15)
        )
  # labs(caption = Brown_caption)
H6_dt_pred

In [91]:
Code
df <- H6_model_gam$model |> distinct(Time, .keep_all = TRUE)
H6_termplot <- 
difference_sz(H6_model_gam, 
              df, 
              "Time", "daytype", "a work day", "a free day",
              "s(Time,daytype)",
              exclude = c("s(Time,site)", "s(Time,Id)", "s(Id_date)", "s(Time,exercise"),
              unconditional = FALSE,
              level = 0.95
  ) |> 
    mutate(
         signif = lower > 0 | upper < 0,
         signif_id = consecutive_id(signif)) |> 
  ggplot(aes()) +
  map(c(log10(c(0.1, 0.2, 0.5, 2, 5, 10))),
      \(x) geom_hline(yintercept = x, linewidth = 0.5, col = "grey", linetype = 2)) +
  geom_rect(data = H6_dt_data_phot |> filter(daytype == "a free day"),
            aes(xmin = xmin, ymin = -Inf, ymax = Inf, xmax = xmax),
            alpha = 0.25, inherit.aes = FALSE) +
  geom_ribbon(aes(x = Time, ymin = lower, ymax = upper), alpha = 0.5) +
  geom_hline(yintercept = 0, linewidth = 1, linetype = 2) +
  geom_line(aes(x = Time, y = diff), 
            linewidth = 1) +
  geom_line(data = \(x) filter(x, signif),
            aes(x = Time, y = diff, group = signif_id), 
            col = "red", linewidth = 1) +
    theme_cowplot() +
  scale_y_continuous(labels = expression(10^-1, 5^-1, 2^-1, "1  ", "2  ", "5  ", "10  "), 
                     breaks = log10(c(0.1, 0.2, 0.5, 1, 2, 5, 10)))+
  scale_x_continuous(breaks = seq(0, 24, by = 6)) +
  coord_cartesian(xlim = c(0,24), expand = FALSE) +
  guides(fill = "none") +
  labs(y = "Rel. difference work to free day", x = "Local time (hrs)") +
  theme_sub_strip(background = element_blank(),
                  text = element_blank()) +
  theme(panel.spacing = unit(1, "lines"),
        plot.caption = element_textbox_simple())
In [92]:
Code
Combination of partial plots H6
#| fig-height: 8
#| fig-width: 8
#| filename: Combination of partial plots H6

H6_dt_pred / H6_termplot +
  plot_annotation(tag_levels = "A", 
                  caption = paste(Brown_caption, 
                                  "Panel B: Significant deviations of work from free days are shown in red", sep = "<br>"),
                  theme = 
                    theme(plot.caption = element_textbox_simple(size = rel(1))
                          )
                  )

Code
#| fig-height: 8
#| fig-width: 8
#| filename: Combination of partial plots H6

ggsave("figures/Fig12.pdf", height = 8, width = 7.3)
ggsave("figures/Fig12.png", height = 8, width = 7.3)

Testing other drivers

In [93]:
Code
activity_data <- 
H6_data |> 
  ungroup() |> 
  # select(site, Id, geo.MEDI, starts_with("act_"), Datetime, lightsource_primary) |> 
  pivot_longer(cols = starts_with("act_"), names_to = "activity", names_prefix = "act_", values_to = "value") |> 
  filter(value, !is.na(geo.MEDI)) |>
  distinct(.keep_all = TRUE, site, Id, Datetime, activity) |>
  select(-value)

activity_data <-
  activity_data |> 
  mutate(activity = 
           fct(activity) |> 
           fct_collapse(outdoor = c("free_outdoor", "working_outdoor", "road_open")) |> 
           fct_recode(NULL = "other") |> 
           fct_relevel("home")
         ) |> 
  drop_na(geo.MEDI, daytype, exercise, sleep_duration, activity)
In [94]:
Code
Fit the model for H6
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
H6_formula_1 <- geo.MEDI ~ site*daytype + site*exercise + site*sleep_duration + (1| Id)
H6_formula_2 <- geo.MEDI ~ site*activity + site*daytype + site*exercise + site*sleep_duration + (1| Id)
H6_formula_0_daytype <- geo.MEDI ~ site*activity + site*exercise + site*sleep_duration + (1| Id)
H6_formula_0_exercise <- geo.MEDI ~ site*activity + site*daytype + site*sleep_duration + (1|Id)
H6_formula_0_sleep <- geo.MEDI ~ site*activity + site*daytype + site*exercise + (1|Id)
H6_formula_0_site <- geo.MEDI ~ activity + daytype + exercise + sleep_duration + (1|Id)

#confirmatory
H6_model_1 <- 
  glmmTMB(H6_formula_1, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_2 <- 
  glmmTMB(H6_formula_2, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_daytype <- 
  glmmTMB(H6_formula_0_daytype, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_exercise <- 
  glmmTMB(H6_formula_0_exercise, 
          data = activity_data, 
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_sleep <- 
  glmmTMB(H6_formula_0_sleep, 
          data = activity_data,
          REML = FALSE, 
          family = tweedie(),
          contrasts = list(site = "contr.sum")
          )
H6_model_0_site <- 
  glmmTMB(H6_formula_0_site, 
          data = activity_data,
          REML = FALSE, 
          family = tweedie(),
          # contrasts = list(site = "contr.sum")
          )

comp_daytype <- 
anova(H6_model_0_daytype, H6_model_2)
comp_daytype
Data: activity_data
Models:
H6_model_0_daytype: geo.MEDI ~ site * activity + site * exercise + site * sleep_duration + , zi=~0, disp=~1
H6_model_0_daytype:     (1 | Id), zi=~0, disp=~1
H6_model_2: geo.MEDI ~ site * activity + site * daytype + site * exercise + , zi=~0, disp=~1
H6_model_2:     site * sleep_duration + (1 | Id), zi=~0, disp=~1
                   Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)
H6_model_0_daytype 84 125418 126056 -62625   125250                         
H6_model_2         93 125237 125944 -62526   125051 198.55      9  < 2.2e-16
                      
H6_model_0_daytype    
H6_model_2         ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
comp_exercise <- 
anova(H6_model_0_exercise, H6_model_2)
comp_exercise
Data: activity_data
Models:
H6_model_0_exercise: geo.MEDI ~ site * activity + site * daytype + site * sleep_duration + , zi=~0, disp=~1
H6_model_0_exercise:     (1 | Id), zi=~0, disp=~1
H6_model_2: geo.MEDI ~ site * activity + site * daytype + site * exercise + , zi=~0, disp=~1
H6_model_2:     site * sleep_duration + (1 | Id), zi=~0, disp=~1
                    Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)
H6_model_0_exercise 66 125414 125915 -62641   125282                         
H6_model_2          93 125237 125944 -62526   125051 230.46     27  < 2.2e-16
                       
H6_model_0_exercise    
H6_model_2          ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
comp_sleep <- 
anova(H6_model_0_sleep, H6_model_2)
comp_sleep
Data: activity_data
Models:
H6_model_0_sleep: geo.MEDI ~ site * activity + site * daytype + site * exercise + , zi=~0, disp=~1
H6_model_0_sleep:     (1 | Id), zi=~0, disp=~1
H6_model_2: geo.MEDI ~ site * activity + site * daytype + site * exercise + , zi=~0, disp=~1
H6_model_2:     site * sleep_duration + (1 | Id), zi=~0, disp=~1
                 Df    AIC    BIC logLik deviance  Chisq Chi Df Pr(>Chisq)    
H6_model_0_sleep 84 125278 125916 -62555   125110                             
H6_model_2       93 125237 125944 -62526   125051 59.059      9  2.035e-09 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Code
#| filename: Fit the model for H6
#| fig-height: 10
#| fig-width: 8
AIC(H6_model_0_sleep, H6_model_0_exercise, H6_model_0_daytype, H6_model_2) |> 
  arrange(AIC) |> 
  rownames_to_column("model") |> gt()
model df AIC
H6_model_2 93 125237.0
H6_model_0_sleep 84 125278.1
H6_model_0_exercise 66 125413.5
H6_model_0_daytype 84 125417.6

Hypothesis 7

Details

Hypothesis 7 states:

There is a ceiling effect of photoperiod with level, duration, and exposure-history-based metrics

Type of model: GAMM

Base formula: - \text{Metric} \sim s(\text{photoperiod}) + s(Site, bs = "re") + s(Participant, bs = "re")

Notes: - Only metrics that have proven significant in H1 will be used here. These are for -photoperiod and level: Mean, Brightest 10h mean, and Darkest 10h mean -photoperiod and duration: Duration above 1000, above 250 wake, below 10 pre-Sleep, and Period above 250 -photoperiod and exposue history: Dose - The confirmatory tests look at the base model compared to the Null models for each covariate - The tensor product of latitude and photoperiod was removed towards singular effects, as the 2d plot is sparsely populated along latitude

Specific formulas: - \text{Metric} \sim s(Site, bs = "re") + s(Participant, bs = "re") (Null model)

Preparation

In [95]:
Code
H7_data <- 
metrics |> 
  select(-c(H1_1_sig, H1_lat_sig)) |> 
  filter(H1_phot_sig, metric_type %in% c("level", "duration", "exposure history")) |> 
  mutate(H7_1 = "log_zero_inflated(metric) ~ s(photoperiod) + s(site, bs = 're') + s(Id, bs = 're')",
         H7_0 = "log_zero_inflated(metric) ~  s(site, bs = 're') + s(Id, bs = 're')",
         across(c(H7_1, H7_0), \(x) map(x, as.formula)),
         data = data |> map(\(x) x |> mutate(site = factor(site),
                                             Id = factor(Id)))
         )

Analysis

In [96]:
Code
H7_models <- 
H7_data |> 
  mutate(
    across(c(H7_1, H7_0),
           \(x) pmap(list(data, x),
                     \(data, formula) {
                       bam(formula, 
                            data = data, 
                            discrete = TRUE,
                            method = "fREML",
                            nthreads = 10
                            )
                     })),
    AICs = pmap(list(H7_1, H7_0), AIC),
    dAIC = map_dbl(AICs, \(x) x$AIC[[1]] - x$AIC[[2]]),
    signif = dAIC < -2
  )

The following models are significant:

In [97]:
Code
H7_models |> 
  filter(signif) |> 
  select(metric_type, name, dAIC) |> 
  gt() |> 
  fmt_number()
metric_type name dAIC
level Mean −5.42
level brightest_10h_mean −4.26
level darkest_10h_mean −5.17
duration duration_above_1000 −3.44
duration period_above_250 −3.46
exposure history dose −6.56

Mean

In [98]:
Code
#| fig-width: 12
#| fig-height: 5
mod <- H7_models |> filter(signif) |> pull(H7_1)

gam_deriv_plot(mod[[1]], H7_models$name[[1]])

Brightest 10h mean

In [99]:
Code
#| fig-width: 12
#| fig-height: 5
gam_deriv_plot(mod[[2]], "Brightest 10h mean")

Darkest 10h mean

In [100]:
Code
#| fig-width: 12
#| fig-height: 5
gam_deriv_plot(mod[[3]], "Darkest 10h mean")

Duration above 1000 lx melanopic EDI

In [101]:
Code
#| fig-width: 12
#| fig-height: 5
gam_deriv_plot(mod[[4]], "Duration above 1000 lx melanopic EDI")

Period above 250 lx melanopic EDI

In [102]:
Code
#| fig-width: 12
#| fig-height: 5
gam_deriv_plot(mod[[5]], "Period above 250 lx melanopic EDI")

Melanopic EDI dose

In [103]:
Code
#| fig-width: 12
#| fig-height: 5
gam_deriv_plot(mod[[6]], "Melanopic EDI dose")

In [104]:
Code
#| fig-height: 21
#| fig-width: 10
gam_deriv_plot(mod[[1]], H7_models$name[[1]]) /
gam_deriv_plot(mod[[3]], "Darkest 10h mean") /
gam_deriv_plot(mod[[4]], "Duration above 1000 lx melanopic EDI") /
gam_deriv_plot(mod[[6]], "Melanopic EDI dose") /
gam_deriv_plot(mod[[5]], "Period above 250 lx melanopic EDI") /
gam_deriv_plot(mod[[1]], H7_models$name[[1]]) + plot_annotation(tag_levels = "A")

Code
#| fig-height: 21
#| fig-width: 10
ggsave("figures/Fig14.pdf", height = 21, width = 10)
ggsave("figures/Fig14.png", height = 21, width = 10)

Interpretation

  • For level metrics, there is a ceiling effect of photoperiod around 14.7 to 16.4 hours, where increases are no longer significant. This is similar for Duration above 1000 lx melanopic EDI, which has a ceiling effect at 16.6 hours.
  • Period above 250 lx melanopic EDI and Dose don’t show a slow-down effect

Session info

In [105]:
Code cell - Session info
#| code-summary: Code cell - Session info
sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: aarch64-apple-darwin20
Running under: macOS 26.3.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/Berlin
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] ordinal_2025.12-29  legendry_0.2.4      nnet_7.3-20        
 [4] gtsummary_2.5.0     gghighlight_0.5.0   glmmTMB_1.1.14     
 [7] gt_1.3.0            broom.mixed_0.2.9.7 rlang_1.2.0        
[10] patchwork_1.3.2     emmeans_2.0.3       gratia_0.11.2      
[13] itsadug_2.5         plotfunctions_1.5   broom_1.0.12       
[16] sjPlot_2.9.0        ggtext_0.1.2        ggridges_0.5.7     
[19] cowplot_1.2.0       performance_0.16.0  lme4_2.0-1         
[22] Matrix_1.7-3        mgcv_1.9-4          nlme_3.1-168       
[25] LightLogR_0.10.2    melidosData_1.0.6   lubridate_1.9.5    
[28] forcats_1.0.1       stringr_1.6.0       dplyr_1.2.1        
[31] purrr_1.2.2         readr_2.2.0         tidyr_1.3.2        
[34] tibble_3.3.1        ggplot2_4.0.2       tidyverse_2.0.0    

loaded via a namespace (and not attached):
  [1] RColorBrewer_1.1-3  rstudioapi_0.18.0   DHARMa_0.4.7       
  [4] jsonlite_2.0.0      datawizard_1.3.0    magrittr_2.0.5     
  [7] estimability_1.5.1  farver_2.1.2        nloptr_2.2.1       
 [10] rmarkdown_2.31      fs_2.0.1            ragg_1.5.2         
 [13] vctrs_0.7.3         minqa_1.2.8         base64enc_0.1-6    
 [16] effectsize_1.0.2    htmltools_0.5.9     sjmisc_2.8.11      
 [19] sass_0.4.10         parallelly_1.47.0   htmlwidgets_1.6.4  
 [22] sandwich_3.1-1      zoo_1.8-15          TMB_1.9.21         
 [25] commonmark_2.0.0    lifecycle_1.0.5     pkgconfig_2.0.3    
 [28] webshot2_0.1.2      sjlabelled_1.2.0    R6_2.6.1           
 [31] fastmap_1.2.0       rbibutils_2.4.1     future_1.70.0      
 [34] digest_0.6.39       numDeriv_2016.8-1.1 furrr_0.4.0        
 [37] textshaping_1.0.5   labeling_0.4.3      timechange_0.4.0   
 [40] compiler_4.5.0      withr_3.0.2         S7_0.2.1-1         
 [43] backports_1.5.1     MASS_7.3-65         ucminf_1.2.3       
 [46] tools_4.5.0         chromote_0.5.1      otel_0.2.0         
 [49] glue_1.8.0          promises_1.5.0      gridtext_0.1.6     
 [52] grid_4.5.0          see_0.13.0          generics_0.1.4     
 [55] gtable_0.3.6        tzdb_0.5.0          websocket_1.4.4    
 [58] hms_1.1.4           xml2_1.5.2          utf8_1.2.6         
 [61] ggrepel_0.9.8       pillar_1.11.1       markdown_2.0       
 [64] later_1.4.8         splines_4.5.0       lattice_0.22-7     
 [67] renv_1.1.4          tidyselect_1.2.1    knitr_1.51         
 [70] reformulas_0.4.4    litedown_0.9        xfun_0.57          
 [73] statmod_1.5.1       stringi_1.8.7       yaml_2.3.12        
 [76] boot_1.3-31         ggokabeito_0.1.0    evaluate_1.0.5     
 [79] codetools_0.2-20    cli_3.6.6           tweedie_3.0.17     
 [82] xtable_1.8-8        parameters_0.28.3   systemfonts_1.3.2  
 [85] Rdpack_2.6.6        processx_3.8.7      mirai_2.6.1        
 [88] Rcpp_1.1.1-1        globals_0.19.1      coda_0.19-4.1      
 [91] nanonext_1.8.2      parallel_4.5.0      bayestestR_0.17.0  
 [94] mvnfast_0.2.8       listenv_0.10.1      viridisLite_0.4.3  
 [97] mvtnorm_1.3-7       scales_1.4.0        insight_1.5.0      
[100] cards_0.7.1