The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

cgmguru: Practical CGM Analysis Guide

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)
)
#>    rows subjects          first_time           last_time min_glucose
#> 1 13866        5 2015-02-24 17:31:29 2015-06-19 08:59:36          50
#>   max_glucose
#> 1         400
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)
#> # A tibble: 5 × 6
#>   id        sensor_wear_percent sensor_wear ndays start_date         
#>   <chr>                   <dbl>       <dbl> <dbl> <dttm>             
#> 1 Subject 1                79.8        79.8    NA 2015-06-06 16:50:27
#> 2 Subject 2                58.9        58.9    NA 2015-02-24 17:31:29
#> 3 Subject 3                92.1        92.1    NA 2015-03-10 15:36:26
#> 4 Subject 4                98.7        98.7    NA 2015-03-13 12:44:09
#> 5 Subject 5                95.8        95.8    NA 2015-02-28 17:40:06
#> # ℹ 1 more variable: end_date <dttm>

sensor_wear(cgm_data, ndays = 14, reading_minutes = 5)
#> # A tibble: 5 × 6
#>   id        sensor_wear_percent sensor_wear ndays start_date         
#>   <chr>                   <dbl>       <dbl> <dbl> <dttm>             
#> 1 Subject 1                72.3        72.3    14 2015-06-05 08:59:36
#> 2 Subject 2                51.1        51.1    14 2015-02-27 09:38:01
#> 3 Subject 3                38.0        38.0    14 2015-03-02 10:11:05
#> 4 Subject 4                90.9        90.9    14 2015-03-12 10:01:58
#> 5 Subject 5                72.5        72.5    14 2015-02-25 08:04:28
#> # ℹ 1 more variable: end_date <dttm>

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)
#> [1] "subject_summary"        "glycemic_event_summary"
head(all_events$subject_summary)
#> # A tibble: 5 × 24
#>   id          TIR  TITR TBR70 TBR54 TAR180 TAR250    CV    SD mean_glucose   GMI
#>   <chr>     <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl> <dbl> <dbl>        <dbl> <dbl>
#> 1 Subject 1  91.7 73.7   0.14  0      8.2    0.38  26.9  33.3         124.  6.27
#> 2 Subject 2  26.4  3.36  0     0     73.6   26.1   24.0  52.4         218.  8.54
#> 3 Subject 3  81.3 49.8   0.33  0     18.3    5.68  29.1  44.8         154.  6.99
#> 4 Subject 4  95.1 67.7   0.27  0.05   4.61   0     22.4  29.1         130.  6.41
#> 5 Subject 5  62.1 30.1   0.1   0     37.8   11.3   33.6  58.6         175.  7.49
#> # ℹ 13 more variables: uGMI <dbl>, GRI <dbl>, sensor_wear_percent <dbl>,
#> #   hypo_lv1_total_episodes <int>, hypo_lv2_total_episodes <int>,
#> #   hypo_extended_total_episodes <int>, hypo_lv1_excl_total_episodes <int>,
#> #   hypo_rebound_total_episodes <int>, hyper_lv1_total_episodes <int>,
#> #   hyper_lv2_total_episodes <int>, hyper_extended_total_episodes <int>,
#> #   hyper_lv1_excl_total_episodes <int>, hyper_rebound_total_episodes <int>
head(all_events$glycemic_event_summary)
#> # A tibble: 6 × 6
#>   id        type  level    total_episodes avg_ep_per_day avg_minutes_below_54_…¹
#>   <chr>     <chr> <chr>             <int>          <dbl>                   <dbl>
#> 1 Subject 1 hypo  lv1                   1           0.09                       0
#> 2 Subject 1 hypo  lv2                   0           0                          0
#> 3 Subject 1 hypo  extended              0           0                          0
#> 4 Subject 1 hypo  lv1_excl              1           0.09                       0
#> 5 Subject 1 hypo  rebound               0           0                          0
#> 6 Subject 1 hyper lv1                  16           1.44                       0
#> # ℹ abbreviated name: ¹​avg_minutes_below_54_per_episode

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)
#> # A tibble: 5 × 24
#>   id          TIR  TITR TBR70 TBR54 TAR180 TAR250    CV    SD mean_glucose   GMI
#>   <chr>     <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl> <dbl> <dbl>        <dbl> <dbl>
#> 1 Subject 1  91.8 74.0   0.16  0      8.08   0.37  26.7  33.0         123.  6.26
#> 2 Subject 2  25.8  3.28  0     0     74.2   26.5   24.1  52.6         219.  8.54
#> 3 Subject 3  81.3 49.8   0.32  0     18.4    5.44  29.0  44.5         154.  6.98
#> 4 Subject 4  94.9 67.4   0.33  0.03   4.75   0     22.4  29.0         130.  6.41
#> 5 Subject 5  62.0 29.6   0.1   0     37.9   11.5   33.4  58.4         175.  7.49
#> # ℹ 13 more variables: uGMI <dbl>, GRI <dbl>, sensor_wear_percent <dbl>,
#> #   hypo_lv1_total_episodes <int>, hypo_lv2_total_episodes <int>,
#> #   hypo_extended_total_episodes <int>, hypo_lv1_excl_total_episodes <int>,
#> #   hypo_rebound_total_episodes <int>, hyper_lv1_total_episodes <int>,
#> #   hyper_lv2_total_episodes <int>, hyper_extended_total_episodes <int>,
#> #   hyper_lv1_excl_total_episodes <int>, hyper_rebound_total_episodes <int>

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)
#> [1] "subject_summary"        "glycemic_event_summary" "interpolated_data"
head(all_events_with_grid$interpolated_data)
#> # A tibble: 6 × 3
#>   id        time                   gl
#>   <chr>     <dttm>              <dbl>
#> 1 Subject 1 2015-06-06 16:55:00  148.
#> 2 Subject 1 2015-06-06 17:00:00  143.
#> 3 Subject 1 2015-06-06 17:05:00  137.
#> 4 Subject 1 2015-06-06 17:10:00  129.
#> 5 Subject 1 2015-06-06 17:15:00  122.
#> 6 Subject 1 2015-06-06 17:20:00  121.

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)
#> # A tibble: 6 × 3
#>   id        time                   gl
#>   <chr>     <dttm>              <dbl>
#> 1 Subject 1 2015-06-06 16:55:00  148.
#> 2 Subject 1 2015-06-06 17:00:00  143.
#> 3 Subject 1 2015-06-06 17:05:00  137.
#> 4 Subject 1 2015-06-06 17:10:00  129.
#> 5 Subject 1 2015-06-06 17:15:00  122.
#> 6 Subject 1 2015-06-06 17:20:00  121.

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
#> # A tibble: 5 × 3
#>   id        total_episodes avg_ep_per_day
#>   <chr>              <int>          <dbl>
#> 1 Subject 1             16           1.44
#> 2 Subject 2             21           2.13
#> 3 Subject 3              9           1.64
#> 4 Subject 4             13           1.02
#> 5 Subject 5             38           3.72
hypo_lv1$events_total
#> # A tibble: 5 × 3
#>   id        total_episodes avg_ep_per_day
#>   <chr>              <int>          <dbl>
#> 1 Subject 1              1           0.09
#> 2 Subject 2              0           0   
#> 3 Subject 3              1           0.18
#> 4 Subject 4              2           0.16
#> 5 Subject 5              1           0.1
head(hyper_lv1$events_detailed)
#> # A tibble: 6 × 7
#>   id        start_time          start_glucose end_time            end_glucose
#>   <chr>     <dttm>                      <dbl> <dttm>                    <dbl>
#> 1 Subject 1 2015-06-11 15:45:00          193. 2015-06-11 16:50:00        187.
#> 2 Subject 1 2015-06-11 17:25:00          195. 2015-06-11 19:00:00        183.
#> 3 Subject 1 2015-06-11 19:20:00          181. 2015-06-11 19:45:00        187.
#> 4 Subject 1 2015-06-11 22:35:00          187. 2015-06-11 23:45:00        185.
#> 5 Subject 1 2015-06-12 07:50:00          181. 2015-06-12 09:15:00        181.
#> 6 Subject 1 2015-06-13 16:55:00          180. 2015-06-13 18:25:00        186.
#> # ℹ 2 more variables: start_index <int>, end_index <int>

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)
#> # A tibble: 6 × 6
#>   id        type  level    total_episodes avg_ep_per_day avg_minutes_below_54_…¹
#>   <chr>     <chr> <chr>             <int>          <dbl>                   <dbl>
#> 1 Subject 1 hypo  lv1                   1           0.09                       0
#> 2 Subject 1 hypo  lv1_excl              1           0.09                       0
#> 3 Subject 1 hyper lv1                  16           1.44                       0
#> 4 Subject 1 hyper lv2                   2           0.18                       0
#> 5 Subject 1 hyper lv1_excl             14           1.26                       0
#> 6 Subject 2 hyper lv1                  21           2.13                       0
#> # ℹ abbreviated name: ¹​avg_minutes_below_54_per_episode

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
#> # A tibble: 5 × 2
#>   id        episode_counts
#>   <chr>              <int>
#> 1 Subject 1             10
#> 2 Subject 2             22
#> 3 Subject 3              7
#> 4 Subject 4             18
#> 5 Subject 5             42
head(grid_result$episode_start)
#> # A tibble: 6 × 4
#>   id        time                   gl index
#>   <chr>     <dttm>              <dbl> <int>
#> 1 Subject 1 2015-06-11 15:30:07   143   966
#> 2 Subject 1 2015-06-11 17:10:07   157   985
#> 3 Subject 1 2015-06-11 22:00:06   135  1038
#> 4 Subject 1 2015-06-11 22:25:06   162  1043
#> 5 Subject 1 2015-06-12 07:40:04   160  1154
#> 6 Subject 1 2015-06-13 16:34:59   132  1415
head(grid_result$grid_vector)
#> # A tibble: 6 × 1
#>    grid
#>   <int>
#> 1     0
#> 2     0
#> 3     0
#> 4     0
#> 5     0
#> 6     0

