Register paths and datastores

Confirmed paths and access methods for all registers on project 708421

Published

September 9, 2026

Check the modification date on cleaned-data before running the pipeline.

file.info("E:/workdata/708421/cleaned-data/parquet-registers/")$mtime

The registers are not necessarily updated to today. Confirm that coverage matches your study period.

Base paths

# All paths used as constants at the top of scripts
path_parquet_reg <- "E:/workdata/708421/cleaned-data/parquet-registers/"
path_parquet_ext <- "E:/workdata/708421/cleaned-data/parquet-external/"
path_output <- "E:/workdata/708421/workspaces/[yourName]/BS_demens/datasets/"

Overview - all registers on project 708421

This overview is written from the project’s own documentation and from what the DARTER team has passed on, last reviewed 2026-05-15. Most registers are updated to end of 2024 as of 2026 (Anders Aasted Isaksen/DARTER team). Column names are shown after rename_with(tolower). Check your own delivery before you rely on any row here.

Register Access Join key Period Critical column
BEF read_register("bef") pnr All years koen, foed_dag, familie_id
DOD read_sas() from rawdata - see below pnr 1970–2024 doddato (use this for death)
DODSAARS read_register("dodsaars") pnr ~1970–2001 d_dodsdto (stops in 2001 - see below)
VNDS read_register("vnds") pnr All years indud_kode, haend_dato
LPR2 contacts read_register("lpr_adm") recnum Up to March 2019 d_inddto, c_pattype
LPR2 diagnoses read_register("lpr_diag") recnum Up to March 2019 c_diag, c_diagtype
LPR2 psych contacts read_register("t_psyk_adm") k_recnumrecnum 1995–March 2019 v_cprpnr
LPR2 psych diagnoses read_register("t_psyk_diag") v_recnumrecnum 1995–March 2019 c_diag, c_diagtype
LPR3 contacts read_register("lpr_a_kontakt") dw_ek_kontakt March 2019+ kont_starttidspunkt (datetime)
LPR3 diagnoses read_register("lpr_a_diagnose") dw_ek_kontakt March 2019+ diag_kode, diag_kode_type, senere_afkraeftet
LPR3 procedures read_register("procedurer_kirurgi") no working key - see below 2019+ procedurekode, dato_start
LPR2 procedures read_register("lpr_sksopr") recnum 1996–2018 c_opr, d_odto
LPR2 examinations read_register("lpr_sksube") recnum 1999–2018 none delivered - see below
LMDB read_register("lmdb") pnr Approx. 1994+ atc, eksd
UDDA read_register("udda") pnr All years hfaudd, year
FAIK read_register("faik") familie_id All years famaekvivadisp_13
AKM read_register("akm") pnr All years socio13, year
DBSO read_register("dbso") pnr 2010+ datoper_prim, surgery flags
OSDC read_register("osdc") pnr 1977–2024 has_t1d/has_t2d, stable_inclusion_date
Laboratory results read_register("laboratorieproevesvar") cprnummer 2008+ analysiscode, samplingdate, samplevalue (character)

Critical notes

lpr_sksube has no procedure code on DARTER. The parquet holds three columns and no more:

colnames(read_register("lpr_sksube") %>% rename_with(tolower))
# "recnum" "d_odto" "year"

DST documents this register with c_opr, c_oprart, c_osgh and c_tilopr for every year 1999-2019, the same shape as lpr_sksopr. None of them are here, so the table can tell you that a procedure happened and on what date, but not which procedure. There is no filtering trick that recovers them.

If you need examinations and non-surgical treatments, ask for the columns to be added to the delivery. Surgical procedures are unaffected: lpr_sksopr has c_opr and d_odto as documented.

Deaths - the convenient register is the wrong one. dodsaars is in cleaned-data, so read_register("dodsaars") just works and it is the one you reach for first. It also stops at the end of 2001. dod (Døde i Danmark) is the register that covers deaths, 1970-2025 at DST, but on DARTER it is not in cleaned-data: it sits as a raw SAS file. Use dod and its doddato column for censoring at death, and see pitfall 1 - three death registers for the general version.

You do not need a separate extraction on DARTER: the raw file reads directly.

