The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
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[@]metanet.ch.
Data exported from automated insulin delivery (AID) systems are highly heterogeneous: each platform uses its own file format, variable names, data types, and units. The aidR package provides (i) functions to read exports of the most prevalent AID platforms, (ii) functions to standardize them into tidy, platform-independent formats for continuous glucose monitoring (CGM), basal insulin, bolus insulin, carbohydrate data, and self-monitored blood glucose (SMBG) data, and (iii) functions for visualization and conversion to other standardized formats. As a result, a downstream analysis can be written once and applied consistently across data from different AID systems.
The package supports the following steps:
parse_data(clean = FALSE)).parse_data(clean = TRUE)).filter_CGM()) or overwritten
carbohydrate entries (filter_carbs()).check_completeness(),
check_completeness_range()), and verify that the insulin
recorded in the individual data points matches the daily totals reported
by the export (check_insulin_totals()).plot_day()).merge_all()).The rest of this vignette walks through each step, describes the supported export formats, and shows how to filter, check, plot, and convert the resulting data.
aidR uses the content of each file to determine which
platform it came from; it does not use the file name. You can therefore
drop files or folders from several subjects and several platforms into
one folder, and aidR will dispatch every file to the right reader
automatically. Files with irrelevant extensions (e.g. .png,
.pdf, .jpg, .jpeg,
.txt, .json, .html) are silently
ignored.
aidR currently supports seven export formats, each described in more detail on its own page in this documentation:
| Platform | Typical export | Supported AID systems (not exhaustive) |
|---|---|---|
| Glooko | folder with one CSV per data type | CamAPS FX, Omnipod, Tandem Control-IQ, … |
| CareLink (Medtronic) | single .xlsx or .csv export |
MiniMed 780G, … |
| mylife (Ypsomed) | single semicolon-delimited CSV | CamAPS FX |
| Tidepool | single multi-sheet .xlsx workbook |
Tandem Control-IQ, open-source DIY (e.g. Loop, AAPS), … |
| YourLoops (Diabeloop) | folder with one CSV per data type | DBLG1, DBLG2 |
| Tandem Source | folder with one CSV per data type | Tandem Control-IQ |
| Dexcom Clarity | single delimited CSV (CGM only) | Dexcom G6, G7, … (any AID system using a Dexcom sensor) |
The main function of the package is parse_data(), which
expects a subject ID and the path(s) to the exported data (either
relative, e.g. ./exports/my_export_file.csv, or absolute,
e.g. ~/Downloads/exports/my_export_file.csv):
If several exports are available for one subject, e.g. for different
time ranges or from different AID platforms, paths can be a
vector holding all of the file names:
data <- parse_data(id = "1", paths = c("./exports/my_export_file_id_1_month_1.csv",
"./exports/my_export_file_id_1_month_2.csv",
"./exports/my_export_file_id_1_month_3.csv"
)
)A path may point to a single file (e.g. a Tidepool or CareLink export), a folder (e.g. a Glooko or YourLoops export), or a zip archive. aidR detects this automatically, reads every file it finds within the path, and merges them into a single named list for that subject, with one entry per data type.
In the following example, we set clean = FALSE. This
reads all of the files present in the export and applies minimal
standardization (e.g. formatting timestamps), but performs no further
cleaning or tidying. This is useful for (i) inspecting non-standard data
types (e.g. alarms or pump settings rather than CGM), or (ii) simply
reading the data of a single subject, where merging across platforms
does not matter.
path <- system.file("extdata", "tidepool_example.xlsx", package = "aidR")
data_raw <- parse_data(id = "1", paths = path, clean = FALSE)
#> Parsing data for id 1...
str(data_raw, max.level = 1)
#> List of 11
#> $ cgm : tibble [36,873 × 14] (S3: tbl_df/tbl/data.frame)
#> $ basal : tibble [2,392 × 22] (S3: tbl_df/tbl/data.frame)
#> $ bolus : tibble [595 × 19] (S3: tbl_df/tbl/data.frame)
#> $ device_event : tibble [1,224 × 28] (S3: tbl_df/tbl/data.frame)
#> $ bolus_calculator : tibble [587 × 26] (S3: tbl_df/tbl/data.frame)
#> $ basal_schedules : tibble [330 × 22] (S3: tbl_df/tbl/data.frame)
#> $ bg_targets : tibble [330 × 27] (S3: tbl_df/tbl/data.frame)
#> $ carb_ratios : tibble [330 × 24] (S3: tbl_df/tbl/data.frame)
#> $ insulin_sensitivities: tibble [330 × 24] (S3: tbl_df/tbl/data.frame)
#> $ SMBG : tibble [646 × 15] (S3: tbl_df/tbl/data.frame)
#> $ upload : tibble [30 × 20] (S3: tbl_df/tbl/data.frame)Here, path happens to be a single Tidepool export, so
data_raw contains the standard cgm,
basal, bolus, and SMBG data
frames alongside Tidepool-specific extras (device_event,
bolus_calculator, basal_schedules, …).
If a subject’s data spans several files (i.e. paths is a
vector of length > 1), parse_data() merges all of them
together by data type and sorts each resulting data frame by
timestamp. Duplicate rows within a data type are removed
(while ignoring the format column to also filter data
downloaded from two different platforms), see Merging multiple exports
of one subject.
Let us re-run the example above, but now with
clean = TRUE. This is in fact the default, so the argument
can be omitted.
path <- system.file("extdata", "tidepool_example.xlsx", package = "aidR")
data_clean <- parse_data(id = "1", paths = path, clean = TRUE)
#> Parsing data for id 1...
str(data_clean, max.level = 1)
#> List of 7
#> $ cgm : tibble [36,873 × 7] (S3: tbl_df/tbl/data.frame)
#> $ basal : tibble [2,389 × 8] (S3: tbl_df/tbl/data.frame)
#> $ SMBG : tibble [646 × 7] (S3: tbl_df/tbl/data.frame)
#> $ bolus : tibble [595 × 12] (S3: tbl_df/tbl/data.frame)
#> $ carbs : tibble [427 × 10] (S3: tbl_df/tbl/data.frame)
#> $ total_basal:'data.frame': 137 obs. of 5 variables:
#> $ total_bolus:'data.frame': 137 obs. of 5 variables:The additional platform-specific data types from before
(e.g. device_event, bolus_calculator) have
disappeared. Instead, the major data types (CGM, basal, bolus,
carbohydrates, and SMBG) are standardized to a common column layout.
All five start with the same four columns:
id: the subject’s id, as passed to
parse_data().format: the export format this row was parsed from, one
of glooko, carelink, mylife,
tidepool, yourloops,
tandem_source or dexcom_clarity.timestamp: the time of the data point
(POSIXct), in local (wall-clock) time.timezone_offset: the offset of that local time from
UTC, in hours (NA if the export does not report it). UTC
time is therefore
timestamp - lubridate::hours(timezone_offset).Rows are sorted by timestamp, and the columns that
follow are specific to the data type:
cgm: CGM values, one row per sensor
reading
value: the measured CGM value, always in mg/dL
(readings exported in mmol/L are converted).unit: the unit of value (always
mg/dL).sensor_name: the name of the CGM sensor,
NA if not available.basal: delivered basal rates, one row
per interval over which the rate was constant
duration: the duration of the basal rate, in hours,
starting at timestamp.rate: the delivered basal rate, in U/h. The insulin
delivered over the interval is rate * duration.unit: the unit of rate (always
U/h).pump_name: the name of the pump, NA if not
available.bolus: bolus information, one row per
bolus
type: the type of the bolus. Either normal
(delivered in one go), square_wave (delivered gradually and
evenly over a longer period), dual_wave (part delivered
immediately, the rest gradually over a longer period),
biphasic (YourLoops only, a bolus split into two boluses,
separated by a period of time; each part is its own row), or
pen (YourLoops only).total: the total amount of insulin in the bolus
(normal + extended), in U.normal: the portion delivered immediately, in U
(applicable for normal, dual_wave and
biphasic; 0 for
square_wave).extended: the portion delivered gradually over a longer
period, in U (applicable for dual_wave and
square_wave, 0 otherwise).unit: the unit of total,
normal and extended (always
U).duration_extended: the duration of the extended
portion, in hours (for dual_wave and
square_wave, NA otherwise, and also
NA where the platform does not report it).pump_name: the name of the pump, NA if not
available.link_id: an identifier linking the bolus to the
carbohydrate entries that triggered it, NA if not available
(see Linking boluses and
carbohydrates).carbs: user-reported carbohydrate
consumption, one row per entry
value: the carbohydrate value (in g).unit: the unit of value (always
g).label: an optional label for the entry, NA
if not available. Its content is platform-specific: it may be a meal
name entered by the user, or summarize additional information on the
entry (e.g. that it was a fatty meal).estimated_absorption_duration: the absorption duration
in hours, as estimated by the user, NA if not
available.is_hypo_treatment: a logical flagging entries labeled
as hypoglycemia treatment, NA if not available.link_id: an identifier linking the carbohydrate entry
to the bolus it triggered, NA if not available (see Linking boluses and
carbohydrates).SMBG: self-monitored blood glucose,
one row per measurement
value: the measured SMBG value, always in mg/dL
(readings exported in mmol/L are converted).unit: the unit of value (always
mg/dL).sensor_name: the name of the SMBG device,
NA if not available.This layout is the same for all platforms and can therefore be merged across subjects.
In addition, two data types hold the daily insulin totals, with one row per day rather than one row per data point:
total_basal: id,
format, date, total_basal, and
sourcetotal_bolus: id,
format, date, total_bolus, and
sourcetotal_basal and total_bolus give the
insulin delivered on that day, in units. Where a platform reports a
daily aggregate itself, that value is used and source is
"reported"; otherwise the total is summed over the cleaned
basal (as rate * duration) or
bolus data and source is
"summed":
| Format | total_basal |
total_bolus |
|---|---|---|
| Glooko | reported | reported |
| Tandem Source | reported | summed |
| CareLink | - | summed |
| Tidepool | summed | summed |
| YourLoops | summed | summed |
| mylife | - | summed |
| Dexcom Clarity | - | - |
While the summed total merely reflects the underlying records, the reported total can indicate whether those records are complete. This matters in particular for Omnipod users on Glooko: the exported basal field contains only rates delivered in manual mode, omitting those delivered in automated mode, while the reported daily total covers both. Comparing the two therefore provides a check on the completeness of the export.
Note that there is no total_basal for mylife, since a
mylife export contains no basal data at all, and none for CareLink,
since a CareLink export reports only the scheduled basal rates
rather than the basal insulin effectively delivered. For the same
reason, neither platform yields a cleaned basal data type.
A Dexcom Clarity export carries neither total_basal nor
total_bolus, as it is a CGM export and contains no insulin
data at all.
If paths holds several exports,
parse_data() merges them per data type and sorts each data
frame by timestamp. However, exports may overlap, for
example for consecutive downloads covering the same days, or the same
pump uploaded to two platforms. aidR therefore removes duplicates in
three steps:
format are kept once (the first occurrence).
Rows where all measurement columns are NA are dropped.total_basal or total_bolus (e.g. a partial day
at the edge of one export and the full day in another), the largest
value is kept, as it comes from the export covering the full day.carbs,
bolus, SMBG, and basal, two rows
are considered near-duplicates if they
unit,basal),
andcarbs), 0.05 U
(total of bolus), 1 mg/dL (SMBG),
or 0.05 U/h (rate of basal).basal rows must therefore also have the
same duration (within 1 minute); rows with a missing
duration are never considered near-duplicates. Of each
duplicate pair, the row with a link_id is kept to maintain
the link of carbohydrate and bolus - if none of the two rows contains a
link_id, the row with more non-missing columns is kept.
Missing columns of the kept row (e.g. a label) are filled
from the removed row, in which case format combines both
export platforms to indicate that information is pulled from two sources
(e.g. "glooko/tidepool"). The removed rows can be inspected
with removed_entries().Near-duplicates are not detected if two exports are shifted in time, e.g. due to daylight saving time or differing time zone settings. aidR therefore warns if the entries of two formats match much better when shifted by one hour, but leaves the data unchanged.
Since the cleaned data is split by data type, a bolus and the
carbohydrates entered for it in the bolus calculator end up in two
different data frames. For exports that are informative about which
carbohydrate entry triggered which bolus, aidR keeps this
information in the column link_id of both
bolus and carbs: rows sharing a
link_id belong to the same event. This is the case for
Bolus Calculator sheet, where the Bolus column
refers to the Id of the bolus in the Bolus
sheet.For all other platforms (CareLink, mylife, YourLoops), and for
Tidepool exports with a Food sheet, link_id is
NA. The same goes for boluses delivered without entering
carbohydrates. While it would be possible to match bolus and
carbohydrate entries for these platforms using timestamps, aidR
currently does not implement this.
CamAPS FX lets users flag a meal as slowly absorbed (over 3 to 4 hours). Although this is a single meal, Glooko exports encode it as a series of equal-sized carbohydrate entries, one every 30 minutes, possibly with unrelated entries in between. For example:
"","Zeitstempel","Insulin.Typ","Blutzuckereingabe..mg.dl.","Kohlenhydrataufnahme..g.","Kohlenhydratverhältnis","Abgegebenes.Insulin..E.","Anfängliche.Abgabe..E.","Verzögerte.Abgabe..E.","Seriennummer"
"1",2026-05-19 05:33:00,"Normal",0,21,NA,2.4,NA,NA,"CamAPS mylife YpsoPump"
"2",2026-05-19 05:34:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"3",2026-05-19 06:04:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"4",2026-05-19 06:34:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"5",2026-05-19 07:04:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"6",2026-05-19 07:34:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"7",2026-05-19 08:04:00,"Normal",0,1.2,NA,0,NA,NA,"CamAPS mylife YpsoPump"
"8",2026-05-19 14:18:00,"Normal",0,32,NA,2.9,NA,NA,"CamAPS mylife YpsoPump"
"9",2026-05-19 17:53:00,"Normal",0,27,NA,2.5,NA,NA,"CamAPS mylife YpsoPump"
Here, the user entered 7 g of slowly absorbed carbohydrates at 05:34 with an absorption time of 3 hours. The Glooko export splits this into six entries of 7/6 \(\approx\) 1.2 g, spaced 30 minutes apart.
To stay consistent with platforms that do not split slowly absorbed
carbohydrates, aidR merges these entries back into a single entry and
records the absorption time in the column
estimated_absorption_duration. The merged entry keeps the
timestamp of the first entry, holds the summed carbohydrate amount, and
gets an estimated_absorption_duration of 0.5 hours per
merged entry (3 hours in this example). The remaining entries and their
associated zero-unit boluses are removed; you can inspect them with
removed_entries().
To turn off merging, set merge_slow_carbs = FALSE in
parse_data():
AID data is often noisy and may require additional filtering. While we encourage users to inspect the parsed data for platform-specific issues, aidR implements a few filters that we have found particularly useful. CGM data in particular is frequently affected in two ways: individual values may be physiologically implausible (below or above the range a sensor can measure), and the same period may be covered by two overlapping data streams whose timestamps are shifted by a few seconds or minutes but that otherwise carry the same signal (often the case for DIY open-source systems).
filter_CGM() addresses both. It first truncates values
to a plausible range, clamping anything below
truncate_min_cgm (40 mg/dL by default) up to that value and
anything above truncate_max_cgm (400 mg/dL by default) down
to it. It then downsamples the CGM series to a regular time grid
(5-minute bins by default): each reading is assigned to a time bin, and
within each bin only the reading closest to the bin’s start is kept. Two
streams that differ only by a shift of a few seconds or minutes
therefore fall into the same bins and collapse to a single value, so a
duplicated stream is removed rather than doubling the number of readings
for that period.
nrow(data_clean$cgm)
#> [1] 36873
data_clean_filter <- filter_CGM(data_clean)
nrow(data_clean_filter$cgm)
#> [1] 36702The two truncation bounds and the downsampling interval can each be
adjusted or disabled: truncate_min_cgm (default 40 mg/dL),
truncate_max_cgm (default 400 mg/dL), and
min_minutes_difference_cgm (default 5 minutes); set any of
them to NULL to skip that particular step. Only CGM is
filtered, and only once it has been cleaned (i.e. after
parse_data(clean = TRUE)); all other data types are
returned unchanged.
Raw data sometimes contains a pattern like this:
"","Zeitstempel","Insulin.Typ","Blutzuckereingabe..mg.dl.","Kohlenhydrataufnahme..g.","Kohlenhydratverhältnis","Abgegebenes.Insulin..E.","Anfängliche.Abgabe..E.","Verzögerte.Abgabe..E.","Seriennummer"
"1",2026-06-21 17:42:00,"Normal",353,0,NA,2.44,NA,NA,"XXX"
"2",2026-06-21 18:33:00,"Normal",0,403,NA,0,NA,NA,"XXX"
"3",2026-06-21 18:34:00,"Normal",0,40,5,8,NA,NA,"XXX"
"4",2026-06-21 20:29:00,"Normal",0,0,5,5,NA,NA,"XXX"
"5",2026-06-21 21:37:00,"Normal",381,0,NA,1.157,NA,NA,"XXX"
Here, the user apparently entered 403 g of carbohydrates at 18:33 by
mistake. The corresponding bolus was cancelled (the delivered bolus is
zero), and the correct amount was then entered with a new bolus (40g at
18:34). Some platforms, most notably Glooko, export all of these
entries, so the same meal would be counted more than once.
filter_carbs() detects and removes such cases.
Specifically, it removes a carbohydrate entry if
link_id (see
Linking boluses and
carbohydrates), so only entries linked to a bolus can be removed.
For platforms without links (CareLink, mylife, YourLoops, Tidepool with
a Food sheet), nothing is removed;link_id) lies at most max_minutes_overwrite
minutes (2 minutes by default) before or after it. The order of the two
entries does not matter, and the bolus of the other entry is not
checked;The zero bolus of a removed entry is removed from the
bolus data as well. All removed rows can be inspected with
removed_entries(), together with the reason and, for
carbohydrates, the entry that overwrote them
(matched_to_timestamp, matched_to_value):
path <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_clean_2 <- parse_data(id = "1", paths = path, clean = TRUE)
#> Parsing data for id 1...
data_clean_filter_2 <- filter_carbs(data_clean_2)
removed_entries(data_clean_filter_2)
#> $carbs
#> id format timestamp timezone_offset value unit label
#> 1 1 glooko 2026-06-21 22:19:00 NA 403 g NA
#> estimated_absorption_duration is_hypo_treatment
#> 1 NA NA
#> link_id matched_to_timestamp matched_to_value
#> 1 1|2026-06-21 22:19:00|403|0|NA|NA|1 2026-06-21 22:20:00 40
#> removed_reason
#> 1 overwritten carbohydrate entry
#>
#> $bolus
#> id format timestamp timezone_offset type total normal extended
#> 1 1 glooko 2026-06-21 22:19:00 NA normal 0 0 0
#> unit duration_extended pump_name link_id
#> 1 U NA NA 1|2026-06-21 22:19:00|403|0|NA|NA|1
#> removed_reason
#> 1 zero bolus of overwritten carbohydrate entryBefore running an analysis, it is often useful to verify that a
subject actually has data of every type you need, and that it is not
missing entire days. check_completeness() reports if a data
type is absent, or present but empty:
check_completeness_range() goes a step further and
checks, for a given date range, which specific days have no data at all
for each type. This is useful for spotting gaps such as a sensor or pump
disconnecting for a few days, or users that exported data from a wrong
time range:
check_completeness_range(
data_clean,
first_date = "2019-01-10",
last_date = "2019-05-31"
)
#> Id 1: No data of type 'cgm' found for days 2019-04-15 - 2019-04-19, 2019-05-21!
#> Id 1: No data of type 'basal' found for days 2019-04-15 - 2019-04-19!
#> Id 1: No data of type 'bolus' found for days 2019-04-15 - 2019-04-19!
#> Id 1: No data of type 'carbs' found for days 2019-04-15 - 2019-04-19!In this case, it appears that there is a gap in all data types spanning from 2019-04-15 until 2019-04-19, and additionally no CGM data for 2019-05-21.
Both functions accept a necessary_types argument to
restrict (or extend) which data types are checked. They also accept the
data of several subjects at once (the output of
merge_all(), see below), in which case every subject is
checked separately and the messages name the id they refer to.
check_insulin_totals() asks whether the reported insulin
adds up: it compares the insulin summed over the individual
basal entries against the daily totals in
total_basal, and prints a message for every day where the
two disagree. Per default, only the basal insulin is checked - the
argument types extends the check to the bolus insulin
(types = c("basal", "bolus"), comparing bolus
against total_bolus).
This is only informative where the daily total was reported by the
export itself, i.e. where source == "reported". Where
aidR summed the total itself
(source == "summed"), the two agree by construction and are
skipped. In practice, this means the check is useful for Glooko (basal
and bolus) and for Tandem Source (basal only).
A mismatch means the individual data points do not cover the whole day. The Glooko example export shows the typical picture for an Omnipod user:
glooko_path <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_glooko <- parse_data(id = "1", paths = glooko_path)
#> Parsing data for id 1...
check_insulin_totals(data_glooko)
#> Id 1: The summed basal insulin does not match the reported 'total_basal' for day 2026-06-22!The message names the days that disagree. As with the completeness checks, the data of several subjects can be passed at once, and each is then checked separately. To see how far they disagree, use the comparison that the function returns invisibly - here for both insulin types:
totals <- suppressMessages(
check_insulin_totals(data_glooko, types = c("basal", "bolus"))
)
totals
#> id type date summed reported within_tolerance
#> 1 1 basal 2026-06-22 0.08166667 21.20 FALSE
#> 2 1 bolus 2026-06-22 16.25000000 16.25 TRUEThe bolus totals agree exactly, while the summed basal is a small
fraction of the reported one (see column within_tolerance
which types and dates fail the comparison). This is the signature of an
Omnipod user: the export records the basal rates delivered in manual
mode only, so almost all of the basal insulin - everything delivered in
automated mode - is missing from basal, but is still
included in total_basal. For such subjects, an analysis
based on basal would largely understate the insulin
actually delivered, whereas total_basal remains
correct.
plot_day() plots one day of a subject’s data as two
stacked panels: the CGM trace over the shaded target range with the SMBG
measurements as diamonds, and the insulin and carbohydrate information
below it - the basal rate as a filled step area, the boluses as labelled
circles, and the carbohydrates as labelled boxes, with the insulin
delivered over the whole day given on top:
The argument
days may take several days at once. Per
default, every day that has CGM data is plotted (one plot per day).
Glucose units can be specified with the argument units
as "mg/dl" (default) or "mmol/l".
A data type (except CGM) that is absent is simply left out, so the
function also works for exports that carry no basal data at all, such as
CareLink. If many small boluses were delivered, this can be hard to read
- in this case, the argument min_bolus can be used to
de-clutter the plot: boluses below min_bolus are then only
marked by a small tick at their time, without a circle and without their
value, and a note next to the daily totals says so.
F Once every subject’s data has been parsed and cleaned,
merge_all() combines them into one long-format data set per
data type:
path_2 <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_2 <- parse_data(id = "2", paths = path_2) # Note: clean = TRUE by default
#> Parsing data for id 2...
all_data <- list("1" = data_clean, "2" = data_2)
combined <- merge_all(all_data)
str(combined, max.level = 1)
#> List of 7
#> $ cgm : tibble [37,091 × 7] (S3: tbl_df/tbl/data.frame)
#> $ basal : tibble [2,390 × 8] (S3: tbl_df/tbl/data.frame)
#> $ bolus : tibble [603 × 12] (S3: tbl_df/tbl/data.frame)
#> $ carbs : tibble [435 × 10] (S3: tbl_df/tbl/data.frame)
#> $ SMBG : tibble [647 × 7] (S3: tbl_df/tbl/data.frame)
#> $ total_basal:'data.frame': 138 obs. of 5 variables:
#> $ total_bolus:'data.frame': 138 obs. of 5 variables:
head(combined$cgm)
#> # A tibble: 6 × 7
#> id format timestamp timezone_offset value unit sensor_name
#> <chr> <chr> <dttm> <dbl> <dbl> <chr> <lgl>
#> 1 1 tidepool 2019-01-10 01:01:41 -5 120 mg/dL NA
#> 2 1 tidepool 2019-01-10 01:06:41 -5 120 mg/dL NA
#> 3 1 tidepool 2019-01-10 01:11:40 -5 118 mg/dL NA
#> 4 1 tidepool 2019-01-10 01:16:40 -5 116 mg/dL NA
#> 5 1 tidepool 2019-01-10 01:21:40 -5 113 mg/dL NA
#> 6 1 tidepool 2019-01-10 01:26:39 -5 112 mg/dL NA
tail(combined$cgm)
#> # A tibble: 6 × 7
#> id format timestamp timezone_offset value unit sensor_name
#> <chr> <chr> <dttm> <dbl> <dbl> <chr> <lgl>
#> 1 2 glooko 2026-06-22 23:30:00 NA 233 mg/dL NA
#> 2 2 glooko 2026-06-22 23:35:00 NA 229 mg/dL NA
#> 3 2 glooko 2026-06-22 23:40:00 NA 225 mg/dL NA
#> 4 2 glooko 2026-06-22 23:45:00 NA 222 mg/dL NA
#> 5 2 glooko 2026-06-22 23:50:00 NA 220 mg/dL NA
#> 6 2 glooko 2026-06-22 23:55:00 NA 215 mg/dL NABy default, merge_all() returns the five core types
(cgm, basal, bolus,
carbs, SMBG), as well as
total_basal and total_bolus; a subject missing
one of them simply contributes no rows to that type. The
types argument restricts which data types are merged.
Three helper functions convert the standardized data into the formats expected by other tools.
get_iglu_format() reshapes the cgm data
frame into the id/time/gl layout
expected by the iglu
package, so that CGM metrics and visualizations from iglu can be used
directly:
iglu_1 <- get_iglu_format(data_clean)
head(iglu_1)
# A tibble: 6 × 3
id time gl
<int> <dttm> <dbl>
1 1 2019-01-10 01:01:41 120
2 1 2019-01-10 01:06:41 120
3 1 2019-01-10 01:11:40 118
4 1 2019-01-10 01:16:40 116
5 1 2019-01-10 01:21:40 113
6 1 2019-01-10 01:26:39 112
iglu::plot_agp(iglu_1)get_cgmquantify_format() builds the data frame that the
metric functions of the cgmquantify
package 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, so
the result can be handed to any cgmquantify function directly and no
intermediate file has to be written:
cgmq_1 <- get_cgmquantify_format(data_clean)
head(cgmq_1)
Time glucose Date time_of_day type_of_event
1 2019-01-10 01:01:41 120 2019-01-10 01:01:41 0
2 2019-01-10 01:06:41 120 2019-01-10 01:06:41 0
3 2019-01-10 01:11:40 118 2019-01-10 01:11:40 0
4 2019-01-10 01:16:40 116 2019-01-10 01:16:40 0
5 2019-01-10 01:21:40 113 2019-01-10 01:21:40 0
6 2019-01-10 01:26:39 112 2019-01-10 01:26:39 0
cgmquantify::interdaycv(cgmq_1)
[1] 36.77765
cgmquantify::J_index(cgmq_1)
[1] 50.83163Note that cgmquantify applies to a single individual only, and
get_cgmquantify_format() requires the id
argument as soon as data holds more than one
id.
write_DIAX_format() writes CGM, basal, bolus, carbs, and
SMBG data to one JSON file per subject, following the DIAX
specification. It accepts either the output of parse_data()
for a single subject or the output of merge_all() for
several, and writes one file per id to
paste0(prefix, id, ".json"):
A typical study-level pipeline combines all of the above for every subject in a loop:
# Define a data frame: first column is id, second column the export paths
# An id may appear multiple times if multiple exports (e.g. across many months) are available
# Note: filenames are not used to determine the file type and/or id
input <- data.frame(id = c("1", "1", "1", "2", "3", "3", "4", "5"),
paths = c(
"./tidepool_id_1_period_1.xlsx",
"./tidepool_id_1_period_2.xlsx",
"./tidepool_id_1_period_3.xlsx",
"./glooko_period_1.zip",
"./yourloops_1.zip",
"./yourloops_2.zip",
"./tidepool_id_4_period_1.xlsx",
"./carelink.csv"
)
)
ids <- unique(input$id)
all_data <- list()
options(warn = 1) # warn immediately
for (id in ids) {
all_data[[id]] <- parse_data(id, input$paths[input$id == id])
all_data[[id]] <- filter_CGM(all_data[[id]])
all_data[[id]] <- filter_carbs(all_data[[id]])
check_completeness(all_data[[id]])
check_insulin_totals(all_data[[id]])
}
combined <- merge_all(all_data)
# Inspect a single day of a single subject
plot_day(combined, id = "1", days = "2026-06-22")
# write DIAX files
diax_dir <- "./diax"
write_DIAX_format(combined, prefix = file.path(diax_dir, "diax_id_"))
# Plot using iglu
iglu_combined <- get_iglu_format(combined)
iglu::plot_glu(iglu_combined, subjects = "2")combined can then be filtered, summarized, converted to
other formats, or exported for statistical analysis exactly as shown in
the previous section.
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.