Medication (ATC)

Extract drug exposure from LMDB - filtered to your cohort

Published

September 9, 2026

Prescription medication lives in LMDB (the Prescription Register). The register has one row per dispensed prescription, so the same person can have hundreds of rows. The two columns you almost always use are:

LMDB starts in 1995, and is generally used from 1997. There is no medication history before that, so a lookback window that reaches further back is empty rather than negative - and “no prescriptions” will look like “not a user”.

The full confirmed column list is in Register reference.

LMDB only covers prescriptions dispensed at community pharmacies. Three things are therefore systematically missing: drugs given during hospital admission, drugs dispensed directly by hospitals (e.g. chemotherapy and immunosuppressants) and drugs for certain institutionalized people (Pottegård et al. 2017). If you need in-hospital medication, a newer Hospital Medication Register (Sygehusmedicinregisteret, data from 2018) exists, but it is still unvalidated, incomplete and accessed via the Danish Health Data Authority, not DST - use it with caution (Rosenkrantz et al. 2024).

Exposure, outcome or covariate? Same extract

Medication can be all three. It is solved exactly as for diagnoses (Extract from LPR): you make one extract of the dispensings, and the role is decided by when eksd falls relative to index_date - not by a separate page per role:

Role When What you do Page
Exposure The date exposure starts First eksd = the person’s index date Phase 10
Covariate (medication at baseline) eksd < index_date (e.g. 6-12 months before) Ever/never or a count in a window before index Comorbidity
Outcome (new treatment) eksd > index_date First eksd after index Outcomes
Time-varying (on/off treatment) Changes during follow-up Start/stop format Time-varying

So you do not write three different medication extracts. You write one, and the date filter relative to index determines the role.

The pattern

ATC codes have no D-prefix (unlike ICD in LPR, see Understand LPR), so you match directly on the start of the code. read_register() requires fastreg set up with the path to your registers - see Phase 4 if you did not convert them from SAS yourself:

library(fastreg) # read_register()
library(arrow) # open_dataset() - fallback without fastreg
library(dplyr)

cohort_pnrs <- unique(readRDS("path/to/full_cohort.rds")$pnr) # your cohort from Phase 10

medication <- read_register("lmdb") %>% # without fastreg: open_dataset("path/to/lmdb/")
  rename_with(tolower) %>%
  semi_join(tibble(pnr = cohort_pnrs), by = "pnr") %>% # ONLY your cohort
  filter(substr(atc, 1, 5) == "A10BJ") %>% # GLP-1 analogues - filter BEFORE collect
  select(pnr, atc, eksd, vnr) %>%
  collect() %>% # only HERE is data pulled into RAM
  group_by(pnr) %>%
  arrange(eksd) %>%
  slice(1) %>%
  ungroup() # first dispensing per person

saveRDS(medication, "path/to/extract_medication.rds")

substr(atc, 1, 5) == "A10BJ" matches the whole ATC level (all GLP-1 analogues). For several groups at once, use regex as in LPR: grepl("^A10BJ|^A10BA", atc). The code-matching pattern (regex, %in% with a code list, !!) is explained in Extract from LPR and Function guide. Reducing to one row per person is explained in Long ↔︎ wide format - here slice(1) for the first dispensing, but it could also be ever/never or a count in a window (see the role table above).

From dispensings to “is this person a user?”

One dispensing is weak evidence of treatment: people fill a prescription once and stop. A common definition is therefore at least two dispensings within a window, with the onset date being the date the definition is first met.

library(dplyr) # group_by, mutate, lag, filter, summarise

# Does the person have `min_dispensings` fills inside `window_days`?
# lag() looks back to the (min_dispensings - 1)th previous fill for that person,
# so the gap between them is the span of the whole qualifying run.
flag_users <- function(data, min_dispensings = 2, window_days = 365) {
  data %>%
    arrange(pnr, eksd) %>%                       # lag() needs the rows in date order
    group_by(pnr) %>%
    mutate(
      lookback = lag(eksd, n = min_dispensings - 1),
      gap_days = as.numeric(eksd - lookback)
    ) %>%
    ungroup() %>%
    filter(!is.na(gap_days), gap_days <= window_days) %>%  # the run is short enough
    group_by(pnr) %>%
    summarise(onset_date = min(eksd), .groups = "drop")    # first date the rule is met
}

users <- flag_users(medication, min_dispensings = 2, window_days = 365)

The two numbers are your choices, not standards - write them into your methods. One dispensing is enough for a drug taken once; a chronic treatment usually wants two or more.

This is also how medication can enter a comorbidity definition: for conditions treated largely outside hospital, LMDB catches people LPR never sees. Whether to define a condition from diagnoses, prescriptions or both is a design decision - see Comorbidity - diagnoses, prescriptions, or both?.

