Extract from LPR

Practical recipes - the two approaches, helper functions and integration with the cohort

Published

September 9, 2026

This page shows how to extract diagnoses from LPR with code. It builds on the structure from Understand LPR: the periods (LPR2/LPR3), the D-prefix, the diagnosis types (A/B/G) and the filter for retracted diagnoses. Read that page first if you haven’t already.

You use the same extraction pattern as in Phase 5 and Phase 6 - just applied to LPR’s two generations. It is the most important and probably the most complex part of the guide.

The examples assume you already have a cohort: cohort, a data.frame with pnr and index_date per person, built in Phase 10. That phase comes later and uses the pattern you learn here, so read them in order and come back to this code once your cohort exists. They also use inner_join() and bind_rows(), explained in Phase 12.

Check the column names in your own delivery before you load. The examples use the names the registers typically have, but LPR_A renamed a number of them, and what you were given is not what the register contains. List them with arrow::schema(), and look the confirmed ones up in Overview of registers.

Two facts from Understand LPR that the code below depends on: use the LPR_A files and filter them with lprindberetningssystem == "LPR3" (why), and expect a spike in diagnosis counts around 2019-2020 if your analysis counts diagnoses or looks at trends (why).

Fetch diagnoses from LPR

There is one decision to make, and it is about your study, not about LPR: do you need one outcome, or several?

  • One outcome (or a couple): filter on your ICD codes inside the query, so only the matching rows are ever read. That is the worked example below.
  • Several outcomes: pull all A and B diagnoses once into a table called all_dx, then filter that table per outcome. Same code, three small changes - see Several outcomes at once. Querying LPR once and reusing the result is much faster than re-scanning the registers per outcome, and the reusable get_lpr_diagnoses() function is built for exactly this.

Either way the shape is identical, and it comes straight from Understand LPR: a hospital contact’s pnr and date live in one table and the ICD code in another, so every extract is a join between a contact register and a diagnosis register, done once per LPR generation and then stacked.

The examples use read_register("name") (fastreg); without fastreg use open_dataset("path/to/<register>/"). LPR is one of the largest registers, so it must be in parquet - reading it from SAS pulls the whole thing into RAM. Convert first if nobody has: Parquet and fastreg.

The code filters to your cohort with semi_join(tibble(pnr = ...)) rather than filter(pnr %in% ...), because it pushes down into Arrow/DuckDB better and needs no !! - see semi_join(). You still need !! for a local code list inside filter(), e.g. substr(c_diag, 2, 4) %in% !!CODES. CODES must then hold three-character codes. The left side is always three characters, so a four-character code such as E119 silently matches nothing: filter on E11 and narrow afterwards on the full c_diag.

One outcome: filter on your codes

The example fetches diabetes mellitus (E10-E14). Replace CODES_REGEX with your own codes. It runs in four steps, one per LPR generation plus the combine.

Setup. The cohort’s pnr and the codes you are looking for:

library(fastreg) # read_register - read a register by name
library(arrow)   # open_dataset - fallback without fastreg
library(dplyr)

cohort_pnrs <- unique(cohort$pnr)
CODES_REGEX <- "^DE1[0-4]"   # diabetes mellitus (E10-E14) - with D-prefix

Step 1 - LPR2 somatic (lpr_adm + lpr_diag, up to March 2019). This is the shape all three steps follow: open both tables, keep your cohort, take pnr + key + date from the contact register, join the diagnosis register on the contact key, and only then collect().

lpr_adm  <- read_register("lpr_adm")  %>% rename_with(tolower)
lpr_diag <- read_register("lpr_diag") %>% rename_with(tolower)

lpr2_dm <- lpr_adm %>%
  semi_join(tibble(pnr = cohort_pnrs), by = "pnr") %>%   # ONLY your cohort. OMIT this line if you want the WHOLE population
  select(pnr, recnum, date_contact = d_inddto) %>%  # d_inddto is RENAMED to date_contact (see below)
  inner_join(
    lpr_diag %>%                                    # diagnosis register: has recnum + ICD code, but neither pnr nor date
      filter(c_diagtype %in% c("A", "B"),           # A + B = action/secondary diagnoses; add "G" if you also want underlying conditions
             grepl(CODES_REGEX, c_diag)) %>%         # keep only your codes - filter BEFORE collect (D-prefix in the regex)
      select(recnum, c_diag, c_diagtype),
    by = "recnum"                                   # recnum = the key linking contact and diagnosis in LPR2
  ) %>%
  collect() %>%                                      # ONLY here is data pulled into RAM (everything above runs in the database)
  mutate(icd3 = substr(c_diag, 2, 4))                # strip the D-prefix: "DE11" -> "E11"