library(haven) # read_sas() - reads .sas7bdat straight into R
library(dplyr) # rename_with, select, mutate, semi_join, summarise

# DARTER-SPECIFIC PATH (project 708421). On another project the raw files sit
# somewhere else, may be named differently, and access to rawdata can depend on
# your project role - ask your data manager for the path to the death register.
dod <- read_sas("E:/rawdata/708421/Grunddata/dod2024.sas7bdat") %>%
  rename_with(tolower)

# This is a whole-population file read straight into RAM, so cut it to your
# cohort as soon as you have one.
deaths <- dod %>%
  semi_join(cohort, by = "pnr") %>%
  select(pnr, death_date = doddato) %>%
  mutate(death_date = as.Date(death_date))

# Check how far back the file actually reaches before you trust it:
deaths %>% summarise(min(death_date), max(death_date))

Do the range check. The file is named dod2024, and the row above says 1970-2024, but nobody on the DARTER team has confirmed the start year of this extract. If it turns out to begin in 2001, and your study period reaches further back, you need dodsaars for the early years as well:

dodsaars <- read_sas("E:/rawdata/708421/Grunddata/dodsaars2001.sas7bdat") %>%
  rename_with(tolower) %>%
  select(pnr, death_date = d_dodsdto) # note: different column name

deaths <- bind_rows(deaths, mutate(dodsaars, death_date = as.Date(death_date))) %>%
  distinct(pnr, .keep_all = TRUE) # one death date per person

Only do this if the range check says you have to. If both files cover 1970-2001, stacking them puts every pre-2001 death in twice, and distinct() then keeps whichever came first rather than the one you meant.

Why it matters: run on dodsaars alone and everyone who died after 2001 is treated as alive for the past 25 years. That skews censoring and matching in 01_build_cohorts.R, with no error message. See pitfall 1.

LPR3 - duplicate risk: lpr_a_kontakt and lpr_a_diagnose contain data from two formats (LPR_F and LPR_A). Always filter on lprindberetningssystem == "LPR3". See pitfall 5.

Laboratory results - use only one source: laboratorieproevesvar_ (>2.2 billion rows) replaces lab_forsker/lab_dm_forsker. The old files still exist and cover the same data - use only one to avoid duplicates. Because the register is so large, semi_join(tibble(pnr = cohort$pnr), by = "pnr") and select() before collect() are essential. Two things to watch when extracting:

  • Tests are identified by NPU codes in the analysiscode column - filter on the NPU codes your analysis needs.
  • samplevalue is a character column - it can contain text like “not detected” or “negative”, not just numbers. Convert with care (as.numeric() returns NA on text values).

See pitfall 7 - Laboratory results for a code example.

procedurer_kirurgi - the pnr join is not solved. This is a project-specific delivery of the LPR3 procedures; it is not a DST register, and it does not appear in DST’s register list. Like every procedure table it carries no pnr: it identifies procedures, not people, so you have to join to a contact table to get one. On DARTER neither route works cleanly.

Column Type Contents
procedurekode character SKS procedure code - match on this (e.g. "KJDF10")
dato_start Date Procedure date
dw_ek_kontakt character The intended join key - but NA for every row here (confirmed 2026-06-02)
dw_ek_forloeb character Course-level key - usable, but many NA
proceduretype character "P" = procedure, "+" = add-on code
procedurekode_parent character Parent procedure code
dato_slut Date Procedure end date
lprindberetningssystem character LPR reporting system
procedureregistrering_id character Internal registration ID

The empty keys are by design, but the pattern here is still wrong. Sundhedsdatastyrelsen’s guide to LPR3 for researchers states that LPR3 has four real join keys - dw_ek_helbredsforloeb, dw_ek_forloeb, dw_ek_kontakt and dw_ek_procedureregistrering - and that in a table where more than one of them could apply, “only the key that is relevant to join on is filled in”. A procedure attaches either to a single contact or to a whole course, and the other key is deliberately left empty.

So NA on one key is not a defect. What does not fit is the proportion: the same guide says there will be “relatively few rows with course procedures” in procedurer_kirurgi, yet here dw_ek_kontakt is NA on every row (confirmed 2026-06-02), which is the opposite of what the register should look like. That points at the extract rather than at LPR3.

