cgmguru: Practical CGM Analysis Guide

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4,
  message = FALSE,
  warning = FALSE
)

library(cgmguru)

iglu_available <- requireNamespace("iglu", quietly = TRUE)
ggplot2_available <- requireNamespace("ggplot2", quietly = TRUE)

Overview

cgmguru provides high-performance tools for continuous glucose monitoring (CGM) analysis. It combines two complementary workflows:

This guide expands the package-level vignette into a practical workflow. It is based on the longer vignette("intro", package = "cgmguru") source and focuses on how to move from a raw CGM table to subject summaries, event tables, GRID starts, postprandial peaks, and simple visual checks.

Data Contract

Most cgmguru functions expect a data frame with these columns:

| Column | Meaning | | --- | --- | | id | Subject identifier | | time | POSIXct timestamp | | gl | Glucose value in mg/dL |

Rows may contain multiple subjects. For speed and reproducibility, it is good practice to order by id and time before analysis, especially when chaining several functions.

data(example_data_5_subject, package = "iglu")
data(example_data_hall, package = "iglu")

cgm_data <- orderfast(example_data_5_subject)

data.frame(
  rows = nrow(cgm_data),
  subjects = length(unique(cgm_data$id)),
  first_time = min(cgm_data$time),
  last_time = max(cgm_data$time),
  min_glucose = min(cgm_data$gl, na.rm = TRUE),
  max_glucose = max(cgm_data$gl, na.rm = TRUE)
)
cat("The 'iglu' package is not available, so example-data chunks are skipped.\n")

Two Preprocessing Models

The package intentionally separates event-grid analysis from GRID-family analysis.

Glycemic event functions use an iglu-compatible event grid:

These functions can infer reading_minutes per subject from the median positive timestamp spacing. They align to a full-day grid, interpolate gaps up to inter_gap, remove larger gap-masked rows, and classify events by contiguous segments.

GRID-family functions operate on the rows you pass in:

They do not automatically call the event-grid interpolation pipeline. If you want GRID analysis on an interpolated series, explicitly pass the interpolated data.

Subject-Level Overview

sensor_wear() calculates how much observed CGM data are present. By default it uses each subject's original timestamp span. Supplying ndays switches to a fixed retrospective window ending at each subject's last valid timestamp, or at a common end_date if you provide one.

sensor_wear(cgm_data, reading_minutes = 5)

sensor_wear(cgm_data, ndays = 14, reading_minutes = 5)

For a broader one-row-per-subject summary, use detect_all_events().

all_events <- detect_all_events(
  cgm_data,
  reading_minutes = 5,
  sensor_wear_ndays = 14
)

names(all_events)
head(all_events$subject_summary)
head(all_events$glycemic_event_summary)

The subject_summary table includes CGM summary metrics such as time in range, time below range, time above range, mean glucose, variability, GMI/uGMI, GRI, and sensor_wear_percent. By default, these summary glucose metrics are calculated from the original raw glucose values. Set summary_metrics_source = "preprocessed" to calculate them from the internal event grid after interpolation and gap masking.

all_events_preprocessed <- detect_all_events(
  cgm_data,
  reading_minutes = 5,
  summary_metrics_source = "preprocessed"
)

head(all_events_preprocessed$subject_summary)

Inspecting the Event Grid

When you need to audit event boundaries, use return_interpolated = TRUE. The returned interpolated_data is the grid used internally for event classification.

all_events_with_grid <- detect_all_events(
  cgm_data,
  reading_minutes = 5,
  return_interpolated = TRUE
)

names(all_events_with_grid)
head(all_events_with_grid$interpolated_data)

The standalone helper interpolate_cgm() is useful when you want to inspect preprocessing before running event detectors.

event_grid <- interpolate_cgm(cgm_data, reading_minutes = 5)
head(event_grid)

Hypoglycemia and Hyperglycemia Events

Use the standalone event functions when you need detailed event boundaries for one event type. Presets are available through type = "lv1", "lv2", "extended", and "lv1_excl".

hyper_lv1 <- detect_hyperglycemic_events(
  cgm_data,
  type = "lv1",
  reading_minutes = 5,
  return_interpolated = FALSE
)

hypo_lv1 <- detect_hypoglycemic_events(
  cgm_data,
  type = "lv1",
  reading_minutes = 5,
  return_interpolated = FALSE
)

hyper_lv1$events_total
hypo_lv1$events_total
head(hyper_lv1$events_detailed)

detect_all_events() is usually the most convenient interface for reporting, because it aggregates hypo- and hyperglycemia definitions in one call.

nonzero_events <- all_events$glycemic_event_summary[
  all_events$glycemic_event_summary$total_episodes > 0,
]
head(nonzero_events)

GRID Analysis

The GRID (Glucose Rate Increase Detector) workflow detects rapid glucose rises, often used as candidate meal or postprandial start points.