Step 2 - LPR3 (lpr_a_kontakt + lpr_a_diagnose, March 2019 onwards). Same shape, different table and column names, plus two things LPR2 does not have: the lprindberetningssystem filter and the retracted-diagnosis filter.

lpr3_k <- read_register("lpr_a_kontakt")  %>% rename_with(tolower) %>%
  filter(lprindberetningssystem == "LPR3")   # keep only LPR3-system rows (avoid the LPR2 overlap)
lpr3_d <- read_register("lpr_a_diagnose") %>% rename_with(tolower)

lpr3_dm <- lpr3_k %>%
  semi_join(tibble(pnr = cohort_pnrs), by = "pnr") %>%
  select(pnr, dw_ek_kontakt, date_contact = kont_starttidspunkt) %>%  # LPR3's date column renamed to the SAME name
  inner_join(
    lpr3_d %>%
      filter(diag_kode_type %in% c("A", "B"),
             is.na(senere_afkraeftet) | senere_afkraeftet != "Ja",  # drop withdrawn diagnoses (LPR3 only)
             grepl(CODES_REGEX, diag_kode)) %>%
      select(dw_ek_kontakt, c_diag = diag_kode, c_diagtype = diag_kode_type),  # rename LPR3 names -> same names as LPR2
    by = "dw_ek_kontakt"                               # LPR3's contact key (NOT recnum as in LPR2)
  ) %>%
  collect() %>%
  mutate(date_contact = as.Date(date_contact),         # the LPR3 date is a datetime -> make it a plain date
         icd3 = substr(c_diag, 2, 4))

Step 3 - LPR2 psychiatric, only if you need F-codes (t_psyk_adm + t_psyk_diag). Skip this step entirely if your outcome is not an F-code.

psyk_adm  <- read_register("t_psyk_adm")  %>%
  rename_with(tolower) %>% rename(pnr = v_cpr, recnum = k_recnum)
psyk_diag <- read_register("t_psyk_diag") %>%
  rename_with(tolower) %>% rename(recnum = v_recnum)

lpr2_psyk_dm <- psyk_adm %>%
  semi_join(tibble(pnr = cohort_pnrs), by = "pnr") %>%
  select(pnr, recnum, date_contact = d_inddto) %>%
  inner_join(
    psyk_diag %>%
      filter(c_diagtype %in% c("A", "B"),
             grepl(CODES_REGEX, c_diag)) %>%          # same codes as above
      select(recnum, c_diag, c_diagtype),
    by = "recnum"
  ) %>%
  collect() %>%
  mutate(icd3 = substr(c_diag, 2, 4))

Step 4 - stack them. This works only because all three steps produced the same column names:

dm_dx <- bind_rows(lpr2_dm, lpr2_psyk_dm, lpr3_dm)   # drop lpr2_psyk_dm if you skipped step 3
# Columns: pnr | date_contact | c_diag | c_diagtype | icd3

What must match, and what you choose. The register’s own names (pnr, recnum, d_inddto, c_diag, c_diagtype, dw_ek_kontakt, kont_starttidspunkt, diag_kode) must be spelled exactly as they are in the register. The names on the left of = inside select() are yours: date_contact and icd3 are chosen here, not required. The renaming is the point - LPR2’s date column is d_inddto and LPR3’s is kont_starttidspunkt, so giving both the name date_contact is what lets bind_rows() stack them in step 4. Keep those harmonised names identical across the generations and the object names (lpr2_dm) can be whatever you like.

Many codes? Build the regex instead of typing it:

codes <- c("E10", "E11", "E12", "E13", "E14")
CODES_REGEX <- paste0("^D(", paste(codes, collapse = "|"), ")")

