Time-Weighted Averages in Epidemiological Noise Studies

General definitions

In epidemiological studies of environmental noise, the general term time-weighted average is often used to describe energy-based averaging of noise levels over defined periods of time (European Union 2002). This approach is necessary because noise is measured in decibels (dB), a logarithmic scale, and cannot be averaged using arithmetic means (Brink and Haagsma 2024, p 25). Instead, energy-based (logarithmic) averaging is applied to accurately reflect cumulative exposure (Day–evening–night noise level 2025).

Why energy averaging is needed - not simple arithmetic mean

When calculating average noise levels over time, it’s critical to use energy-based rather than arithmetic averaging due to the logarithmic nature of the decibel scale. The difference becomes particularly important when there are large variations in noise levels.

The Logarithmic Nature of Sound

Sound intensity follows a logarithmic scale where each 10 dB increase represents a 10-fold increase in sound energy. Therefore, arithmetic averaging of decibel values leads to systematic underestimation of the true average noise exposure.

Note

An arithmetic average of 60 dB and 70 dB would give 65 dB, but this is incorrect because 70 dB has 10 times more sound energy than 60 dB. The correct energy-based average is approximately 67 dB.

Visual Demonstration of the Difference

Code
# Generate sample noise data with large variations
noise_samples <- c(60, 70, 60, 60, 60, 60, 60, 60, 60, 60)  # One very loud measurement
sample_times <- 1:length(noise_samples)

# Calculate both types of averages
arithmetic_avg <- mean(noise_samples)
energy_avg <- 10 * log10(mean(10^(noise_samples/10)))

# Create a plot to visualize the difference
plot(sample_times, noise_samples, type = "o", pch = 16, 
     xlab = "Sample", ylab = "Noise Level (dB)",
     main = "Arithmetic vs. Energy-Based Averaging",
     ylim = c(min(noise_samples) - 5, max(noise_samples) + 5))

# Add horizontal lines for both averages
abline(h = arithmetic_avg, col = "red", lwd = 2, lty = 2)
abline(h = energy_avg, col = "blue", lwd = 2, lty = 2)

# Add text labels
legend("topright", 
       legend = c(paste("Arithmetic Mean:", round(arithmetic_avg, 1), "dB"),
                  paste("Energy-Based Mean:", round(energy_avg, 1), "dB")),
       col = c("red", "blue"), 
       lwd = 2, lty = 2)

# Add explanation text
text(8, 62, "The arithmetic mean\nunderestimates the\ntrue noise exposure", col = "red")
text(8, 68, "Energy-based averaging\ncorrectly accounts for\nthe loud event", col = "blue")

Energy Contribution by Noise Level

The following visualization demonstrates why energy-based averaging is necessary by showing the relative energy contribution of different noise levels:

Code
# Create a range of noise levels from 50 to 80 dB
noise_levels <- 50:80
energy_values <- 10^(noise_levels/10)

# Calculate relative energy compared to 50 dB
relative_energy <- energy_values / energy_values[1]

# Create a data frame for plotting
energy_df <- data.frame(
  noise_level = noise_levels,
  relative_energy = relative_energy
)

# Create a bar plot of relative energy
barplot(energy_df$relative_energy, names.arg = energy_df$noise_level,
        log = "y",  # Use log scale for y-axis
        col = colorRampPalette(c("lightblue", "darkblue"))(length(noise_levels)),
        xlab = "Noise Level (dB)",
        ylab = "Relative Energy (compared to 50 dB, log scale)",
        main = "Relative Sound Energy by Noise Level")

# Add grid lines
grid(nx = NA, ny = NULL, lty = 2, col = "gray")

# Add text labels for key values
text(10, 10^3, "70 dB = 100× the energy of 50 dB", cex = 0.9)
text(25, 10^7, "80 dB = 1,000× the energy of 50 dB", cex = 0.9)

# Add subtitle
mtext("Note logarithmic scale - energy increases by factor of 10 for every 10 dB", side = 3, line = 0.5)

The general formula for calculating a time-weighted average noise level (Lden) over ( n ) periods is:

\[ Lden_{avg} = 10 \cdot \log_{10} \left( \frac{1}{n} \sum_{i=1}^{n} 10^{Lden_i/10} \right) \]

where ( \(Lden_i\) ) is the noise level in decibels for the ( i )-th period.

The division by 10 in the time-weighted average formula is due to the mathematical relationship between decibels and the actual physical quantities they represent.

  1. Converting back to energy domain: Decibel values are logarithmic representations of the underlying energy. The equation \(10^(Lden_i/10)\) converts the decibel value back to a quantity proportional to energy.

  2. Mathematical basis: Sound levels in dB represent a 10-fold increase in energy for each 10 dB increase. The formula is derived from: \[L = 10 \cdot \log_{10}(I/I_0)\] where I is intensity and I₀ is a reference intensity.

  3. Reversing the logarithm: To average energy correctly, we need to first convert from the logarithmic scale (dB) back to linear energy units using \(10^(Lden_i/10)\), then average these energy values, and finally convert back to dB using \(10*log_{10}()\).

Two specific applications of time-weighted averages are commonly used:

1. Monthly-Weighted Average

This approach is used to estimate an individual’s annual noise exposure when they have changed addresses during a given year. Since annual noise levels (e.g., Lden) differ by location, the final annual exposure is calculated by weighting each location’s Lden by the number of months the individual spent there. The resulting energy-weighted average accounts for partial-year exposures:

Example: - January to March (3 months) at Address A: Lden = 65 dB
- April to December (9 months) at Address B: Lden = 60 dB

\[ Lden_{year} = 10 \cdot \log_{10} \left( \frac{1}{12} \left(3 \cdot 10^{65/10} + 9 \cdot 10^{60/10} \right) \right) \approx 61.9\, \text{dB} \]

Code
# Monthly-Weighted Average - R example
months_a <- 3  # Months at address A
lden_a <- 65   # Noise level at address A (dB)
months_b <- 9  # Months at address B
lden_b <- 60   # Noise level at address B (dB)
total_months <- 12

# Calculate energy-weighted average
lden_year <- 10 * log10((1/total_months) * 
                       (months_a * 10^(lden_a/10) + 
                        months_b * 10^(lden_b/10)))

cat("Annual Lden:", round(lden_year, 1), "dB\n")
Annual Lden: 61.9 dB
Code
# Load required packages
library(dplyr)
library(knitr)

# Create example dataset with address changes
set.seed(123)
address_data <- tibble(
  id = rep(1:3, each = 3),  # 3 individuals
  address = c("A", "B", "B",  # Individual 1's addresses
              "C", "D", "D",  # Individual 2's addresses
              "E", "E", "F"), # Individual 3's addresses
  start_month = c(1, 4, 11,   # Start months for each address period
                  1, 6, 9,
                  1, 3, 8),
  end_month = c(3, 10, 12,    # End months for each address period
                5, 8, 12,
                2, 7, 12),
  lden_db = c(65, 60, 56,     # Noise levels at each address (dB)
              62, 59, 63,
              70, 68, 64)
)

# Calculate months at each address
address_data <- address_data %>%
  mutate(months_at_address = end_month - start_month + 1)

# Display the dataset
kable(address_data, caption = "Example address history dataset")
Example address history dataset
id address start_month end_month lden_db months_at_address
1 A 1 3 65 3
1 B 4 10 60 7
1 B 11 12 56 2
2 C 1 5 62 5
2 D 6 8 59 3
2 D 9 12 63 4
3 E 1 2 70 2
3 E 3 7 68 5
3 F 8 12 64 5
Code
# Function to calculate energy-weighted average
calc_energy_avg <- function(months, lden_values) {
  total_months <- sum(months)
  weighted_sum <- sum(months * 10^(lden_values/10))
  return(10 * log10(weighted_sum / total_months))
}

# Calculate annual Lden for each individual
annual_lden <- address_data %>%
  group_by(id) %>%
  summarize(
    annual_lden_db = calc_energy_avg(months_at_address, lden_db),
    total_months = sum(months_at_address),
    address_count = n_distinct(address)
  )