grid_result <- grid(cgm_data, gap = 15, threshold = 130)

grid_result$episode_counts
head(grid_result$episode_start)
head(grid_result$grid_vector)

Lowering the threshold or gap generally makes detection more sensitive.

sensitive_grid <- grid(cgm_data, gap = 10, threshold = 120)
head(sensitive_grid$episode_counts)

Postprandial Maxima

maxima_grid() combines GRID starts with local maxima to identify likely postprandial peaks within a time window.

maxima_result <- maxima_grid(
  cgm_data,
  threshold = 130,
  gap = 60,
  hours = 2
)

maxima_result$episode_counts
head(maxima_result$results)

For a more explicit step-by-step version of the workflow, chain the lower-level helpers.

grid_starts <- start_finder(grid_result$grid_vector)

mod_grid_result <- mod_grid(
  cgm_data,
  grid_starts,
  hours = 2,
  gap = 60
)

mod_grid_starts <- start_finder(mod_grid_result$mod_grid_vector)

max_after_result <- find_max_after_hours(
  cgm_data,
  mod_grid_starts,
  hours = 2
)

local_maxima <- find_local_maxima(cgm_data)

new_maxima <- find_new_maxima(
  cgm_data,
  max_after_result$max_index,
  local_maxima$local_maxima_vector
)

mapped_maxima <- transform_df(grid_result$episode_start, new_maxima)
between_maxima <- detect_between_maxima(cgm_data, mapped_maxima)

head(mapped_maxima)
head(between_maxima$results)

Excursion Analysis

excursion() identifies glucose excursions, defined as rises greater than 70 mg/dL within 2 hours, excluding starts preceded by hypoglycemia.

excursion_result <- excursion(cgm_data, gap = 15)

excursion_result$episode_counts
head(excursion_result$episode_start)

Quick Visualization

Visual checks are useful after tuning thresholds. The plot below shows one subject's glucose trace, clinical thresholds, and GRID episode starts.

subject_id <- unique(cgm_data$id)[1]
subject_data <- cgm_data[cgm_data$id == subject_id, ]
subject_grid_starts <- grid_result$episode_start[
  grid_result$episode_start$id == subject_id,
]

ggplot2::ggplot(subject_data, ggplot2::aes(x = time, y = gl)) +
  ggplot2::geom_line(linewidth = 0.3, color = "steelblue") +
  ggplot2::geom_hline(yintercept = c(54, 70), linetype = "dashed",
                      color = "darkorange") +
  ggplot2::geom_hline(yintercept = c(180, 250), linetype = "dashed",
                      color = "firebrick") +
  ggplot2::geom_point(
    data = subject_grid_starts,
    ggplot2::aes(x = time, y = gl),
    color = "black",
    size = 1.4
  ) +
  ggplot2::labs(
    title = paste("CGM Trace and GRID Starts:", subject_id),
    x = "Time",
    y = "Glucose (mg/dL)"
  ) +
  ggplot2::theme_minimal()
cat("The 'ggplot2' package is not available, so the plot is skipped.\n")

Scaling Up

The same workflow applies to larger multi-subject datasets. The example below uses example_data_hall from iglu.

hall_data <- orderfast(example_data_hall)

hall_summary <- detect_all_events(hall_data, reading_minutes = 5)
hall_grid <- grid(hall_data, gap = 15, threshold = 130)

data.frame(
  subjects = length(unique(hall_data$id)),
  rows = nrow(hall_data),
  summary_rows = nrow(hall_summary$subject_summary),
  grid_rows = nrow(hall_grid$grid_vector)
)

Function Map

| Task | Main function | | --- | --- | | Order CGM rows | orderfast() | | Calculate observed data coverage | sensor_wear() | | Build an event preprocessing grid | interpolate_cgm() | | Report all event summaries | detect_all_events() | | Detect one hypo/hyper event type | detect_hypoglycemic_events(), detect_hyperglycemic_events() | | Detect rapid glucose rises | grid() | | Detect local peaks | find_local_maxima() | | Combine GRID starts and peaks | maxima_grid() | | Find peaks/minima around starts | find_max_after_hours(), find_max_before_hours(), find_min_after_hours(), find_min_before_hours() | | Refine and map postprandial peaks | find_new_maxima(), transform_df(), detect_between_maxima() | | Detect large glucose excursions | excursion() |

Next Steps

For more detailed examples, open the focused vignettes:

browseVignettes("cgmguru")
vignette("intro", package = "cgmguru")
vignette("detect_all_events", package = "cgmguru")
vignette("grid", package = "cgmguru")
vignette("maxima_grid", package = "cgmguru")

References



Try the cgmguru package in your browser

Any scripts or data that you put into this service are public.

cgmguru documentation built on July 8, 2026, 9:06 a.m.