F-codes (dementia, depression)? Extend CODES_REGEX, e.g. "^DE1[0-4]|^DF0[0-3]|^DG30", and do not skip step 3 - F-diagnoses before March 2019 live only in the psychiatric registers.

Alternative: compact extraction (single-table approach)

A colleague may have shown you this shorter approach:

lpr <- left_join(lpr_adm, lpr_diag, by = "RECNUM") %>%
  filter(C_DIAGTYPE == "A",
         grepl("^S72", C_DIAG)) %>%
  group_by(PNR) %>%
  filter(D_INDDTO == min(D_INDDTO)) %>%
  slice(1) %>%
  ungroup()

It is shorter but has three pitfalls on DST data:

  1. D-prefix error: "^S72" does NOT match "DS72..." in DST data - returns zero rows with no error message. Use "^DS72" (with D) or strip the prefix first.
  2. left_join instead of inner_join: Keeps all admissions from lpr_adm - including those with no matching diagnosis. Unnecessarily heavy on national registers.
  3. No pnr filter: Loads the entire population’s data. Correct when building a cohort (Phase 10), not when extracting from an existing one.

Several outcomes at once

If your study has more than one LPR outcome, do not run the steps above once per outcome - that re-scans the registers every time. Query LPR once, keep all A and B diagnoses in a table called all_dx, and filter that table per outcome.

It is the same four steps, with three changes:

  1. Drop CODES_REGEX and the grepl() line from every step. You are no longer filtering on codes at the source.
  2. Name the results lpr2_dx, lpr3_dx, lpr2_psyk_dx and combine them into all_dx instead of dm_dx. Always include the psychiatric step here - all_dx is meant to serve outcomes you have not thought of yet.
  3. Save it, so later scripts reuse it rather than re-querying: saveRDS(all_dx, "path/to/all_dx.rds").

Everything else - the joins, the keys, the renames, bind_rows() - is unchanged.

all_dx is large. You are pulling every A and B diagnosis for your whole cohort into RAM. On a big cohort, save it and then work from the .rds rather than keeping it and the registers open at once. Free it with rm() + gc() when you are done (Phase 5).

Doing this by hand for each project is what get_lpr_diagnoses() below is for: it is the four steps wrapped up, with the code list as an argument.

Filter your extracted table for specific outcomes

all_dx has one row per diagnosis, so a person with five dementia contacts has five rows. An analysis dataset needs one row per person, with the date of their first qualifying diagnosis. The four lines from group_by() to ungroup() are what turn one into the other - see From event log to one row per person for the other way of collapsing.

library(dplyr) # filter, inner_join, group_by, arrange, slice, left_join

CODES <- c("G30", "F00", "F01", "F02", "F03")   # dementia - change to your outcome

outcome <- all_dx %>%
  # 1. Keep only the diagnoses that count as your outcome
  filter(icd3 %in% CODES) %>%

  # 2. Attach each person's index_date. inner_join also DROPS anyone who is not
  #    in the cohort, so this filters and adds a column in one step.
  #    Use cohort_clean here once you have excluded prevalent cases (Phase 10).
  inner_join(cohort %>% select(pnr, index_date), by = "pnr") %>%

  # 3. Keep only contacts AFTER index. This is what makes it an outcome rather
  #    than pre-existing disease. Use < instead for a baseline covariate.
  filter(date_contact > index_date) %>%

  # 4. Reduce to the FIRST event per person:
  group_by(pnr) %>%              #    handle each person separately
  arrange(date_contact) %>%      #    oldest contact first
  slice(1) %>%                   #    keep row 1 of each person = earliest contact
  ungroup() %>%                  #    remove the grouping again (see note below)

  # 5. Rename the surviving date to something that says what it is
  select(pnr, event_date = date_contact)

# Back onto the full cohort. left_join keeps EVERYONE, so people with no
# diagnosis get NA in event_date - that is the correct value for "no event",
# and they are censored at end of follow-up rather than dropped.
result <- cohort %>%
  select(pnr) %>%
  left_join(outcome, by = "pnr")

saveRDS(result, "path/to/extract_dementia.rds")   # change filename for each new outcome

Why slice(1) and not min(). slice(n) picks rows by position, not by value: slice(1) keeps the first row of each group. It only means “earliest” because arrange(date_contact) put the oldest first - swap that for arrange(desc(date_contact)) and the same slice(1) gives you the latest contact instead. See slice().

