Back to Article
Personal light exposure: Metric preparation
Download Source

Personal light exposure: Metric preparation

Individual, behavioural, and environmental determinants of personal light exposure in daily life: a multi-country wearable and experience-sampling study

Last modified:

April 17, 2026

Preface

This script works off the data preprocessing, to produce one dataframe containing all metrics in numeric form, ready for analysis.

How metrics must be interpreted

  • Timing and duration-based metrics are converted to decimal-hours.
  • In the case of timing they mean decimal-hours from midnight.
  • The darkest_10h are centered on midnight, i.e., values before midnight are negative.
  • mean value metrics used a geometric mean (with log_zero_inflated()).

For a single, unnested dataframe of metrics execute:

metrics_glasses |> unnest(data)

Setup

In [1]:
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
library(melidosData)
library(LightLogR)
Warning: package 'LightLogR' was built under R version 4.5.2
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'.
library(readxl)
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
library(performance)
Warning: package 'performance' was built under R version 4.5.2
library(cowplot)

Attaching package: 'cowplot'

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

    stamp
library(ggridges)
library(downlit)
library(xml2)
Warning: package 'xml2' was built under R version 4.5.2
In [2]:
Load data
load("data/metrics_separate_glasses.RData")
load("data/metrics_separate_chest.RData")
load("data/preprocessed_glasses_2.RData")
load("data/preprocessed_chest_2.RData")
demographics <- load_data("demographics") |> flatten_data()

Preparation

Context

In [3]:
Prepare photoperiods
photoperiod_daily <- 
light_glasses_processed2 |> 
  group_by(site, Id, Date) |> 
  summarize(photoperiod = mean(photoperiod, na.rm = TRUE) |> as.numeric(),
            .groups = "drop_last")

photoperiod_participant <- 
light_glasses_processed2 |> 
  group_by(site, Id) |> 
  summarize(photoperiod = mean(photoperiod, na.rm = TRUE) |> as.numeric(),
            .groups = "drop_last")

photoperiod_daily_chest <- 
light_chest_processed2 |> 
  group_by(site, Id, Date) |> 
  summarize(photoperiod = mean(photoperiod, na.rm = TRUE) |> as.numeric(),
            .groups = "drop_last")

photoperiod_participant_chest <- 
light_chest_processed2 |> 
  group_by(site, Id) |> 
  summarize(photoperiod = mean(photoperiod, na.rm = TRUE) |> as.numeric(),
            .groups = "drop_last")
In [4]:
Prepare contextual info
demographics <- 
  demographics |> 
  select(site, Id, age, sex, gender, employment_status)

context <- 
  tibble(site = names(melidos_cities),
         country = unname(melidos_countries),
         tz = unname(melidos_tzs),
         city = unname(melidos_cities),
         ) |> 
  left_join(
    melidos_coordinates |> map(enframe) |> list_rbind(names_to = "site") |> pivot_wider() |> rename(lat = `1`, lon = `2`),
    by = "site"
  )

demographics <- 
  demographics |> 
  left_join(context, by = join_by("site"))

Metrics

In this section, we will combine all the metrics into one data-frame

In [5]:
Prepare participant metrics
metrics_glasses <- 
metric_glasses_participant |> 
  left_join(demographics, by = c("site", "Id")) |> 
  left_join(photoperiod_participant, by = c("site", "Id")) |> 
  pivot_longer(c(interdaily_stability, intradaily_variability), names_to = "name", values_to = "metric") |> 
  ungroup() |> 
  nest(.by = name) |> 
  mutate(type = "participant")

metrics_chest <- 
metric_chest_participant |> 
  left_join(demographics, by = c("site", "Id")) |> 
  left_join(photoperiod_participant_chest, by = c("site", "Id")) |> 
  pivot_longer(c(interdaily_stability, intradaily_variability), names_to = "name", values_to = "metric") |> 
  ungroup() |> 
  nest(.by = name) |> 
  mutate(type = "participant")
In [6]:
Prepare participant-day metrics
metrics_glasses <-
  rbind(
    metrics_glasses,
    metric_glasses_participantday |>
      #converting datetimes to times
      Datetime2Time() |>
      mutate(
        #converting times and durations to decimal hours
        across(c(where(hms::is_hms), where(is.duration)), 
               \(x) as.numeric(x) / 60 / 60),
        #centering nighttime variables on midnight
        across(
          c(darkest_10h_offset, darkest_10h_onset, darkest_10h_midpoint),
          \(x) ifelse(x > 12, x - 24, x)
        )
      ) |>
      left_join(demographics, by = c("site", "Id")) |> 
      left_join(photoperiod_daily, by = c("site", "Id", "Date")) |> 
      pivot_longer(c(Mean:duration_above_250_wake), names_to = "name", values_to = "metric") |>
      ungroup() |>
      nest(.by = name) |> 
      mutate(type = "participant-day")
  )