arrange() before lag(), always. lag() works on row order, not on dates. If the rows are not sorted by person and date, gap_days is computed against an arbitrary earlier row and the result is quietly wrong rather than an error.

Which ATC column?

LMDB does not hold the ATC code once. It holds it five times, at five levels, in five separate columns. DST’s variable list for LMDB labels them ATC-kode detaljeret and ATC-niveau 1 to ATC-niveau 4. They are cumulative: each level repeats the levels above it and adds its own characters.

Column ATC level Characters Example What it is
atc1 1 1 C anatomical main group
atc2 2 3 C09 therapeutic subgroup
atc3 3 4 C09A pharmacological subgroup
atc4 4 5 C09AA chemical subgroup
atc 5 7 C09AA05 chemical substance

The column names and their levels come from DST’s variable list. The character counts are not stated there; they follow from the WHO standard, where level 2 is by definition the three-character code (WHO Collaborating Centre for Drug Statistics Methodology).

A pattern longer than the column can never match, and R will not tell you. atc2 holds three characters. grepl("N02A", atc2) compares a four-character pattern against three-character values, so it matches nothing: zero rows, no error, no warning. The same goes for filter(atc2 %in% c("A10A", "A10B")). An empty result looks like a finding rather than a bug, and it is easy to write up as “no exposed patients” when the truth is that the filter could not fire.

So filter on atc, not on the level columns. It is seven characters, it contains every level as a prefix, and substr(atc, 1, 3) gives you exactly what atc2 holds. There is no case where a level column lets you filter something atc cannot:

library(dplyr) # filter, select, collect

# Ischaemic heart disease treatment, defined as organic nitrates (C01DA):
ihd <- lmdb %>%
  filter(grepl("^C01DA", atc)) %>% # the full definition, in one filter
  select(pnr, atc, eksd) %>%
  collect()

Use ^ to anchor the pattern at the start of the code. It costs nothing and it is the right habit from LPR, where a code genuinely can turn up mid-string.

Which way should you write the filter?

There are two ways to keep the codes you want, and the length of your codes decides which one to use. ATC codes are not all the same length: N02 is three characters, A10BA is five.

Your code list Use Why
All codes have the same length substr() + %in% Faster, and easy to read
Codes have different lengths grepl() with ^ Handles any length

All codes the same length. Cut atc down to that length, then check whether the result is on your list:

library(dplyr) # filter, select, collect

# Four drug groups, all three characters long
my_codes <- c("A10", "C09", "N02", "M01")

lmdb %>%
  filter(substr(atc, 1, 3) %in% !!my_codes) %>% # keep the first 3 characters, look them up
  select(pnr, atc, eksd) %>%
  collect()

substr(atc, 1, 3) means “the first three characters of atc”. Change the 3 to match your codes: use 1, 5 for five-character codes such as A10BA.

Codes of different lengths. One substr() cannot serve N02 and A10BA at the same time, because they need different cut lengths. Use grepl() instead, which matches whatever you give it:

lmdb %>%
  filter(grepl("^N02|^A10BA|^C09AA", atc)) %>% # ^ means "starts with", | means "or"
  select(pnr, atc, eksd) %>%
  collect()

In doubt? Use grepl(). It works for every code length, so it is never wrong. substr() + %in% was about twice as fast on a twenty-million-row test, which is worth having when you fetch many drug groups at once, but a correct slow filter beats a fast broken one.

Does it save memory to filter on atc2 first? No. It is a reasonable thing to wonder, since atc2 is the short column, but the filter runs before anything is loaded: nothing reaches your memory until collect(), and what lands there is the rows that survived, which are the same rows either way. substr(atc, 1, 3) gives you exactly what atc2 contains, so the two are the same query.

There is a practical catch as well. Comorbidity definitions often need more than three characters (A10BA rather than A10), and atc2 cannot narrow that far. You would filter on atc2, then bring in atc afterwards to get down to the codes you actually wanted: two steps for a result one step gives you. And if a code longer than three characters is compared against atc2 on its own, it matches nothing and returns no rows without an error, so it is worth doing in one step on atc.

What the level columns are actually for: grouping, not filtering. When you want a descriptive breakdown by therapeutic subgroup, atc2 is a ready-made grouping column and saves you a substr():

medication %>%
  count(atc2, sort = TRUE) # dispensings per therapeutic subgroup

That is the use case. Filtering is not.

TipCheck this on your own extract before you trust it

Two minutes, and it settles both whether the columns exist and how they are coded:

library(dplyr) # select, collect

colnames(lmdb) # do you even have atc1-atc4? Not every delivery does
lmdb %>%
  select(atc, atc1, atc2, atc3, atc4) %>%
  head(20) %>%
  collect()