# Display results
kable(annual_lden, 
      caption = "Annual time-weighted average noise exposure by individual",
      digits = 1)
Annual time-weighted average noise exposure by individual
id annual_lden_db total_months address_count
1 61.6 12 2
2 61.8 12 2
3 67.3 12 2
Code
# Visualize the results
par(mar = c(5, 4, 3, 1))
barplot(annual_lden$annual_lden_db, 
        names.arg = paste("ID", annual_lden$id),
        ylab = "Annual Lden (dB)",
        main = "Time-weighted average noise exposure",
        col = "lightblue")

2. Multi-Year Energy Average

This method is used to compute preceding cumulative exposures over several years, typically used in studies assessing chronic exposure effects.

It is applied in two main ways:

  • Time-fixed: A fixed period (e.g., 5 years) prior to the baseline of the study. Each participant has one value.
  • Time-varying: A floating window (e.g., most recent 5 years before each time point in the follow-up). This results in a time-varying exposure variable, which is commonly used in Cox proportional hazards models.

Example for Time-Fixed 5-Year Average:
Lden values over the 5 years: 63, 64, 62, 61, 60 dB

\[ Lden_{5years} = 10 \cdot \log_{10} \left( \frac{1}{5} (10^{6.3} + 10^{6.4} + 10^{6.2} + 10^{6.1} + 10^{6.0}) \right) \approx 62.2\, \text{dB} \]

Code
# Multi-Year Energy Average - R example
# Example data: yearly Lden values for 2010-2014 (preceding years for 2014)
yearly_lden <- c(63, 64, 62, 61, 60)  # Lden values for 5 consecutive years
years <- 2010:2014  # The years these values correspond to
n_years <- length(yearly_lden)

# Function for calculating energy-based average
energy_avg <- function(decibels) {
  10 * log10(mean(10^(decibels/10)))
}

# Calculate the time-fixed 5-year average for baseline year 2014
lden_5year <- energy_avg(yearly_lden)
cat("5-year energy-average Lden for 2014 (based on 2010-2014):", round(lden_5year, 1), "dB\n\n")
5-year energy-average Lden for 2014 (based on 2010-2014): 62.2 dB
Code
# Time-varying example: 8 years of noise data (2010-2017)
all_years <- 2010:2017
all_years_lden <- c(65, 64, 63, 62, 61, 60, 59, 58)  # Noise values for 2010-2017
names(all_years_lden) <- all_years  # Label the data with years

# Define follow-up years for which we want to calculate preceding exposure
follow_up_years <- 2014:2017

# Calculate 5-year preceding exposure for each follow-up year
cat("Time-varying preceding 5-year exposures:\n")
Time-varying preceding 5-year exposures:
Code
for(year in follow_up_years) {
  # Define 5-year window preceding and including the current year
  window_years <- (year-4):year
  window_data <- all_years_lden[as.character(window_years)]
  
  # Calculate energy-average for this window
  window_avg <- energy_avg(window_data)
  
  # Display result with window years
  cat("Year", year, "exposure (based on", min(window_years), "-", year, "):", 
      round(window_avg, 1), "dB\n")
}
Year 2014 exposure (based on 2010 - 2014 ): 63.2 dB
Year 2015 exposure (based on 2011 - 2015 ): 62.2 dB
Year 2016 exposure (based on 2012 - 2016 ): 61.2 dB
Year 2017 exposure (based on 2013 - 2017 ): 60.2 dB
Code
# Visualize how the 5-year preceding window shifts
par(mar = c(5, 4, 4, 1))
plot(all_years, all_years_lden, type = "o", pch = 16, 
     xlab = "Year", ylab = "Annual Lden (dB)",
     main = "Preceding 5-year windows for different follow-up years",
     xlim = c(min(all_years), max(all_years)), ylim = c(min(all_years_lden)-1, max(all_years_lden)+1))