Two things that catch people out:

  • ungroup() is not optional. A grouped table stays grouped, and every later mutate() or summarise() silently operates per person instead of on the whole table.
  • Ties are broken arbitrarily. If someone has two qualifying contacts on the same date, slice(1) keeps whichever row came first, with no rule behind it. That is usually fine for a first-event date, since the date is identical either way - but not if you are also keeping columns that differ between those two rows, such as the hospital or the specific ICD code.

Excluding prevalent cases - people who already had the diagnosis before index date - happens in Phase 10. Once you have done that, use cohort_clean in place of cohort above.

Using duckplyr instead of dplyr? union_all() combines before collect() and needs identical column names and types, so rename the LPR3 columns to the LPR2 format first.

Try it yourself - runnable example with synthetic data

This example requires RStudio installed locally on your computer - not the DST server. The synthetic dataset (fakeregs) is not available on DST. Download R: cran.r-project.org · Download RStudio: posit.co/download/rstudio-desktop It uses open_dataset() on local synth_data/ folders; fastreg’s read_register() is for a configured DST project, not ad-hoc local folders.

The example extracts CVD diagnoses (ischaemic heart disease, ICD-10 I20–I25) from LPR2 and LPR3 combined - the complete pattern from the theory section above, but runnable locally with synthetic data. It follows the one-outcome route: specific codes are filtered out before collect().

The synthetic LPR data is generated with the fakeregs package, which you already know from Phase 6 - First extraction. If you have already generated and saved data there, synth_data/lpr_adm/ is ready and you can skip the preparation block.

Adapted from Anders Aasted Isaksen’s dev/common_tasks_datatable.qmd in fakeregs (MIT licence, Steno Diabetes Center Aarhus). Rewritten to dplyr + arrow and adapted to this guide’s pattern.

# Install fakeregs for the first time:
# install.packages("pak"); pak::pak("steno-aarhus/fakeregs")

library(fakeregs)   # synthetic DST register data
library(dplyr)      # filter, select, mutate, inner_join, bind_rows
library(arrow)      # open_dataset, write_parquet

# Preparation: generate synthetic data (run only once) -------------------------
bp             <- generate_background_pop()
lpr_adm_synth  <- generate_lpr_adm(background_df = bp)
lpr_diag_synth <- generate_lpr_diag(background_df = lpr_adm_synth)
lpr_a_k_synth  <- generate_lpr_a_kontakt(background_df = bp)
lpr_a_d_synth  <- generate_lpr_a_diagnose(background_df = lpr_a_k_synth)

dir.create("synth_data/lpr_adm",        recursive = TRUE, showWarnings = FALSE)
dir.create("synth_data/lpr_diag",       recursive = TRUE, showWarnings = FALSE)
dir.create("synth_data/lpr_a_kontakt",  recursive = TRUE, showWarnings = FALSE)
dir.create("synth_data/lpr_a_diagnose", recursive = TRUE, showWarnings = FALSE)
write_parquet(lpr_adm_synth,  "synth_data/lpr_adm/lpr_adm.parquet")
write_parquet(lpr_diag_synth, "synth_data/lpr_diag/lpr_diag.parquet")
write_parquet(lpr_a_k_synth,  "synth_data/lpr_a_kontakt/lpr_a_kontakt.parquet")
write_parquet(lpr_a_d_synth,  "synth_data/lpr_a_diagnose/lpr_a_diagnose.parquet")

The path is relative to your working directory - check with getwd(). If you have already run the preparation block in Phase 6, synth_data/lpr_adm/ is already saved.

# Extract CVD diagnoses (same four steps as above) -----------------------------
# The ICD codes we are looking for - change these to your own outcome
CVD_CODES <- c("I20", "I21", "I22", "I23", "I24", "I25")   # ischaemic heart disease

# LPR2 somatic (up to March 2019) ----------------------------------------------
lpr_adm  <- open_dataset("synth_data/lpr_adm/")  %>% rename_with(tolower)   # LPR2 contact table - synthetic
lpr_diag <- open_dataset("synth_data/lpr_diag/") %>% rename_with(tolower)   # LPR2 diagnosis table - synthetic