Expect C / C09 / C09A / C09AA / C09AA05 across a row: each column repeating the one before it. If instead you see isolated fragments (09 in atc2), every ^-anchored pattern in this section is wrong for your delivery and you should filter on atc only.

Note that names(lmdb) will not answer this. On a lazy connection it returns the connection object’s own parts (src, lazy_query), not the table’s columns, so it looks as though the columns are missing. colnames() works on the DuckDB object read_register() returns; if you opened LMDB with open_dataset() instead, use names(collect(head(lmdb, 0))) - see Phase 7 - The shape.

atc3 and atc4 are not filled in for every product. Not all preparations are classified to that depth, so do not assume the columns are complete. Count the blanks in your own extract before you build anything on them.

ATC is not enough: same substance, different product

ATC classifies by active substance, not by product or indication. Two brands with the same substance therefore get the same ATC - and cannot be told apart on atc alone:

  • Ozempic (semaglutide, type 2 diabetes) and Wegovy (semaglutide, weight loss) both have ATC A10BJ06. Filter on ATC only, and you mix diabetes treatment together with weight-loss treatment.

Two columns separate them:

  • vnr (varenummer): the unique key to the actual product (package). It is the only reliable way to isolate one specific product. The vnr-to-product lookup comes from the medicine taxonomy (KAT / Danish Health Data Authority); name/packtext hold the product text if you want to recognise it by eye.
  • indo (indication code): a coded indication (from the Medicinpriser catalogue, LMS 25), not free text. The code is recorded only when the prescriber picks an indication from the drop-down menu in the electronic prescription. If the doctor types the indication as free text instead, it is not transferred to the register and indo is left blank. So it can in principle separate the same substance across indications, but it is often empty. Use vnr as the primary product key and indo as a supporting signal, not a clean filter.
# 1. Build your own list of the varenumbers that belong to the product
#    (look them up in the medicine taxonomy - one product has several varenumbers):
ozempic_vnr <- c("xxxxxx", "yyyyyy") # placeholders - replace with your looked-up numbers

# 2. Keep only the dispensings whose vnr is on your list:
medication %>%
  filter(atc == "A10BJ06") %>% # semaglutide (Ozempic AND Wegovy)
  filter(vnr %in% !!ozempic_vnr) # keep only rows with a vnr from your list

Keep the two names in the last line apart: vnr (on its own) is the register’s column holding the varenummer - the product’s ID, just as pnr is the person’s ID. ozempic_vnr is your own R vector of the varenumbers you looked up for Ozempic; you choose the name yourself (same pattern as the code list in Extracting data step by step). %in% keeps the rows where vnr is found in your list, and !! sends your local list into the lazy query (explained in Extract from LPR and Function guide).

Which vnr belongs to which product depends on package and strength (one product has several varenumbers). Look them up in the medicine taxonomy for your exact study period rather than assuming, and document the list in your code. For indo, Harbi & Pottegård 2024 found a recorded indication code on 82% of prescriptions (about 88% corrected) and almost 100% correct when present - but missingness is markedly higher before 1 October 2017 (when electronic prescribing became mandatory) and varies by drug class (about 8% missing for systemic anti-infectives versus 28% for blood-related agents). 5.6-36% of codes are nonspecific (e.g. “for the heart” for beta-blockers); whether a nonspecific code is usable depends on your question. The value set is the Danish Health Data Authority’s drug classification (Medicinpriser). As a side note: a validly recorded code does not mean the prescriber chose the correct indication - drop-down menus make wrong choices easy, so the code is not necessarily the clinical truth.

A dispensing is not the same as intake. eksd tells you the prescription was collected at the pharmacy, not that the patient took the drug, and certainly not for how long. For an exposure that lasts over time - treatment episodes, grace periods (the allowed gap between two prescriptions before a person counts as having stopped) and adherence measures such as PDC (proportion of days covered: the share of follow-up time covered by medicine) or MPR (medication possession ratio: amount dispensed divided by the length of the period) - you must build exposure windows from quantity and strength, not just count dispensings. The relevant LMDB fields are apk (number of packages), packsize (pack size, i.e. units per package) and strnum (the numeric strength; the unit is in strunit). Note that the dosage field itself (doso) is essentially empty (recorded for ~0.06% of prescriptions in the same validation), so dose must be derived from these package fields. Ready-made tools for this are in heaven (medicinMacro() for drug exposure windows from LMDB, and hypertensionMedication() if the exposure you need is antihypertensive treatment, which it defines from ATC codes for you). If your index date or exposure status depends on future medication, you get immortal time bias: if you require a person to fill the prescription (or two prescriptions) after index to count as exposed, then by construction they survived until that date - and the “immortal” time between index and the first fill makes the exposure look artificially protective. Instead, start follow-up for the exposed at the first dispensing, or treat medication as a time-varying exposure.

See also

Back to top