# Add colored segments for each 5-year window
colors <- c("red", "blue", "green", "purple")
for(i in 1:length(follow_up_years)) {
  year <- follow_up_years[i]
  window_years <- (year-4):year
  window_data <- all_years_lden[as.character(window_years)]
  window_avg <- energy_avg(window_data)
  
  # Highlight the window
  points(window_years, all_years_lden[as.character(window_years)], 
         pch = 16, col = colors[i], cex = 1.2)
  lines(window_years, all_years_lden[as.character(window_years)], 
        col = colors[i], lwd = 2)
  
  # Add a horizontal line for the average
  segments(min(window_years), window_avg, max(window_years), window_avg, 
          col = colors[i], lwd = 2, lty = 2)
  
  # Add label
  text(mean(window_years), window_avg + 0.5, 
       paste0("Avg: ", round(window_avg, 1), " dB"), col = colors[i])
}

# Add legend
legend("topright", 
       legend = paste("Window for", follow_up_years), 
       col = colors, lwd = 2, pch = 16)

Code
library(dplyr)
library(tidyr)
library(knitr)
library(ggplot2)

# Create longitudinal dataset with yearly noise exposures
set.seed(456)

# Create noise exposure data for multiple participants over multiple years
years <- 2010:2019  # 10 years of data
participant_ids <- 1:4  # 4 participants

# Create the dataset
noise_data <- expand.grid(
  id = participant_ids,
  year = years
) %>%
  as_tibble() %>%
  # Generate realistic noise values with some trends and variations
  group_by(id) %>%
  mutate(
    # Base noise level varies by participant
    base_noise = case_when(
      id == 1 ~ 60,
      id == 2 ~ 65,
      id == 3 ~ 58,
      id == 4 ~ 63
    ),
    # Add yearly variations with slight decreasing trend
    yearly_variation = runif(n(), -3, 3) - 0.2 * (year - min(year)),
    lden = base_noise + yearly_variation
  ) %>%
  ungroup()

# Display a sample of the data
kable(head(noise_data, 10), 
      caption = "Sample of yearly noise exposure data by participant")
Sample of yearly noise exposure data by participant
id year base_noise yearly_variation lden
1 2010 60 -2.4626904 57.53731
2 2010 65 -0.7623245 64.23768
3 2010 58 -1.9207012 56.07930
4 2010 63 2.8455239 65.84552
1 2011 60 -1.9369261 58.06307
2 2011 65 -1.8925486 63.10745
3 2011 58 1.1304616 59.13046
4 2011 63 -1.8961756 61.10382
1 2012 60 0.9977316 60.99773
2 2012 65 1.1306302 66.13063
Code
# Function to calculate energy-weighted average over years
calc_energy_avg_window <- function(noise_values) {
  10 * log10(mean(10^(noise_values/10)))
}

# 1. Time-fixed: Calculate 5-year average before baseline (2015)
fixed_window_data <- noise_data %>%
  filter(year >= 2010, year <= 2014) %>%  # 5-year window before 2015
  group_by(id) %>%
  summarize(
    baseline_year = 2015,
    time_fixed_avg_lden = calc_energy_avg_window(lden),
    years_included = paste(min(year), "-", max(year))
  )

# Display time-fixed results
kable(fixed_window_data, 
      caption = "Time-fixed 5-year average noise exposure (2010-2014)",
      digits = 1)
Time-fixed 5-year average noise exposure (2010-2014)
id baseline_year time_fixed_avg_lden years_included
1 2015 60.1 2010 - 2014
2 2015 65.1 2010 - 2014
3 2015 58.5 2010 - 2014
4 2015 63.9 2010 - 2014
Code
# 2. Time-varying: Calculate for each year the preceding 5-year exposure window
# For each year from 2014-2019, we calculate exposure from the 5 years before it
follow_up_years <- 2014:2019

# Create empty dataframe to store results
time_varying_data <- data.frame()

# For each follow-up year, calculate preceding 5-year average
for (current_year in follow_up_years) {
  # Define the 5-year window preceding the current year
  window_start <- current_year - 4
  window_end <- current_year
  
  # Filter data for this window
  window_data <- noise_data %>%
    filter(year >= window_start, year <= window_end) %>%
    group_by(id) %>%
    summarize(
      year = current_year,  # This is the year FOR WHICH we calculate preceding exposure
      preceding_5yr_lden = calc_energy_avg_window(lden),
      window_years = paste(window_start, "-", window_end)
    )
  
  # Add to results
  time_varying_data <- bind_rows(time_varying_data, window_data)
}