Lowering the threshold or gap generally makes detection more sensitive.

sensitive_grid <- grid(cgm_data, gap = 10, threshold = 120)
head(sensitive_grid$episode_counts)
#> # A tibble: 5 × 2
#>   id        episode_counts
#>   <chr>              <int>
#> 1 Subject 1             11
#> 2 Subject 2             22
#> 3 Subject 3             10
#> 4 Subject 4             22
#> 5 Subject 5             44

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
#> # A tibble: 5 × 2
#>   id        episode_counts
#>   <chr>              <int>
#> 1 Subject 1              8
#> 2 Subject 2             18
#> 3 Subject 3              7
#> 4 Subject 4             16
#> 5 Subject 5             39
head(maxima_result$results)
#> # A tibble: 6 × 8
#>   id    grid_time grid_gl maxima_time maxima_glucose time_to_peak_min grid_index
#>   <chr> <dttm>      <dbl> <dttm>               <dbl>            <dbl>      <int>
#> 1 Subj… 2015-06-…     143 2015-06-11…            276               40        967
#> 2 Subj… 2015-06-…     135 2015-06-11…            209               50       1039
#> 3 Subj… 2015-06-…     160 2015-06-12…            210               40       1155
#> 4 Subj… 2015-06-…     132 2015-06-13…            202               60       1416
#> 5 Subj… 2015-06-…     176 2015-06-14…            227               45       1677
#> 6 Subj… 2015-06-…     166 2015-06-16…            208               65       2223
#> # ℹ 1 more variable: maxima_index <int>

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)
#> # A tibble: 6 × 5
#>   id        grid_time           grid_gl maxima_time         maxima_gl
#>   <chr>     <dttm>                <dbl> <dttm>                  <dbl>
#> 1 Subject 1 2015-06-11 20:30:07     143 2015-06-11 21:10:07       276
#> 2 Subject 1 2015-06-12 03:00:06     135 2015-06-12 03:50:06       209
#> 3 Subject 1 2015-06-12 03:25:06     162 2015-06-12 03:50:06       209
#> 4 Subject 1 2015-06-12 12:40:04     160 2015-06-12 13:20:04       210
#> 5 Subject 1 2015-06-13 21:34:59     132 2015-06-13 22:34:59       202
#> 6 Subject 1 2015-06-14 22:39:55     176 2015-06-14 23:24:55       227
head(between_maxima$results)
#> # A tibble: 6 × 6
#>   id        grid_time           grid_gl maxima_time  maxima_glucose time_to_peak
#>   <chr>     <dttm>                <dbl> <dttm>                <dbl>        <dbl>
#> 1 Subject 1 2015-06-11 15:30:07     143 2015-06-11 …            276         2400
#> 2 Subject 1 2015-06-11 22:00:06     135 2015-06-11 …            158         1200
#> 3 Subject 1 2015-06-11 22:25:06     162 2015-06-11 …            209         1500
#> 4 Subject 1 2015-06-12 07:40:04     160 2015-06-12 …            210         2400
#> 5 Subject 1 2015-06-13 16:34:59     132 2015-06-13 …            202         3600
#> 6 Subject 1 2015-06-14 17:39:55     176 2015-06-14 …            227         2700

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
#> # A tibble: 5 × 2
#>   id        episode_counts
#>   <chr>              <int>
#> 1 Subject 1              9
#> 2 Subject 2             14
#> 3 Subject 3             11
#> 4 Subject 4             17
#> 5 Subject 5             34
head(excursion_result$episode_start)
#> # A tibble: 6 × 8
#>   id        time            gl maxima_time maxima_glucose time_to_peak_min index
#>   <chr>     <dttm>       <dbl> <dttm>               <dbl>            <dbl> <int>
#> 1 Subject 1 2015-06-11 …    87 2015-06-11…            170              120   949
#> 2 Subject 1 2015-06-11 …   187 2015-06-11…            267               75   982
#> 3 Subject 1 2015-06-11 …   120 2015-06-11…            202              120  1027
#> 4 Subject 1 2015-06-13 …    95 2015-06-13…            172              120  1400
#> 5 Subject 1 2015-06-14 …   113 2015-06-14…            190              120  1650
#> 6 Subject 1 2015-06-15 …    85 2015-06-15…            158              120  1896
#> # ℹ 1 more variable: maxima_index <int>

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)
)
#>   subjects  rows summary_rows grid_rows
#> 1       19 34890           19     34890

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

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.