lpr2_cvd <- lpr_adm %>%
  select(pnr, recnum, date_contact = d_inddto) %>%           # select only necessary columns
  inner_join(
    lpr_diag %>%
      filter(c_diagtype %in% c("A", "B"),                    # only action and secondary diagnoses
             substr(c_diag, 2, 4) %in% !!CVD_CODES) %>%       # !! sends the local R vector to DuckDB
      select(recnum, c_diag),                    # only join key and diagnosis code
    by = "recnum"                                             # join key in LPR2
  ) %>%
  collect() %>%                                              # HERE data is fetched into R
  mutate(icd3 = substr(c_diag, 2, 4))                        # save cleaned code as new column

# LPR3 (March 2019 and onwards) ------------------------------------------------
lpr3_k <- open_dataset("synth_data/lpr_a_kontakt/")  %>% rename_with(tolower)   # LPR3 contact table - synthetic
lpr3_d <- open_dataset("synth_data/lpr_a_diagnose/") %>% rename_with(tolower)   # LPR3 diagnosis table - synthetic

lpr3_cvd <- lpr3_k %>%
  select(pnr, dw_ek_kontakt, date_contact = kont_starttidspunkt) %>%   # dw_ek_kontakt is join key to lpr_a_diagnose
  inner_join(
    lpr3_d %>%
      filter(diag_kode_type %in% c("A", "B"),
             is.na(senere_afkraeftet) | senere_afkraeftet != "Ja",  # exclude retracted diagnoses
             substr(diag_kode, 2, 4) %in% !!CVD_CODES) %>%   # !! sends the local R vector to DuckDB
      select(dw_ek_kontakt, c_diag = diag_kode),             # rename to c_diag for consistency with LPR2
    by = "dw_ek_kontakt"                                     # join key in LPR3
  ) %>%
  collect() %>%                                              # fetch into R
  mutate(
    date_contact = as.Date(date_contact),                    # datetime → date
    icd3         = substr(c_diag, 2, 4)                      # strip D-prefix: "DI21" → "I21"
  )

# Combine and save -------------------------------------------------------------
all_cvd <- bind_rows(lpr2_cvd, lpr3_cvd)                   # stack LPR2 and LPR3

nrow(all_cvd)                                               # check: number of diagnosis rows
length(unique(all_cvd$pnr))                                 # check: number of unique individuals
table(all_cvd$icd3)                                         # distribution across codes

saveRDS(all_cvd, "path/to/extract_cvd.rds")                # save - change path to your own folder

get_lpr_diagnoses() - the whole pattern as one function

Everything above is the same four steps with the code list swapped. Rather than copy them per outcome, wrap them once: get_lpr_diagnoses() takes the code list as an argument and returns the combined extract. Define it at the top of your script, or in a functions.R you source().

If you have more than one or two outcomes, this is the version to use.

  • One place to fix when a register or a column name changes, instead of one fix per copy
  • Each outcome becomes a single call rather than ~40 lines
  • It keeps the diagnosis-type column (c_diagtype), so you can narrow the case definition for a sensitivity analysis later (see Diagnosis types) without re-querying LPR

Read the four steps above first: when a column name in your delivery differs from the one the function assumes, you need to recognise which step it belongs to. On DARTER, DARTER - overview and pipeline carries this function already adapted, with the confirmed register names.

See the full get_lpr_diagnoses() function and usage
library(fastreg) # read_register - read a register by name
library(arrow)   # open_dataset - fallback without fastreg
library(dplyr)