# Arrange data
time_varying_data <- time_varying_data %>%
  arrange(id, year)

# Display results
kable(head(time_varying_data, 8), 
      caption = "Time-varying preceding 5-year noise exposure",
      digits = 1)
Time-varying preceding 5-year noise exposure
id year preceding_5yr_lden window_years
1 2014 60.1 2010 - 2014
1 2015 60.2 2011 - 2015
1 2016 60.0 2012 - 2016
1 2017 59.3 2013 - 2017
1 2018 58.2 2014 - 2018
1 2019 57.2 2015 - 2019
2 2014 65.1 2010 - 2014
2 2015 65.2 2011 - 2015
Code
# Visualize time-varying exposures
ggplot(time_varying_data, aes(x = year, y = preceding_5yr_lden, 
                             color = factor(id), group = id)) +
  geom_line(size = 1) +
  geom_point() +
  labs(
    title = "Preceding 5-year average noise exposure by year",
    subtitle = "Each point shows exposure averaged over the 5 years preceding that year",
    x = "Year",
    y = "Preceding 5-year average Lden (dB)",
    color = "Participant ID"
  ) +
  theme_minimal() +
  scale_x_continuous(breaks = follow_up_years)

These methods ensure that noise exposure assessments reflect the true energy burden over time, accounting for address history and the chronicity of exposure.

Pressure-Based (20log10) vs Energy-Based Averaging (10log10)

A common point of confusion arises from the distinction between pressure-based and energy-based calculations when working with sound levels in decibels (dB). This is particularly relevant when discussing how to compute time-weighted or moving averages in noise exposure assessment.

The sound pressure level (SPL) is defined physically as:

\[ \text{SPL} = 20 \cdot \log_{10} \left( \frac{p}{p_0} \right) \]

where ( p ) is the measured sound pressure and ( p_0 = 20, ) is the reference pressure. This formula is used to convert raw sound pressure data into decibels.

In contrast, environmental noise indicators such as Lden and Lnight are already pressure-derived, energy-equivalent values expressed in dB. Once these values are available, we cannot average them arithmetically due to their logarithmic nature. Instead, we must convert them back into the energy domain before averaging:

\[ L_{avg} = 10 \cdot \log_{10} \left( \frac{1}{n} \sum_{i=1}^{n} 10^{L_i/10} \right) \]

This is known as energy-based averaging, and it is the correct approach for computing moving averages, multi-year exposures, or time-weighted values when using Lden or similar noise indicators.

Practical Interpretation

Although WHO’s Environmental Noise Guidelines for the European Region refer to Lden and Lnight as “sound pressure levels”, these are understood to be standardized, time-averaged energy equivalents based on SPL. The use of “20 log10” refers to the original calculation of SPL from pressure—not the method of averaging multiple Lden values.

Therefore, for epidemiological exposure assessment:

  • The “20 log10” formula is used in physical measurement of sound pressure (e.g., by sensors).
  • The “10 log10” formula is used in averaging exposure values like Lden that are already expressed in dB.

This distinction ensures accurate calculation of cumulative or time-varying noise exposure levels in health studies.

References

Brink, Mark, and Juanita Haagsma. 2024. “Determining the Population Health Impact of Environmental Noise.” In A Sound Approach to Noise and Health, 75–96. Springer Nature Singapore Singapore. https://library.oapen.org/bitstream/handle/20.500.12657/94584/978-981-97-6121-0.pdf?sequence=1#page=82.
Day–evening–night noise level. 2025. “Day–Evening–Night Noise Level – Wikipedia.” https://en.wikipedia.org/wiki/Day%E2%80%93evening%E2%80%93night_noise_level#Definition.
European Union. 2002. “Directive 2002/49/EC of the European Parliament and of the Council of 25 June 2002 Relating to the Assessment and Management of Environmental Noise.” http://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32002L0049&from=EN.