metrics_chest <-
  rbind(
    metrics_chest,
    metric_chest_participantday |>
      #converting datetimes to times
      Datetime2Time() |>
      mutate(
        #converting times and durations to decimal hours
        across(c(where(hms::is_hms), where(is.duration)), 
               \(x) as.numeric(x) / 60 / 60),
        #centering nighttime variables on midnight
        across(
          c(darkest_10h_offset, darkest_10h_onset, darkest_10h_midpoint),
          \(x) ifelse(x > 12, x - 24, x)
        )
      ) |>
      left_join(demographics, by = c("site", "Id")) |> 
      left_join(photoperiod_daily_chest, by = c("site", "Id", "Date")) |> 
      pivot_longer(c(Mean:duration_above_250_wake), names_to = "name", values_to = "metric") |>
      ungroup() |>
      nest(.by = name) |> 
      mutate(type = "participant-day")
  )
In [7]:
types <- read_excel("data/metric_types.xlsx")
In [8]:
metrics_glasses <-
  metrics_glasses |> 
  unnest(data) |> 
  mutate(across(c(site, Id, country, city, Date), factor)) |> 
  nest(data = -c(name, type)) |> 
  left_join(types, by = "name")

metrics_chest <-
  metrics_chest |> 
  unnest(data) |> 
  mutate(across(c(site, Id, country, city, Date), factor)) |> 
  nest(data = -c(name, type)) |> 
  left_join(types, by = "name")

Distributional characteristics

In [9]:
metrics_glasses |> 
  unnest(data) |> 
  ggplot(aes(x=metric)) +
  geom_density_ridges(aes(y=site, fill = site))+
  facet_wrap(~ name, scales = "free") +
  scale_fill_manual(values = melidos_colors) +
  theme_cowplot()
Picking joint bandwidth of 52.4
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.0695
Picking joint bandwidth of 0.5
Picking joint bandwidth of 0.487
Picking joint bandwidth of 0.501
Picking joint bandwidth of 2750
Picking joint bandwidth of 0.386
Picking joint bandwidth of 0.846
Picking joint bandwidth of 0.824
Picking joint bandwidth of 0.608
Picking joint bandwidth of 0.382
Picking joint bandwidth of 0.676
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.769
Picking joint bandwidth of 0.0374
Picking joint bandwidth of 1.74
Picking joint bandwidth of 0.578
Picking joint bandwidth of 0.253
Warning: Removed 293 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

ggsave("figures/exploration/metric_distribution_glasses.pdf", width = 18, height = 15)
Picking joint bandwidth of 52.4
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.0695
Picking joint bandwidth of 0.5
Picking joint bandwidth of 0.487
Picking joint bandwidth of 0.501
Picking joint bandwidth of 2750
Picking joint bandwidth of 0.386
Picking joint bandwidth of 0.846
Picking joint bandwidth of 0.824
Picking joint bandwidth of 0.608
Picking joint bandwidth of 0.382
Picking joint bandwidth of 0.676
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.769
Picking joint bandwidth of 0.0374
Picking joint bandwidth of 1.74
Picking joint bandwidth of 0.578
Picking joint bandwidth of 0.253
Warning: Removed 293 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
In [10]:
metrics_chest |> 
  unnest(data) |> 
  ggplot(aes(x=metric)) +
  geom_density_ridges(aes(y=site, fill = site))+
  facet_wrap(~ name, scales = "free") +
  scale_fill_manual(values = melidos_colors) +
  theme_cowplot()
Picking joint bandwidth of 50.7
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.051
Picking joint bandwidth of 0.494
Picking joint bandwidth of 0.519
Picking joint bandwidth of 0.495
Picking joint bandwidth of 3530
Picking joint bandwidth of 0.42
Picking joint bandwidth of 0.788
Picking joint bandwidth of 0.743
Picking joint bandwidth of 0.606
Picking joint bandwidth of 0.354
Picking joint bandwidth of 0.693
Picking joint bandwidth of 0.0388
Picking joint bandwidth of 0.198
Picking joint bandwidth of 0.642
Picking joint bandwidth of 0.0408
Picking joint bandwidth of 1.61
Picking joint bandwidth of 0.551
Picking joint bandwidth of 0.23
Warning: Removed 365 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

ggsave("figures/exploration/metric_distribution_chest.pdf", width = 18, height = 15)
Picking joint bandwidth of 50.7
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.572
Picking joint bandwidth of 0.051
Picking joint bandwidth of 0.494
Picking joint bandwidth of 0.519
Picking joint bandwidth of 0.495
Picking joint bandwidth of 3530
Picking joint bandwidth of 0.42
Picking joint bandwidth of 0.788
Picking joint bandwidth of 0.743
Picking joint bandwidth of 0.606
Picking joint bandwidth of 0.354
Picking joint bandwidth of 0.693
Picking joint bandwidth of 0.0388
Picking joint bandwidth of 0.198
Picking joint bandwidth of 0.642
Picking joint bandwidth of 0.0408
Picking joint bandwidth of 1.61
Picking joint bandwidth of 0.551
Picking joint bandwidth of 0.23
Warning: Removed 365 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

Export data

In [11]:
save(metrics_glasses, file = "data/metrics_glasses.RData")
save(metrics_chest, file = "data/metrics_chest.RData")