The two keys are not the same level either: dw_ek_kontakt points at one contact, dw_ek_forloeb at a whole course, which can span several contacts. Swapping one for the other changes what a row means.

The documented route for course-level procedures, if you do not have the forloeb table, is to join on dw_ek_forloeb in the contacts table. That gets you to a person, but at course level, not contact level.

So there is still no verified way to attach pnr to a single LPR3 procedure through this file. Use lpr_a_procregistrering instead: it is the real DST register, it joins on dw_ek_kontakt, and DST even flags rows with no contact in flag_proc_uden_kont. Column names differ:

procedurer_kirurgi lpr_a_procregistrering
procedurekode proc_kode
dato_start proc_starttidspunkt (datetime)
dw_ek_forloeb dw_ek_kontakt (a working join key)

Confirm lpr_a_procregistrering is in your delivery before you rely on it.

Two more things from the same source, worth knowing before you plan around this table. procedurer_kirurgi is not a DARTER invention: it is Forskerservice’s own table name, and it holds surgical procedures only. Non-surgical ones, including indication codes, are in a separate table called procedurer_andre. And add-on codes get their own rows, with procedurekode_parent and proceduretype_parent saying which procedure they belong to, so counting rows counts add-ons as procedures.

DBSO: The identifier column is cpr in raw parquet - renamed to pnr by 00_prepare_dbso.R. All code uses pnr after that.

OSDC: a classified diabetes population is already computed and lies ready on the project, so you normally do not run the algorithm yourself - but you can, if you need to change the definitions. read_register("osdc") finds it, but only once fastreg has been pointed at the project folders (see loading templates); without that it will not resolve the name. Both routes are described on Diabetes population (OSDC); the package itself is documented at steno-aarhus.github.io/osdc.

The one thing worth knowing here: the output has two inclusion dates, and picking between them is a study decision, not a formality. raw_inclusion_date is the person’s second inclusion event; stable_inclusion_date is the same date but NA when it falls before 1998, where the underlying data is not considered reliable. Filter on the stable one and your early cases disappear without a word.

Loading templates

fastreg replaces dstDataPrep on DARTER. Registers are now loaded with fastreg::read_register("name") - the same function as in the general guide. If you have been using dstDataPrep::load_database(), that code still runs, but write new code with fastreg. You point fastreg at DARTER’s folders once per script - both of them, per fastreg’s own documentation:

options(
  fastreg.project_workdata_dir = "E:/workdata/708421/cleaned-data/",
  fastreg.project_rawdata_dir = "E:/rawdata/708421/"
)

Then read_register("bef") works by name. See “Didn’t convert the data yourself?” in Parquet and fastreg for the general version.

library(fastreg) # read_register() - access to DST registers
library(dplyr) # rename_with, rename, left_join, select

# Standard register - via read_register:
bef <- read_register("bef") %>% rename_with(tolower) # lazy connection; lowercase columns

# Psychiatric LPR2 - requires renaming v_cpr and k_recnum:
psyk_adm <- read_register("t_psyk_adm") %>%
  rename_with(tolower) %>% # lowercase columns
  rename(pnr = v_cpr, recnum = k_recnum) # v_cpr → pnr; k_recnum → recnum

# DBSO - parquet-external (converted from SAS via 00_prepare_dbso.R):
dbso <- read_register("dbso") %>% rename_with(tolower) # lazy connection

# OSDC - pre-computed diabetes classification (see osdc.qmd):
dm_pop <- read_register("osdc") # pnr, has_t1d, has_t2d, raw_/stable_inclusion_date

# LPR3 procedures - join via dw_ek_forloeb (NOT dw_ek_kontakt - is NA for all rows):
proc <- read_register("procedurer_kirurgi") %>%
  rename_with(tolower) %>% # lowercase columns
  left_join(
    read_register("lpr_a_kontakt") %>%
      rename_with(tolower) %>%
      select(dw_ek_forloeb, pnr), # fetch pnr via the forloeb key
    by = "dw_ek_forloeb" # join key - dw_ek_kontakt does not work
  )
# proc is still lazy - add filter() and collect() before use

See also

Back to top