# Function: get_lpr_diagnoses() - reusable LPR extract -------------------------
get_lpr_diagnoses <- function(pnr_vector, icd_codes = NULL, diagtypes = c("A", "B")) {
  # Arguments:
  #   pnr_vector : your cohort's pnr, e.g. unique(cohort$pnr)
  #   icd_codes  : optional vector of ICD codes to pull, e.g. c("E11", "I50") or your
  #                own code list. Matches on the D-prefix, so both 3-char
  #                ("E11" -> all E11x) and 4-char ("I700") work.
  #                Default NULL = pull ALL diagnoses (run once, save, filter locally).
  #   diagtypes  : "A"=action, "B"=secondary, "G"=grundmorbus (LPR2 only)
  # read_register (fastreg) reads each register by name - without fastreg: open_dataset("E:/workdata/[projectnumber]/cleaned-data/parquet-registers/<register>/") %>% rename_with(tolower)

  # keep_codes() applies the icd_codes filter to EACH of the three register queries
  # below, BEFORE collect(), so only the wanted rows are pulled (that's why it appears
  # three times). icd_codes = NULL -> no filter (pull everything).
  keep_codes <- function(x) {
    if (is.null(icd_codes)) return(x)                                # NULL = pull everything
    pattern <- paste0("^D(", paste(icd_codes, collapse = "|"), ")")  # D-prefix + code (3 or 4 chars)
    x %>% filter(grepl(pattern, c_diag))
  }


  # Open registers
  lpr_adm   <- read_register("lpr_adm")   %>% rename_with(tolower)   # LPR2 somatic contacts
  lpr_diag  <- read_register("lpr_diag")  %>% rename_with(tolower)   # LPR2 somatic diagnoses
  psyk_adm  <- read_register("t_psyk_adm")  %>% rename_with(tolower) %>%
    rename(pnr = v_cpr, recnum = k_recnum)                            # LPR2 psychiatric contacts
  psyk_diag <- read_register("t_psyk_diag") %>% rename_with(tolower) %>%
    rename(recnum = v_recnum)                                          # LPR2 psychiatric diagnoses
  lpr3_k    <- read_register("lpr_a_kontakt")  %>% rename_with(tolower) %>%
    filter(lprindberetningssystem == "LPR3")
  lpr3_d    <- read_register("lpr_a_diagnose") %>% rename_with(tolower)  # LPR3 diagnoses

  # LPR2 somatic
  lpr2_dx <- lpr_adm %>%
    semi_join(tibble(pnr = pnr_vector), by = "pnr") %>%
    select(pnr, recnum, date_contact = d_inddto) %>%
    inner_join(
      lpr_diag %>% filter(c_diagtype %in% !!diagtypes) %>% select(recnum, c_diag, c_diagtype),
      by = "recnum"
    ) %>%
    keep_codes() %>% # filter to icd_codes (before collect)
    collect() %>%
    mutate(icd3 = substr(c_diag, 2, 4))                       # strip D-prefix

  # LPR2 psychiatric
  lpr2_psyk_dx <- psyk_adm %>%
    semi_join(tibble(pnr = pnr_vector), by = "pnr") %>%
    select(pnr, recnum, date_contact = d_inddto) %>%
    inner_join(
      psyk_diag %>% filter(c_diagtype %in% !!diagtypes) %>% select(recnum, c_diag, c_diagtype),
      by = "recnum"
    ) %>%
    keep_codes() %>% # filter to icd_codes (before collect)
    collect() %>%
    mutate(icd3 = substr(c_diag, 2, 4))

  # LPR3
  lpr3_dx <- lpr3_k %>%
    semi_join(tibble(pnr = pnr_vector), by = "pnr") %>%
    select(pnr, dw_ek_kontakt, date_contact = kont_starttidspunkt) %>%
    inner_join(
      lpr3_d %>%
        filter(diag_kode_type %in% !!diagtypes,
               is.na(senere_afkraeftet) | senere_afkraeftet != "Ja") %>%
        select(dw_ek_kontakt, c_diag = diag_kode, c_diagtype = diag_kode_type),
      by = "dw_ek_kontakt"
    ) %>%
    keep_codes() %>% # filter to icd_codes (before collect)
    collect() %>%
    mutate(date_contact = as.Date(date_contact),               # datetime → date
           icd3 = substr(c_diag, 2, 4))

  bind_rows(lpr2_dx, lpr2_psyk_dx, lpr3_dx) %>%              # combine the three sources
    select(pnr, date_contact, c_diag, c_diagtype, icd3)      # identical columns, drop join keys (recnum/dw_ek_kontakt)
}

Use the function - one call per extraction, only change CODES:

cohort    <- readRDS("path/to/full_cohort.rds")
pnr_list  <- unique(cohort$pnr)

