Dose tracking arithmetic for GLP-1 schedules in R

An R Markdown walkthrough of the four calculations that a GLP-1 dosing log actually needs: reconstituted concentration, syringe units per dose, a schedule table with dates, and vial depletion. Last updated 2026-07-31.

Why this document exists

People who track a GLP-1 titration on paper get tripped up by the same three things: the concentration changes every time the diluent volume changes, the number printed on an insulin syringe is not milligrams, and nobody remembers which week of which step they are on. All three are arithmetic, so they belong in code rather than in a notebook margin.

Everything below is plain base R. There are no packages to install and no data files to download. If you would rather not run anything, I keep the same arithmetic as a web form in the Peptide Tracker & Calculator, which is what I actually open when I am standing in front of a vial.

Scope. This is a numeric-methods document. The dose steps used below are an example input chosen to exercise the code, not a recommendation, and nothing here is clinical guidance. Concentration, dose and schedule must come from your prescriber and the product label. No trial data, outcome statistics or clinical citations appear in this document because none were used to write it.

1. Reconstituted concentration

A lyophilised vial has a mass in milligrams. It has no concentration until you add diluent. Concentration is one division, and it is the number that every later step depends on.

reconstitute <- function(vial_mg, diluent_ml) {
  stopifnot(vial_mg > 0, diluent_ml > 0)
  vial_mg / diluent_ml          # mg per mL
}

conc <- reconstitute(vial_mg = 5, diluent_ml = 2)
conc
## [1] 2.5

A 5 mg vial reconstituted with 2 mL gives 2.5 mg/mL. Reconstitute the same vial with 1 mL and you get 5 mg/mL, and every syringe reading you wrote down last week is now wrong by a factor of two. This is the single most common source of a mistracked dose, and it is why the diluent volume belongs in the log next to the dose, not in your memory.

2. Milligrams to syringe units

A U-100 insulin syringe is graduated so that 100 units is 1 mL. The graduation is a volume, not a mass, so converting a dose in milligrams needs the concentration from step 1.

units_for_dose <- function(dose_mg, conc_mg_per_ml, syringe_units_per_ml = 100) {
  dose_mg / conc_mg_per_ml * syringe_units_per_ml
}

doses <- c(0.25, 0.5, 1.0, 1.7, 2.4)
data.frame(
  dose_mg   = doses,
  volume_ml = round(doses / conc, 3),
  units_u100 = round(units_for_dose(doses, conc), 1)
)
##   dose_mg volume_ml units_u100
## 1    0.25     0.100         10
## 2    0.50     0.200         20
## 3    1.00     0.400         40
## 4    1.70     0.680         68
## 5    2.40     0.960         96

Two practical points fall out of this table. First, at 2.5 mg/mL a 2.4 mg dose is 96 units, which is nearly the whole barrel of a 1 mL syringe, so a thicker reconstitution would be needed to keep the draw in a comfortable range. Second, the pretty round numbers here are an artefact of the example; at 2.7 mg/mL the same 1.7 mg dose is 62.96 units, and you have to decide in advance whether you round to the nearest whole unit and record the rounded value or the intended one. Record the value you actually drew.

3. A schedule table with real dates

The schedule is a step function: a list of doses and how many weeks each is held. Turning it into dates is where a log stops being a list of numbers and becomes something you can check against a calendar.

build_schedule <- function(steps_mg, weeks_per_step, conc_mg_per_ml, start_date) {
  stopifnot(length(steps_mg) == length(weeks_per_step))
  first_week <- cumsum(c(1, head(weeks_per_step, -1)))
  data.frame(
    step       = seq_along(steps_mg),
    dose_mg    = steps_mg,
    weeks      = weeks_per_step,
    first_week = first_week,
    start      = start_date + (first_week - 1) * 7,
    units      = round(units_for_dose(steps_mg, conc_mg_per_ml), 1),
    step_mg    = steps_mg * weeks_per_step
  )
}

sched <- build_schedule(
  steps_mg       = doses,
  weeks_per_step = c(4, 4, 4, 4, 4),
  conc_mg_per_ml = conc,
  start_date     = as.Date("2026-01-05")
)
sched$cumulative_mg <- cumsum(sched$step_mg)
sched
stepdose_mgweeksfirst_weekstartunitsstep_mgcumulative_mg
10.25412026-01-05101.01.0
20.50452026-02-02202.03.0
31.00492026-03-02404.07.0
41.704132026-03-30686.813.8
52.404172026-04-27969.623.4

The cumulative_mg column is the useful one for planning supply: this example consumes 23.4 mg to reach the end of step 5, which is five 5 mg vials with 1.6 mg left over. The weeks_per_step vector is deliberately a parameter and not a constant, because a real schedule almost never holds every step for exactly the same number of weeks. Repeat a step by raising its entry, skip one by setting it to zero and filtering, and the dates downstream move themselves.

Handling a missed week

A missed dose shifts every subsequent date, which is the whole reason to compute dates rather than write them down. The cheapest fix is to keep a vector of actual injection dates and rebuild the schedule from the last real one.

reschedule_from <- function(sched, resume_date, from_step) {
  keep <- sched$step >= from_step
  offset <- as.numeric(resume_date - sched$start[keep][1])
  sched$start[keep] <- sched$start[keep] + offset
  sched
}

Storing the intended schedule and the observed dates separately, then diffing them, is more honest than editing the plan in place. It also means the log can answer "how many doses did I actually take at 1 mg" without you reconstructing it from memory.

4. Vial depletion and reorder timing

The last piece is knowing when you run out. Whole doses per vial is a floor, not a quotient, because the remainder in the vial is not a usable dose.

doses_per_vial <- function(vial_mg, dose_mg) floor(vial_mg / dose_mg)

data.frame(
  dose_mg   = doses,
  per_vial  = doses_per_vial(5, doses),
  waste_mg  = round(5 - doses_per_vial(5, doses) * doses, 2)
)
##   dose_mg per_vial waste_mg
## 1    0.25       20     0.00
## 2    0.50       10     0.00
## 3    1.00        5     0.00
## 4    1.70        2     1.60
## 5    2.40        2     0.20

At 1.7 mg the waste is 1.6 mg per vial, nearly a third of it, which is a genuine argument for choosing a vial size that divides evenly into the dose you expect to sit at longest. Note also that floor on a floating point quotient is where this calculation can bite you: floor(5 / 0.1) is not guaranteed to be 50 on every platform, so round the quotient to a sensible number of decimal places before flooring if your doses are not binary-exact.

Combine that with the schedule and you get a reorder date. I built the browser version of this because I wanted the projection on a phone rather than in an R session; if that is useful to you it lives in the GLP-1 dose tracker and vial calculator.

Provenance and limitations