Dose Titration / Up-Titration to Target Dose
Operational construction, from real-world dosing data, of a per-patient dose trajectory that distinguishes the titration period (the early phase in which a drug is started low and adjusted upward toward a target/maintenance dose) from the stable maintenance phase — yielding time-to-target, dose intensity during titration, and reached-vs-not-reached-target variables, while avoiding the immortal time, titration-speed confounding, and treat-to-target reverse causation that static index-dose definitions create.
In plain language
Dose titration is when a drug is started low and the dose is walked upward over days or months until it reaches a steady "maintenance" dose or hits a target (like a blood-pressure or blood-sugar goal). In real-world data there is no dose column, so you rebuild each day's dose from the prescription details and then split a patient's history into the early "ramp up" period and the later stable period. The big traps: a patient labeled by the high dose they eventually reached had to survive long enough to get there (a fake survival advantage), and sicker patients often get pushed to higher doses, so higher doses can look harmful when they are really just a marker of being sicker.
Many drugs are not started at their effective dose. The clinician begins low and walks the dose upward over days to months — to find the tolerable maximum, to track a moving target (a lab, a symptom), or to ramp through a known tolerability barrier — before settling at a maintenance dose that is then held. Insulin is titrated to a fasting-glucose target; antihypertensives are up-titrated until blood pressure is controlled; levothyroxine is adjusted to a TSH target; antidepressants are stepped up for efficacy or down for side effects; GLP-1 agonists such as semaglutide follow a fixed escalation schedule to limit nausea; warfarin is dosed to an INR target. Dose-titration construction turns a stream of pharmacy fills (or EHR med orders / administrations) into a trajectory in which each interval carries the dose actually being taken at that point, and labels which intervals belong to the titration ramp versus the stable maintenance plateau. As with all time-varying exposure work, this is the analysis, not a preprocessing step: a static "index dose" pulled from the first fill mislabels the entire ramp.
Core conceptual distinction
Dose here is a time-varying quantity with two regimes, and three things must be separated and pre-specified. (1) The titration period vs the maintenance phase: the titration period runs from initiation until the dose stabilizes (no further sustained increase for a declared confirmation window, e.g. two consecutive refills at the same daily dose); the maintenance phase is the stable plateau that follows. (2) What counts as "the target/maintenance dose": it may be a guideline target (the labeled maintenance dose), a patient-specific stable dose (whatever dose the patient holds), or a physiologic target defined by a downstream measurement (INR 2-3, TSH in range) rather than the milligrams themselves. These are different variables and answer different questions. (3) The titration-derived estimands: time-to-target dose (a duration from initiation to first reaching/stabilizing at target), dose intensity during titration (cumulative or average daily dose over the ramp, or the slope of the ramp), and reached vs did not reach target (a binary, or competing-risk, endpoint where discontinuation and death compete with reaching target). A slow titrator and a fast titrator who both end at 30 mg have identical index doses and identical maintenance doses but completely different ramps — and that ramp is often the exposure of interest (tolerability, early effectiveness) or the confounder that wrecks a naive analysis.
Pros, cons, and trade-offs
(specific and comparative). - vs time-updated-exposures-cumulative-dose-rwe (the parent family): Titration construction is a specialization of time-varying exposure that adds the regime split (ramp vs plateau) and the titration-specific estimands (time-to-target, ramp slope, reached-target). It inherits the same long-format machinery — episode stitching, grace periods, lagging — but layers a change-point on top. Prefer the general cumulative-dose framing when you only need a running dose/burden and the ramp itself is not of interest; prefer titration framing when the ramp's speed, completion, or intensity is the exposure, the effect modifier, or the confounder. - vs a static index-dose definition (first-fill dose held forward): A static index dose is simple and immune to look-ahead, but for a titrated drug it is almost always wrong — it labels the patient by their lowest, starting dose and ignores that most of the early follow-up was spent ramping. Using it as a baseline covariate manufactures immortal time (the patient must survive long enough to up-titrate) and gross exposure misclassification. Prefer static index dose only when dose genuinely does not change (fixed-dose combinations) or when an intention-to-treat-by-starting-dose estimand is explicitly wanted. - vs treatment-patterns-lines-of-therapy / switch-add-on-augmentation-rwe: LOT and switch/augmentation logic operate at the level of which drug(s); titration operates within a single drug at the level of how much. A dose increase of the same molecule is titration, not a new line and not augmentation. They are complementary layers: a patient can be on LOT1, never switch, and still have a rich titration trajectory. Do not encode a dose increase as a switch — it inflates switching rates and erases the titration signal.
When to use
Any question where the dose changes by design over early follow-up and that change matters: tolerability and persistence during a known-difficult ramp (GLP-1 nausea, antidepressant start-up); time-to-control in treat-to-target diseases (insulin to fasting glucose, antihypertensives to BP, levothyroxine to TSH, warfarin to INR); dose-intensity or ramp-slope as an effect modifier of effectiveness or safety; characterizing the share of real-world initiators who ever reach the guideline target dose (clinical inertia studies); and defining a clean "stable maintenance dose" index date for a downstream comparative analysis that should start at plateau, not at the chaotic ramp.
When NOT to use — and when it is actively misleading or dangerous
- Immortal time during titration. If "reached target dose" or "high maintenance dose" is used as a baseline group label, every patient in that group had to survive — and stay observed and adherent — through the entire ramp to qualify. Follow-up that begins at initiation but classifies by a dose reached later makes the ramp period immortal: events during titration cannot occur in the high-dose group by construction, fabricating a survival advantage for higher doses. Classify dose as time-varying (low until the increase actually happens), or use a landmark at a fixed post-initiation time, or clone-censor-weight the titration strategies. - Confounding by titration speed. Why a patient titrates fast or slow is rarely random: it tracks tolerability, baseline severity, visit frequency, and clinician aggressiveness. Comparing fast vs slow titrators, or high vs low maintenance dose, without adjusting for the reasons dose was changed confounds the dose effect with the indication for changing it. The ramp slope is both an exposure and a marker of the latent process driving it. - Treat-to-target reverse causation (the most dangerous trap). In treat-to-target dosing the dose is increased because the patient is not responding — sicker patients get pushed to higher doses. A naive analysis then finds that higher doses associate with worse outcomes, reading the consequence of poor control as a cause. This is confounding by indication operating through a feedback loop: the time-varying confounder (the lab/symptom) responds to the drug AND drives the next dose change. A standard time-dependent model that simply adjusts for the current lab conditions on a mediator and is biased; only g-methods (IPTW marginal structural models, g-estimation) recover the causal effect when dose is titrated to a measured target.
Data-source operational depth
- Claims (FFS): There is no "dose" field. Daily dose is inferred as `NDC strength x dispensed quantity / days_supply` (e.g., 30 tablets of a 10 mg NDC over 30 days = 10 mg/day). A dose increase shows up as a fill of a higher-strength NDC, or more tablets of the same strength, or a shorter days_supply for the same quantity — all of which must be normalized to mg/day before a "titration step" can be detected. Real failure modes: (a) pill- splitting and titration packs / starter kits break the strength x quantity arithmetic (a 30-count "dose pack" that steps 0.25 -> 1.0 mg over a month has one NDC but several daily doses); (b) Medicare Advantage / capitated person-time lacks FFS fill claims, freezing the inferred dose and hiding titration steps — exclude MA-only spans; (c) 90-day mail order and stockpiling blur the timing of a dose change; (d) the physiologic target (INR, TSH, fasting glucose) is invisible in claims — you can see the milligrams change but not the lab that drove it, so treat-to-target confounding cannot be adjusted in claims-only data. - EHR: Dose can come from the medication order + structured sig ("take 1 tablet twice daily, increase to 2 tablets after 1 week"), the MAR (administrations — true for inpatient/infusional), or the e-prescribe feed. EHR's advantage is the target itself is observable (labs, vitals, INR, TSH), making it the substrate where treat-to-target feedback can actually be modeled — but sigs are often free-text and titration instructions live in a single order's text, so the dose change is documented without a new order. Parse the sig, and prefer the MAR for administered drugs. - Registry: Protocol or disease registries often capture target attainment and dose adjustments prospectively (the cleanest source for "reached target"), but dose holds and down-titrations are frequently noted only in unstructured text, overstating the dose actually taken; link to claims for fill completeness. - Linked claims-EHR-registry: The ideal substrate — claims for fill completeness and dose timing, EHR for the target lab that drove each step (so g-methods can model the feedback), registry for adjudicated target attainment. Order/fill/administration date discrepancies must be reconciled before any titration-step date is set.
Worked claims example
Question: time-to-target-dose and titration-period dose intensity for an oral drug with a 10 -> 20 -> 30 mg up-titration to a 30 mg maintenance target, FFS claims, one patient. (1) Build daily dose per fill: `daily_dose = NDC_strength_mg x quantity / days_supply`. Fill on day 0: 30 tablets of 10 mg over 30 days -> 10 mg/day. Fill on day 28: 30 tablets of 20 mg over 30 days -> 20 mg/day. Fill on day 56: 30 tablets of 30 mg over 30 days -> 30 mg/day. Fill on day 86 and day 116: 30 mg/day each. (2) Detect titration steps: daily dose rises 10 -> 20 -> 30, so days 0-55 are the titration ramp. (3) Define stable/maintenance dose: require two consecutive fills at the same daily dose with no further increase; 30 mg first appears on day 56 and repeats on days 86 and 116, so 30 mg is confirmed stable as of day 56. (4) Time-to-target: target = 30 mg label maintenance; first reached on day 56, so time-to-target = 56 days and the titration period = days 0-55 (56 days). (5) Dose intensity during titration: average daily dose over the ramp = (10 mg x 28 days + 20 mg x 28 days)/56 days = (280 + 560)/56 = 15 mg/day, i.e. 50% of target — the patient spent the ramp well below the maintenance dose, which a static "index dose = 10 mg" and a static "maintenance dose = 30 mg" both misrepresent. (6) Analysis hygiene: classify dose as time-varying (10 mg on [0,28), 20 mg on [28,56), 30 mg from 56); if comparing maintenance-dose levels for an outcome, do not label baseline by the day-56 dose (immortal time) — use a landmark or g-method, and if the dose steps were driven by an unobserved-in-claims target (e.g., glucose), state that treat-to-target confounding is unadjustable in claims-only data and requires linked labs.
Worked example
Scenario
One patient starts an oral drug that is meant to be up-titrated 10 -> 20 -> 30 mg, with 30 mg as the maintenance target. We see five pharmacy fills over four months in a claims table. There is no dose field, so we infer the daily dose from each fill, find where the dose stabilizes, and compute how long it took to reach the 30 mg target and how intense dosing was during the ramp.
Dataset
The raw rows an analyst would see in a claims pharmacy table (one row per fill).
| person_id | fill_date | drug | strength_mg | quantity | days_supply |
|---|---|---|---|---|---|
| 2001 | 2023-01-01 | exampledrug | 10 | 30 | 30 |
| 2001 | 2023-01-29 | exampledrug | 20 | 30 | 30 |
| 2001 | 2023-02-26 | exampledrug | 30 | 30 | 30 |
| 2001 | 2023-03-28 | exampledrug | 30 | 30 | 30 |
| 2001 | 2023-04-27 | exampledrug | 30 | 30 | 30 |
Steps
Infer daily dose for each fill = strength_mg x quantity / days_supply. Fill 1 = 10*30/30 = 10 mg/day; fill 2 = 20 mg/day; fills 3-5 = 30 mg/day.
The dose rises 10 -> 20 -> 30, so the first two fills (days 0-55) are the titration ramp.
A "stable maintenance dose" needs two consecutive fills at the same dose with no later increase. 30 mg first appears on day 56 and repeats on days 86 and 116, so 30 mg is confirmed stable starting day 56.
Target = 30 mg label maintenance; first reached on day 56, so time-to-target = 56 days and the titration period is days 0-55 (56 days long).
Titration dose intensity = time-weighted average daily dose over the ramp = (10 mg x 28 days + 20 mg x 28 days) / 56 days = (280 + 560) / 56 = 15 mg/day, which is 50% of the 30 mg target.
Result
Reached the 30 mg target on day 56; titration period = 56 days; titration-period dose intensity = 15 mg/day (50% of target). A static "index dose = 10 mg" and "maintenance dose = 30 mg" both hide that the patient spent the first 56 days well below target.
Timeline Spec
- Title
One patient up-titrating 10 to 20 to 30 mg over a 56-day ramp to a 30 mg target dose
- Window
- Start
2023-01-01
- End
2023-05-27
- Label
Observation: initiation through stable maintenance
- Events
- Label
Fill 1
- Start
2023-01-01
- Length Days
28
- Quantity
10 mg/day
- Label
Fill 2
- Start
2023-01-29
- Length Days
28
- Quantity
20 mg/day
- Label
Fill 3 (target reached)
- Start
2023-02-26
- Length Days
30
- Quantity
30 mg/day
- Label
Fill 4
- Start
2023-03-28
- Length Days
30
- Quantity
30 mg/day
- Label
Fill 5
- Start
2023-04-27
- Length Days
30
- Quantity
30 mg/day
- Spans
- Kind
exposed
- Start
2023-01-01
- End
2023-02-25
- Label
Titration period: 56 days, intensity 15 mg/day
- Kind
followup
- Start
2023-02-26
- End
2023-05-27
- Label
Maintenance phase at 30 mg target
- Result
- Label
Reached 30 mg target on day 56; titration period = 56 days
- Value
56
Runnable example
python implementation
Construct a per-patient dose trajectory from dosing rows, identify the titration period vs maintenance plateau, and flag time-to-target. Required input (cleaned, de-duplicated, one row per fill): fills : person_id, fill_date (datetime64), strength_mg...
import pandas as pd
STABLE_FILLS = 2 # consecutive fills at the same daily dose to confirm a stable maintenance dose
TARGET_MG = 30.0 # label/guideline maintenance target (set per drug; None = patient-specific stable dose)
def build_dose_trajectory(fills: pd.DataFrame) -> pd.DataFrame:
f = fills.sort_values(["person_id", "fill_date"]).copy()
# 1) Infer daily dose from claims arithmetic and normalize across NDC strengths.
f["daily_dose"] = (f["strength_mg"] * f["quantity"] / f["days_supply"]).round(3)
out = []
for pid, g in f.groupby("person_id", sort=False):
g = g.reset_index(drop=True)
day0 = g.loc[0, "fill_date"]
doses = g["daily_dose"].tolist()
dates = g["fill_date"].tolist()
# 2) Identify the first dose that is sustained (no later increase) = stable maintenance dose.
stable_dose, stable_date = None, None
for i in range(len(doses)):
run = [d for d in doses[i:] if d >= doses[i]] # never drops below at/after i
same = sum(1 for d in doses[i:] if d == doses[i])
no_later_increase = all(d <= doses[i] for d in doses[i:])
if same >= STABLE_FILLS and no_later_increase:
stable_dose, stable_date = doses[i], dates[i]
break
target = TARGET_MG if TARGET_MG is not None else stable_dose
reached = stable_dose is not None and target is not None and stable_dose >= target
target_date = stable_date if reached else None
ttt = (target_date - day0).days if target_date is not None else None
titration_days = ttt if ttt is not None else (dates[-1] - day0).days
# 3) Dose intensity during titration = time-weighted mean daily dose over the ramp.
ramp = g[g["fill_date"] < (target_date if target_date is not None else dates[-1])]
if len(ramp):
dur = ramp["days_supply"].clip(lower=1)
intensity = float((ramp["daily_dose"] * dur).sum() / dur.sum())
else:
intensity = float(doses[0])
out.append({
"person_id": pid,
"stable_dose_mg": stable_dose,
"target_mg": target,
"reached_target": bool(reached),
"time_to_target_days": ttt,
"titration_period_days": titration_days,
"titration_dose_intensity_mg": round(intensity, 2),
})
return pd.DataFrame(out)r implementation
Same dose-trajectory construction in data.table: infer daily dose from strength x quantity / days_supply, find the first sustained (non-increasing thereafter) daily dose as the stable maintenance dose, and compute time-to-target, the reached-target flag,...
library(data.table)
STABLE_FILLS <- 2L # consecutive same-dose fills confirming a stable maintenance dose
TARGET_MG <- 30 # label/guideline target; set NA_real_ for a patient-specific stable dose
build_dose_trajectory <- function(fills) {
setDT(fills); setorder(fills, person_id, fill_date)
fills[, daily_dose := round(strength_mg * quantity / days_supply, 3)]
one_person <- function(g) {
doses <- g$daily_dose; dates <- g$fill_date; day0 <- dates[1L]
stable_dose <- NA_real_; stable_date <- as.Date(NA)
for (i in seq_along(doses)) {
tail_d <- doses[i:length(doses)]
same <- sum(tail_d == doses[i])
if (same >= STABLE_FILLS && all(tail_d <= doses[i])) { # sustained, no later increase
stable_dose <- doses[i]; stable_date <- dates[i]; break
}
}
target <- if (is.na(TARGET_MG)) stable_dose else TARGET_MG
reached <- !is.na(stable_dose) && !is.na(target) && stable_dose >= target
tdate <- if (reached) stable_date else as.Date(NA)
ttt <- if (!is.na(tdate)) as.integer(tdate - day0) else NA_integer_
tit_days <- if (!is.na(ttt)) ttt else as.integer(dates[length(dates)] - day0)
end <- if (!is.na(tdate)) tdate else dates[length(dates)]
ramp <- g[fill_date < end]
intensity <- if (nrow(ramp))
sum(ramp$daily_dose * pmax(ramp$days_supply, 1L)) / sum(pmax(ramp$days_supply, 1L)) else doses[1L]
list(stable_dose_mg = stable_dose, target_mg = target, reached_target = reached,
time_to_target_days = ttt, titration_period_days = tit_days,
titration_dose_intensity_mg = round(intensity, 2))
}
fills[, one_person(.SD), by = person_id]
}