# Fetch all diagnoses for the cohort (Phase 1 - see hospital contacts page)
all_dx <- get_lpr_diagnoses(
  pnr_vector    = pnr_list,
  diagtypes     = c("A", "B")    # A=action, B=secondary. To also include grundmorbus, add "G" (LPR2 only): c("A", "B", "G")
)
# Returns: pnr | date_contact | c_diag | c_diagtype | icd3

# Extract one outcome from all_dx - repeat per outcome (only if you pulled ALL, i.e. no icd_codes)
CODES <- c("F00", "F01", "F02", "F03", "G30", "G31")   # dementia

dementia <- all_dx %>%
  filter(icd3 %in% CODES) %>%
  inner_join(cohort %>% select(pnr, index_date), by = "pnr") %>%
  filter(date_contact > index_date) %>%
  group_by(pnr) %>% arrange(date_contact) %>% slice(1) %>% ungroup() %>%
  select(pnr, dementia_date = date_contact)

result <- cohort %>% select(pnr) %>% left_join(dementia, by = "pnr")
saveRDS(result, "path/to/extract_dementia.rds")

Two ways to call the function (RAM). For a few codes, pass them via icd_codes - they are filtered BEFORE collect(), so only those rows are pulled: get_lpr_diagnoses(pnr_list, icd_codes = c("E11", "I50")). For many different outcomes, pull everything once (omit icd_codes), save with saveRDS(), free memory with rm() + gc() (Phase 5), then filter locally with readRDS() %>% filter(icd3 %in% ...) - so you don’t re-scan the registers for each outcome.

From event log to one row per person

get_lpr_diagnoses() (and the extracts above) return an event log: many rows per person, one per diagnosis. That is not analysis-ready. Two patterns turn it into one row per person:

A) One outcome with a date (e.g. time-to-event): filter to the codes, and take each person’s first contact - the slice(1) pattern above.

B) Many binary variables at once (e.g. comorbidity flags): build the flags, collapse with any(), and merge onto the cohort. any() returns TRUE if the person has at least one matching diagnosis in the window (0/1, not a count):

library(dplyr)
library(stringr)
library(tidyr) # replace_na

# 1. Build your flags (one expression per variable)
diagnosis_flags <- all_dx %>%
  filter(date_contact <= index_date) %>% # before index only (baseline covariate)
  mutate(
    dx_diabetes = icd3 %in% c("E10", "E11"),
    dx_copd = str_detect(c_diag, "^DJ4[1-4]"),
    dx_heart_failure = str_detect(c_diag, "^DI50")
  )

# 2. Collapse to one row per person: any() = does the person have at least one?
diagnosis_flags_collapsed <- diagnosis_flags %>%
  group_by(pnr) %>%
  summarise(across(starts_with("dx_"), ~ as.integer(any(.))), .groups = "drop")

# 3. Merge onto the cohort; people with no diagnosis get NA -> set to 0
cohort_diagnoses <- cohort %>%
  left_join(diagnosis_flags_collapsed, by = "pnr") %>%
  mutate(across(starts_with("dx_"), ~ replace_na(., 0)))

It gives one 0/1 flag per comorbidity per person, ready to enter your model as covariates. (The same group_by(pnr) %>% summarise(any(.)) pattern is used on the NMI page, for example.)

Remove unwanted diagnoses

A few codes are administrative artefacts rather than the patient’s own disease, and should typically be removed from both outcomes and comorbidity:

  • “Healthy companion” (someone admitted as a companion to another patient, e.g. a parent): ICD-10 DZ763. Related contact/observation codes with no disease: DZ032, DZ038, DZ039.
  • “Diagnosis not found”/unspecified from the ICD-8 era: Y719.
all_dx <- all_dx %>%
  filter(
    !substr(toupper(c_diag), 1, 5) %in% c("DZ763", "DZ032", "DZ038", "DZ039"),
    substr(toupper(c_diag), 1, 4) != "Y719"
  )

What to remove depends on your project and question: DZ03* (observation for suspected disease) is genuinely relevant in some studies and should be kept there. The pattern is adapted from the Plana-Ripoll group’s code on OSF; verify against your own data.

Next steps

You have now extracted diagnoses from two LPR generations. Next steps are to shape and combine your extracts:

Phase 12 - Assemble and prepare the dataset

See also

Back to top