| Title: | Reading and Standardizing Automated Insulin Delivery Data |
|---|---|
| Description: | Parses the native exports of several automated insulin delivery (AID) platforms into a common, platform-independent format for continuous glucose monitoring (CGM), basal, bolus, carbohydrate, and self-monitored blood glucose (SMBG) data. Additional functions check completeness, filter, produce visualizations, and convert the data to other formats. |
| Authors: | Madleina Caduff [aut, cre] (ORCID: <https://orcid.org/0000-0001-8941-4274>), Aritz Lizoain [aut] (ORCID: <https://orcid.org/0009-0004-5516-2573>), Diabetes Center Berne (DCB) [cph, fnd] |
| Maintainer: | Madleina Caduff <[email protected]> |
| License: | GPL-3 |
| Version: | 0.2.4 |
| Built: | 2026-10-28 19:24:53 UTC |
| Source: | https://github.com/cran/aidR |
Accepts either the output of parse_data for a single individual
or the output of merge_all for multiple individuals. Every
individual found in the data is checked separately.
check_completeness(data, necessary_types = c("cgm", "basal", "bolus", "carbs"))check_completeness(data, necessary_types = c("cgm", "basal", "bolus", "carbs"))
data |
A list with all data found for one individual (the output of
|
necessary_types |
A character vector with type names for which data should be available. |
No return value, called for side effects. Issues a message for every individual and data type with missing data.
# A mylife export contains no CGM and no basal data path <- system.file("extdata", "mylife_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) check_completeness(data) # Only require bolus and carbohydrate data check_completeness(data, necessary_types = c("bolus", "carbs"))# A mylife export contains no CGM and no basal data path <- system.file("extdata", "mylife_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) check_completeness(data) # Only require bolus and carbohydrate data check_completeness(data, necessary_types = c("bolus", "carbs"))
Accepts either the output of parse_data for a single individual
or the output of merge_all for multiple individuals. Every
individual found in the data is checked separately.
check_completeness_range( data, first_date, last_date, necessary_types = c("cgm", "basal", "bolus", "carbs") )check_completeness_range( data, first_date, last_date, necessary_types = c("cgm", "basal", "bolus", "carbs") )
data |
A list with all data found for one individual (the output of
|
first_date |
A character string representing the first date for which data should be available. |
last_date |
A character string representing the last date (included) for which data should be available. |
necessary_types |
A character vector with type names for which data should be available. |
No return value, called for side effects. Issues a message for every individual and data type with missing days.
# The example export covers 2026-06-22 only path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) check_completeness_range(data, first_date = "2026-06-21", last_date = "2026-06-23")# The example export covers 2026-06-22 only path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) check_completeness_range(data, first_date = "2026-06-21", last_date = "2026-06-23")
Compares the insulin summed over the individual data points with the daily
total reported by the export itself. Only daily totals with
source == "reported" are checked: where aidR summed the total itself
(source == "summed"), the two agree by construction. In practice this
means Glooko (basal and bolus) and Tandem Source (basal only).
check_insulin_totals(data, types = "basal", rel_tol = 0.2, abs_tol = 1)check_insulin_totals(data, types = "basal", rel_tol = 0.2, abs_tol = 1)
data |
A list with all data found for one individual (the output of
|
types |
Character vector with the types of insulin that are checked, any
of |
rel_tol |
Numeric, the relative tolerance of the comparison. |
abs_tol |
Numeric, the absolute tolerance of the comparison, in units of
insulin. A day is flagged when the absolute difference exceeds both
|
Accepts either the output of parse_data for a single individual
or the output of merge_all for multiple individuals. Every
individual found in the data is checked separately.
By default, only the basal insulin is checked, which is where the mismatches
described below arise. Pass types = c("basal", "bolus") to check the
bolus insulin as well.
A large mismatch means that the individual data points do not cover the whole day. The most common cause are AID systems that record basal rates for manual mode only (e.g. Omnipod on Glooko): the basal insulin delivered in automated mode is missing from the basal rates, but is included in the reported total.
Note that the first and the last day of an export are usually only partially covered, so a mismatch on those days is expected.
Invisibly, a data frame with one row per checked individual, day and type, holding the summed and the reported total and whether the two agree. Called for its side effects: a message is issued for every day where they do not.
path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) check_insulin_totals(data) # Check bolus insulin as well, and keep the comparison res <- check_insulin_totals(data, types = c("basal", "bolus")) respath <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) check_insulin_totals(data) # Check bolus insulin as well, and keep the comparison res <- check_insulin_totals(data, types = c("basal", "bolus")) res
Removes carbohydrate entries that were overwritten by another entry.
filter_carbs(data_id, max_minutes_overwrite = 2)filter_carbs(data_id, max_minutes_overwrite = 2)
data_id |
A list with all data found for one individual. Filtering is only performed if the carbohydrate and bolus data were cleaned and standardized. |
max_minutes_overwrite |
Maximum time difference (in minutes) between an overwritten entry and another entry. Default 2 minutes. If NULL, nothing is removed. |
In the bolus calculator, carbs may be entered, but the bolus may not be delivered (or cancelled), and the carbs may then be entered again, with a bolus shortly after. Some platforms (e.g. Glooko) export all of these entries.
A carbohydrate entry is removed if
its bolus is zero (found via link_id)
another carbohydrate entry of the same format (or sharing
one, for combined formats such as "glooko/tidepool"), with a
different link_id, lies at most max_minutes_overwrite
minutes before or after it.
it is not flagged as a hypo treatment (is_hypo_treatment).
Slowly absorbed carbohydrates merged during cleaning (linked entries with an
estimated_absorption_duration, see clean.glooko) are
never removed and do not count as another entry. The zero bolus linked to a
removed entry is removed from the bolus data as well.
A list with all data found for one individual, filtered, if
necessary. Removed rows are stored in the attribute removed, see
removed_entries().
path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) nrow(data$carbs) data_filtered <- filter_carbs(data) nrow(data_filtered$carbs) removed_entries(data_filtered)path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) nrow(data$carbs) data_filtered <- filter_carbs(data) nrow(data_filtered$carbs) removed_entries(data_filtered)
Filter CGM values
filter_CGM( data_id, truncate_min_cgm = 40, truncate_max_cgm = 400, min_minutes_difference_cgm = 5 )filter_CGM( data_id, truncate_min_cgm = 40, truncate_max_cgm = 400, min_minutes_difference_cgm = 5 )
data_id |
A list with all data found for one individual. Filtering is only performed if the CGM data was cleaned and standardized. |
truncate_min_cgm |
CGM values smaller than this threshold will be truncated. Default 40 mg/dL, if NULL, no filtering is performed. |
truncate_max_cgm |
CGM values larger than this threshold will be truncated. Default 400 mg/dL, if NULL, no filtering is performed. |
min_minutes_difference_cgm |
CGM values are downsampled to this time interval (in minutes). Default 5 minutes, if NULL, no downsampling is performed. |
A list with all data found for one individual, filtered, if necessary.
path <- system.file("extdata", "tandem_source_example.zip", package = "aidR") # Read data data <- parse_data(id = "1", paths = path) range(data$cgm$value) length(data$cgm$value) # Truncate to 40-400 mg/dL and downsample to 5-minute intervals data_filtered <- filter_CGM(data) range(data_filtered$cgm$value) length(data_filtered$cgm$value) # Truncate only, without downsampling data_truncated <- filter_CGM(data, min_minutes_difference_cgm = NULL) range(data_truncated$cgm$value) length(data_truncated$cgm$value)path <- system.file("extdata", "tandem_source_example.zip", package = "aidR") # Read data data <- parse_data(id = "1", paths = path) range(data$cgm$value) length(data$cgm$value) # Truncate to 40-400 mg/dL and downsample to 5-minute intervals data_filtered <- filter_CGM(data) range(data_filtered$cgm$value) length(data_filtered$cgm$value) # Truncate only, without downsampling data_truncated <- filter_CGM(data, min_minutes_difference_cgm = NULL) range(data_truncated$cgm$value) length(data_truncated$cgm$value)
Builds the data frame that the metric functions of the R-package
cgmquantify
expect, with columns Time, glucose, Date, time_of_day and
type_of_event. This is the same layout that cgmquantify::readfile()
produces from a Dexcom export.
get_cgmquantify_format(data, id = NULL)get_cgmquantify_format(data, id = NULL)
data |
A list with all data found for one individual (the output of
|
id |
Character or numeric participant identifier, naming the individual
to convert. Can be omitted if |
Note that cgmquantify always related to a single subject.
A data frame with columns Time (POSIXct), glucose
(mg/dL), Date, time_of_day and type_of_event
(1 above 180 mg/dL, -1 below 70 mg/dL and 0 in
between), sorted by time and compatible with the R-package cgmquantify.
path <- system.file("extdata", "dexcom_clarity_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) cgmquantify_df <- get_cgmquantify_format(data) cgmquantify::summary_glucose(cgmquantify_df)path <- system.file("extdata", "dexcom_clarity_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) cgmquantify_df <- get_cgmquantify_format(data) cgmquantify::summary_glucose(cgmquantify_df)
Convert CGM data to a format compatible with the R-package iglu
get_iglu_format(data)get_iglu_format(data)
data |
A list with all data found for one individual (the output of
|
A data frame with columns id, time and gl,
compatible with the R-package iglu.
path <- system.file("extdata", "dexcom_clarity_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) iglu_df <- get_iglu_format(data) iglu::plot_glu(iglu_df)path <- system.file("extdata", "dexcom_clarity_example.csv", package = "aidR") data <- parse_data(id = "1", paths = path) iglu_df <- get_iglu_format(data) iglu::plot_glu(iglu_df)
Merge data available for all individuals, separately per data type
merge_all( data, types = c("cgm", "basal", "bolus", "carbs", "SMBG", "total_basal", "total_bolus") )merge_all( data, types = c("cgm", "basal", "bolus", "carbs", "SMBG", "total_basal", "total_bolus") )
data |
A named list with the data of each individual (the output of
|
types |
A character vector with the data type names to be merged. Defaults to the five standardized types and the two daily insulin totals. |
A named list with one data frame per data type, holding the data of all individuals. Individuals without data of a given type contribute no rows.
all_data <- list( "1" = parse_data("1", system.file("extdata", "glooko_example.zip", package = "aidR")), "2" = parse_data("2", system.file("extdata", "dexcom_clarity_example.csv", package = "aidR")) ) combined <- merge_all(all_data) names(combined) head(combined$cgm) table(combined$cgm$id) # Merge CGM data only merge_all(all_data, types = "cgm")all_data <- list( "1" = parse_data("1", system.file("extdata", "glooko_example.zip", package = "aidR")), "2" = parse_data("2", system.file("extdata", "dexcom_clarity_example.csv", package = "aidR")) ) combined <- merge_all(all_data) names(combined) head(combined$cgm) table(combined$cgm$id) # Merge CGM data only merge_all(all_data, types = "cgm")
Iterates over one or more paths, discovers all contained files (including inside ZIP archives), and dispatches each file to the appropriate format-specific reader. Supported formats are Glooko, CareLink, mylife, Tidepool, YourLoops, Tandem Source and Dexcom Clarity.
parse_data(id, paths, clean = TRUE, ...)parse_data(id, paths, clean = TRUE, ...)
id |
Character or numeric participant identifier. Used for logging and
stored in the |
paths |
Character vector. One or more file or directory paths to search for data files. Directories are traversed recursively; ZIP archives are extracted automatically. |
clean |
Logical, if |
... |
Additional arguments passed on to the format-specific cleaning
methods, e.g. |
A list with all data found for one individual, merged per data type
and sorted by timestamp. Duplicate rows are removed. Rows removed
during cleaning are stored in the attribute removed, see
removed_entries. NULL if no valid data files were found.
path <- system.file("extdata", "glooko_example.zip", package = "aidR") # Raw data, as found in the export data_raw <- parse_data(id = "1", paths = path, clean = FALSE) names(data_raw) head(data_raw$cgm) # Cleaned and standardized data data <- parse_data(id = "1", paths = path) names(data) head(data$cgm)path <- system.file("extdata", "glooko_example.zip", package = "aidR") # Raw data, as found in the export data_raw <- parse_data(id = "1", paths = path, clean = FALSE) names(data_raw) head(data_raw$cgm) # Cleaned and standardized data data <- parse_data(id = "1", paths = path) names(data) head(data$cgm)
Draws a two-panel plot per day. The upper panel shows the CGM trace over a shaded target range, with the SMBG measurements as diamonds; the lower panel shows the basal rate as a filled step area, the boluses as labelled circles and the carbohydrates as labelled boxes, headed by the insulin delivered over the whole day. Both panels share a 0-24 h axis of the local (wall-clock) time, and a legend is drawn underneath them.
plot_day( data, id = NULL, days = NULL, target = NULL, ylim_cgm = NULL, min_bolus = 0, units = "mg/dl", main = NULL )plot_day( data, id = NULL, days = NULL, target = NULL, ylim_cgm = NULL, min_bolus = 0, units = "mg/dl", main = NULL )
data |
A list with all data found for one individual (the output of
|
id |
Character or numeric participant identifier, naming the individual
to plot. Can be omitted if |
days |
The day(s) to plot, as a |
target |
Numeric vector of length two with the glucose target range, drawn as a shaded band. |
ylim_cgm |
Numeric vector of length two with the limits of the glucose axis. |
min_bolus |
Numeric, the smallest bolus that is drawn in full, in units.
Small correction boluses crowd the panel without adding much, and
|
units |
Character string, the units of the glucose data, either
|
main |
Character string with the plot title. Defaults to the id and the day. Note that the same title is used for every day if several are plotted. |
The daily totals are taken from total_basal and total_bolus
where the export provides them, and are summed over the day otherwise. They
therefore cover insulin that the drawn data may not, such as the basal
delivered in automated mode by an Omnipod, and they are unaffected by
min_bolus. A total that is not recorded at all is shown as "-".
Where min_bolus leaves boluses out of the labelled circles, a note next
to the totals says so.
No return value, called for side effects: one plot is drawn per day.
path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) plot_day(data, days = "2026-06-22") # Custom target range, with boluses below 1 U drawn as ticks only plot_day(data, days = "2026-06-22", target = c(70, 140), min_bolus = 1)path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) plot_day(data, days = "2026-06-22") # Custom target range, with boluses below 1 U drawn as ticks only plot_day(data, days = "2026-06-22", target = c(70, 140), min_bolus = 1)
Rows can be removed while cleaning (e.g. merged slowly absorbed
carbohydrates in clean.glooko) and by filters (e.g.
overwritten carbohydrate entries in filter_carbs).
removed_entries(data_id)removed_entries(data_id)
data_id |
A list with all data found for one individual. |
A named list with one data frame per data type, holding the removed
rows and the column removed_reason. NULL if nothing was
removed.
path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- filter_carbs(parse_data(id = "1", paths = path)) removed_entries(data)path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- filter_carbs(parse_data(id = "1", paths = path)) removed_entries(data)
Accepts either the output of parse_data for a single
individual or the output of merge_all for
multiple individuals. One JSON file following the
DIAX
specification is written per individual.
write_DIAX_format(data, prefix, ids = NULL)write_DIAX_format(data, prefix, ids = NULL)
data |
A list with data of one individual (output of |
prefix |
Character string prepended to the id to build each output
filename, e.g. |
ids |
Character or numeric vector with the individual(s) to write.
Defaults to all ids found in |
No return value, called for side effects: one JSON file is written per id.
path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) out_dir <- tempfile("diax_") dir.create(out_dir) write_DIAX_format(data, prefix = file.path(out_dir, "id_")) list.files(out_dir) unlink(out_dir, recursive = TRUE)path <- system.file("extdata", "glooko_example.zip", package = "aidR") data <- parse_data(id = "1", paths = path) out_dir <- tempfile("diax_") dir.create(out_dir) write_DIAX_format(data, prefix = file.path(out_dir, "id_")) list.files(out_dir) unlink(out_dir, recursive = TRUE)