---
title: "Introduction to aidR"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to aidR}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```
```{r setup, message = FALSE, include = FALSE}
library(aidR)
library(dplyr)
```
## Overview

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:

- **Read** the native exports belonging to one subject (`parse_data(clean = FALSE)`).
- **Clean** the most important data types (CGM, basal, bolus, carbohydrate, and SMBG) into an interchangeable format (`parse_data(clean = TRUE)`).
- **Filter** the merged data, e.g. to remove artefactual or duplicated CGM readings (`filter_CGM()`) or overwritten carbohydrate entries (`filter_carbs()`).
- **Check completeness** of the data, e.g. to flag missing data types or missing days (`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** daily data of one subject, with CGM, basal, bolus, carbohydrate, and SMBG data in one figure (`plot_day()`).
- **Merge data across subjects** into one long-format data set for the whole population (`merge_all()`).
- **Convert** the standardized data into other formats (e.g. [DIAX](https://github.com/Center-for-Diabetes-Technology/DIAX)) and into the layout expected by other R packages (e.g. [iglu](https://irinagain.github.io/iglu/) and [cgmquantify](https://CRAN.R-project.org/package=cgmquantify)).

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.

## Installation

You can install aidR from CRAN with:
```{r, eval = FALSE}
install.packages("aidR")
library(aidR)
```

## Supported export formats

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](format-glooko.html)                       | folder with one CSV per data type    | CamAPS FX, Omnipod, Tandem Control-IQ, ...                    |
| [CareLink (Medtronic)](format-carelink.html)       | single `.xlsx` or `.csv` export      | MiniMed 780G, ...                                            |
| [mylife (Ypsomed)](format-mylife.html)             | single semicolon-delimited CSV       | CamAPS FX                                                    |
| [Tidepool](format-tidepool.html)                   | single multi-sheet `.xlsx` workbook  | Tandem Control-IQ, open-source DIY (e.g. Loop, AAPS), ...    |
| [YourLoops (Diabeloop)](format-yourloops.html)     | folder with one CSV per data type    | DBLG1, DBLG2                                                 |
| [Tandem Source](format-tandem-source.html)         | folder with one CSV per data type    | Tandem Control-IQ                                            |
| [Dexcom Clarity](format-dexcom-clarity.html)       | single delimited CSV (CGM only)      | Dexcom G6, G7, ... (any AID system using a Dexcom sensor)    |

## Reading and cleaning data for one subject

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`):
```{r, eval=FALSE}
data <- parse_data(id = "1", paths = "./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:
```{r, eval=FALSE}
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.

### Without cleaning

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.
```{r}
path <- system.file("extdata", "tidepool_example.xlsx", package = "aidR")
data_raw <- parse_data(id = "1", paths = path, clean = FALSE)
str(data_raw, max.level = 1)
```
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](#merging-multiple-exports-of-one-subject).

### With cleaning

Let us re-run the example above, but now with `clean = TRUE`. This is in fact the default, so the argument can be omitted.
```{r}
path <- system.file("extdata", "tidepool_example.xlsx", package = "aidR")
data_clean <- parse_data(id = "1", paths = path, clean = TRUE)
str(data_clean, max.level = 1)
```
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](#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](#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 `source`
- **`total_bolus`**: `id`, `format`, `date`, `total_bolus`, and `source`

`total_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.

### Merging multiple exports of one subject

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:

1. **Exact duplicates**: rows that are identical in all columns except `format` are kept once (the first occurrence). Rows where all measurement columns are `NA` are dropped.
2. **Daily totals**: if a day appears more than once in `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.
3. **Near-duplicates** (cleaned data only): different platforms may round timestamps or values differently, so the same event is not always exported identically. For `carbs`, `bolus`, `SMBG`, and `basal`, two rows are considered near-duplicates if they
    - come from different formats,
    - have the same `unit`,
    - are at most 5 minutes apart (2 minutes for `basal`), and
    - differ in value by at most 1 g (`carbs`), 0.05 U (`total` of `bolus`), 1 mg/dL (`SMBG`), or 0.05 U/h (`rate` of `basal`).

    Since basal rates change frequently, neighbouring basal rows often have similar rates. Two `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.

### Linking boluses and carbohydrates

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

- **Glooko** and **Tandem Source**, where the bolus files hold the bolus and the entered carbohydrates in the same row, and
- **Tidepool** exports with a `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.

### Slowly absorbed carbohydrates in Glooko

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:

```{r, echo = FALSE, comment = ""}
path <- system.file("extdata", "example_slowly_absorbed_carbs_glooko.csv", package = "aidR")
cat(readLines(path, n = 15), sep = "\n")
```

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()`:

```{r, eval = FALSE}
data <- parse_data(id = "1", paths = path, merge_slow_carbs = FALSE)
```

## Filtering

### CGM

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.
```{r}
nrow(data_clean$cgm)
data_clean_filter <- filter_CGM(data_clean)
nrow(data_clean_filter$cgm)
```
The 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.

### Carbohydrates

Raw data sometimes contains a pattern like this:

```{r, echo = FALSE, comment = ""}
path <- system.file("extdata", "example_carb_override_glooko.csv", package = "aidR")
cat(readLines(path, n = 15), sep = "\n")
```

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

- its bolus is zero. The bolus is found via `link_id` (see [Linking boluses and carbohydrates](#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;
- another carbohydrate entry of the same format (and with a different `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;
- it is not flagged as a hypo treatment or as slowly absorbed carbohydrates.

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`):
```{r}
path <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_clean_2 <- parse_data(id = "1", paths = path, clean = TRUE)
data_clean_filter_2 <- filter_carbs(data_clean_2)
removed_entries(data_clean_filter_2)
```

## Checking data completeness

Before 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:
```{r}
check_completeness(data_clean)
```
`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:
```{r}
check_completeness_range(
  data_clean,
  first_date = "2019-01-10",
  last_date  = "2019-05-31"
)
```
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.

## Checking the daily insulin totals

`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:
```{r}
glooko_path <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_glooko <- parse_data(id = "1", paths = glooko_path)
check_insulin_totals(data_glooko)
```
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:
```{r}
totals <- suppressMessages(
  check_insulin_totals(data_glooko, types = c("basal", "bolus"))
)
totals
```
The 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.

## Plotting single days

`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:
```{r, fig.width=9, fig.height=6.5, out.width="100%"}
plot_day(data_clean, id = "1", days = "2019-04-01")
```
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.

## Merging data across subjects
F
Once every subject's data has been parsed and cleaned, `merge_all()` combines them into one long-format data set per data type:
```{r}
path_2 <- system.file("extdata", "glooko_example.zip", package = "aidR")
data_2  <- parse_data(id = "2", paths = path_2) # Note: clean = TRUE by default

all_data <- list("1" = data_clean, "2" = data_2)

combined <- merge_all(all_data)

str(combined, max.level = 1)
head(combined$cgm)
tail(combined$cgm)
```
By 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.

## Converting to other formats

Three helper functions convert the standardized data into the formats expected by other tools.

### Iglu

`get_iglu_format()` reshapes the `cgm` data frame into the `id`/`time`/`gl` layout expected by the [iglu](https://irinagain.github.io/iglu/) package, so that CGM metrics and visualizations from iglu can be used directly:
```{r, comment="", fig.width=12, fig.height=7, out.width="100%", warning=FALSE}
iglu_1 <- get_iglu_format(data_clean)
head(iglu_1)
iglu::plot_agp(iglu_1)
```

### Cgmquantify

`get_cgmquantify_format()` builds the data frame that the metric functions of the [cgmquantify](https://CRAN.R-project.org/package=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:
```{r, comment="", eval = requireNamespace("cgmquantify", quietly = TRUE)}
cgmq_1 <- get_cgmquantify_format(data_clean)
head(cgmq_1)
cgmquantify::interdaycv(cgmq_1)
cgmquantify::J_index(cgmq_1)
```

Note 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`.

### DIAX

`write_DIAX_format()` writes CGM, basal, bolus, carbs, and SMBG data to one JSON file per subject, following the [DIAX](https://github.com/Center-for-Diabetes-Technology/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")`:
```{r}
diax_dir <- tempfile()
dir.create(diax_dir)
write_DIAX_format(combined, prefix = file.path(diax_dir, "diax_id_"))
list.files(diax_dir)
```

## Putting it all together

A typical study-level pipeline combines all of the above for every subject in a loop:
```{r, eval = FALSE}
# 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.