Saml dine udtræk

Joins og pivots — klargør dit analysedatasæt

Published

June 6, 2026

Du ankommer til denne fase med separate RDS-filer: din kohort fra Fase 10, event-datoer fra Fase 9, kovariater fra Fase 6 og socioøkonomiske variable fra Fase 13. Du forlader med ét analyseklart datasæt klar til Fase 14.

Tip

Kort fortalt: Form hvert udtræk til én række per person (med pivot_wider() eller group_by() + slice()), og kobl dem så sammen på pnr med left_join() til ét analyseklart datasæt.

Inden du kan analysere, skal du:

  1. Forme hvert udtræk til én række per person (de fleste registre er i langt format med mange rækker per person)
  2. Samle dem til ét analyseklart datasæt med left_join()

Det er det, denne fase handler om. Teknikken til trin 1 er enten pivot_wider() eller group_by() + slice() — valget afhænger af, hvad du vil have ud af registret. Trin 2 er altid left_join().

Note

Kodeeksemplerne bruger generiske stier og variabelnavne. Tilpas til dit projekts mappestruktur og kolonnenavne.


Hvad er et analyseklart datasæt?

Et analyseklart datasæt har én række per person og én kolonne per variabel — det vi kalder bredt format:

pnr index_date censor_date followup_years event alder koen nmi_score
001 2015-03-01 2022-07-14 7.4 0 58 2 12
002 2016-11-20 2019-11-20 3.0 1 63 1 7
Note

Eksemplet bruger et kohortestudie med exposed, event og followup_years. For et prævalensstudie vil kolonnestrukturen se anderledes ud — typisk uden opfølgningstid, men med en tværsnitsdato og kovariater målt på det tidspunkt. Teknikken i denne fase (joins, pivots, group_by() + slice()) er den samme uanset studiedesign.

Registrene du trækker fra — LPR, LMDB, BEF — er næsten altid i langt format med mange rækker per person. Denne fase viser, hvordan du former dem om og samler dem.

Hvad har du allerede gemt?

Hvert udtræk fra Fase 6, 9 og 12 m.fl. bør gemmes som sin egen RDS-fil — ét register, ét emne. Eksempel på hvad du typisk har:

Fil Indhold Nøglekolonner
full_cohort.rds Kohorte med index-dato pnr, index_date, exposed
extract_demens.rds Første demensdiagnose pnr, event_date (NA = ingen)
extract_emigration.rds Første emigration pnr, emigration_date (NA = ikke emigreret)
extract_bef.rds Demografi ved index pnr, alder, koen
extract_nmi.rds Komorbiditet ved baseline pnr, nmi_score
extract_ses.rds Socioøkonomiske variable pnr, udda, indkomst, socio

Målet med denne fase er at samle dem til ét datasæt med left_join() — én ny kolonne per fil. Kohorten er rygraden: hvert udtræk kobles på via pnr og tilføjer sine kolonner — alle kohortemedlemmer bevares, og personer uden match får NA.

Important

Saml kun det du skal bruge. Det er fristende at beholde alle kolonner “for en sikkerheds skyld”. Gør det ikke. Afslut altid med select() og vælg eksplicit de kolonner analysen kræver. Et datasæt med kun de nødvendige kolonner er langt lettere at debugge og forstå.


To redskaber til at forme data

De to centrale operationer i denne fase er pivots og joins — de ligner hinanden i navn, men gør noget fundamentalt forskelligt:

Pivots Joins
Hvad det gør Former én tabel om — ændrer formen, ikke indholdet Kobler to tabeller — tilføjer kolonner fra en anden tabel
Input Én tabel To tabeller med en fælles nøgle (typisk pnr)
Output Samme data, ny form Bredere tabel med kolonner fra begge
Brugt til Langt → bredt format (pivot_wider) eller bredt → langt (pivot_longer) Saml kohort + udfald + kovariater til ét datasæt

Tommelfingerregel: Brug pivot når du vil ændre formen på én tabel. Brug join når du vil kombinere to tabeller.

Pivots · Joins


Bredt vs. langt format

Forestil dig tre personer med målinger af vægt ved to tidspunkter.

Bredt (wide) format — én række per person, én kolonne per tidspunkt:

pnr vaegt_baseline vaegt_6mdr
001 120 95
002 105 88
003 98 80

Langt (long) format — én række per observation, én kolonne til tidspunkt og én til værdi:

pnr tidspunkt vaegt
001 baseline 120
001 6mdr 95
002 baseline 105
002 6mdr 88
003 baseline 98
003 6mdr 80

Begge formater indeholder de samme oplysninger — de er bare arrangeret forskelligt.


Hvor er der hvilke formater?

Register Format Hvorfor
BEF Langt Én række per person per snapshot — kvartalsvis siden 2008, ellers årligt
LPR (lpr_adm + lpr_diag) Langt Én række per diagnose per kontakt — en indlæggelse kan have 10 diagnoser
LMDB Langt Én række per ekspederet recept — samme person kan have hundredvis
DODSAARS Bredt Én række per afdød person — allerede én-per-person
Dit analysedatasæt Bredt Én række per person — én kolonne per kovariat

Registre er oftest i langt format. Dit analysedatasæt skal typisk være i bredt format.


Pivots — skift mellem langt og bredt

Langt → bredt med pivot_wider()

library(tidyr)   # pivot_wider, pivot_longer

df_bredt <- df_langt %>%
  pivot_wider(
    names_from  = "tidspunkt",  # hvilken kolonne skal blive til kolonnenavne?
    values_from = "vaegt"       # hvilken kolonne indeholder værdierne?
  )
Warning

Du kan IKKE pivot_wider() en hel LPR-tabel. LPR har potentielt hundredvis af unikke ICD-koder per person — pivot_wider() ville give dig én kolonne per kode, dvs. tusindvis af kolonner og et datasæt der er umuligt at arbejde med.

I stedet bruger du mønstret fra Fase 9: udtræk første dato for én specifik diagnosegruppe, gem som extract_demens.rds, og join den som én kolonne med left_join(). Én udfaldsvariabel = én kolonne = én RDS-fil.

Praktisk eksempel: beskæftigelsesstatus fra AKM — langt til bredt

AKM (arbejdsklassifikationsmodulet) gemmer én række per person per år — langt format. Vil du sammenligne beskæftigelsesstatus ved baseline og opfølgning side om side, kan du pivotere:

library(dplyr)   # filter, mutate
library(tidyr)   # pivot_wider

# AKM i langt format — to år per person:
akm_langt <- data.frame(
  pnr   = c("001", "001", "002", "002", "003", "003"),
  aar   = c(2015, 2020, 2015, 2020, 2015, 2020),
  socio = c(110, 110, 210, 310, 150, 150)   # beskæftigelseskategorier fra AKM (socio13)
)

# Konverter til bredt — én kolonne per år:
akm_bredt <- akm_langt %>%
  pivot_wider(
    names_from   = aar,
    values_from  = socio,
    names_prefix = "socio_"   # giver kolonner: socio_2015, socio_2020
  )

# Resultat:
#   pnr   socio_2015  socio_2020
#   001          110         110
#   002          210         310
#   003          150         150

I praksis vil du typisk ikke pivotere AKM — du filtrerer til ét specifikt år med filter(aar == index_aar) og bruger group_by() + slice() nedenfor. Pivot er relevant, når du eksplicit vil have to tidspunkter side om side i analysen.

Bredt → langt med pivot_longer()

df_langt <- df_bredt %>%
  pivot_longer(
    cols      = c(vaegt_baseline, vaegt_6mdr),  # kolonner der skal "foldes ind"
    names_to  = "tidspunkt",                    # navn på den nye tidspunkt-kolonne
    values_to = "vaegt"                         # navn på den nye værdi-kolonne
  )

Klargør til join — group_by() + slice()

left_join() virker forudsigeligt, når begge tabeller har én række per pnr. Mange registerudtræk har flere rækker per person (langt format). group_by() + slice() er den primære måde at reducere et datasæt til én repræsentativ række per person inden join — det er forberedelse til join, ikke selve join.

Mønstret er altid det samme: gruppér på pnr, sortér så den ønskede række er øverst, tag første række per gruppe.

# BEF: behold kun den nyeste post per person (BEF har ét snapshot per år)
bef_en_per_person <- bef %>%
  group_by(pnr) %>%          # gruppér: alle rækker med samme pnr behandles sammen
  arrange(desc(aar)) %>%     # sortér inden for gruppen: nyeste år øverst (desc = faldende)
  slice(1) %>%               # tag kun den første række per gruppe — dvs. det nyeste år
  ungroup()                  # VIGTIGT: frigiv gruppering efter du er færdig
Important

Husk ungroup() — altid. group_by() sætter et “grouped_df”-mærke på din data.frame, som alle efterfølgende operationer arver. Det betyder: - mutate() beregner per gruppe, ikke per hele datasæt - n() tæller rækker per gruppe, ikke i alt - summarize() giver ét resultat per gruppe

Disse fejl er svære at opdage, fordi koden kører uden fejlbeskeder og giver tilsyneladende fornuftige tal. ungroup() fjerner mærket og gør data.frame normal igen. Tommelfingerregel: afslut altid en group_by()-kæde med ungroup().

# LPR / alle_dx: find første diagnose per person (til udfaldsdefinition)
foerste_demens <- alle_dx %>%
  filter(icd3 %in% c("G30", "F00", "F01", "F02", "F03")) %>%   # kun demenskoder
  group_by(pnr) %>%              # gruppér per person
  arrange(date_contact) %>%      # ældste dato øverst (stigende — ingen desc)
  slice(1) %>%                   # første diagnose per person
  ungroup()                      # frigiv gruppering
# → klar til: kohort %>% left_join(foerste_demens, by = "pnr")
# VNDS: find første udvandring per person (en person kan have udrejst og indrejst igen)
foerste_emigration <- vnds %>%
  filter(indud_kode == "U") %>%   # kun udrejser
  group_by(pnr) %>%
  arrange(haend_dato) %>%
  slice(1) %>%
  ungroup()
Tip

Hvornår bruger du group_by() + slice() vs pivot_wider()? Vil du have én repræsentativ værdi per person (nyeste snapshot, første hændelse, tidligste dato) — brug group_by() + slice(). Vil du have to eller flere tidspunkter side om side som separate kolonner i analysen — brug pivot_wider(). I registerbaseret forskning er group_by() + slice() langt det hyppigste valg.


Joins — kobl to tabeller sammen

I registerbaseret forskning arbejder du næsten altid med data fra to eller flere tabeller, der skal kobles sammen. Alle joins i dplyr tager to tabeller og én nøgle-kolonne, de deler (fx pnr).

Dine udtræk deler typisk alle pnr som nøgle — det er den, du kobler på. Et simpelt ER-diagram over nøglerne:

erDiagram
    full_cohort ||--o| extract_demens : pnr
    full_cohort ||--o| extract_bef : pnr
    full_cohort ||--o| extract_nmi : pnr
    full_cohort {
        string pnr PK
        date index_date
        int exposed
    }
    extract_demens {
        string pnr FK
        date event_date
    }
    extract_bef {
        string pnr FK
        int alder
        int koen
    }
    extract_nmi {
        string pnr FK
        int nmi_score
    }

pnr er primærnøgle (PK) i kohorten og fremmednøgle (FK) i hvert udtræk. ||--o| betyder: hver person i kohorten har nul eller én matchende række i udtrækket.

De joins du vil bruge

library(dplyr)   # left_join, inner_join, semi_join, anti_join

# left_join: behold ALLE rækker fra x, tilføj kolonner fra y
# Personer i x uden match i y får NA.
resultat <- kohort %>%
  left_join(udfald, by = "pnr")   # alle kohort-medlemmer bevares; udfald = NA hvis ingen hændelse

# inner_join: behold kun rækker med match i BEGGE tabeller
resultat <- lpr_adm %>%
  inner_join(lpr_diag, by = "recnum")   # kun kontakter med mindst én diagnose

# right_join: behold alle rækker fra y (sjældent brugt)
# full_join: behold alle rækker fra begge (sjældent brugt)

Filtreringsjoins — ændr ikke kolonner, kun rækker

# semi_join: behold rækker i x SOM HAR match i y
bef_i_kohort <- bef %>%
  semi_join(kohort, by = "pnr")   # kun BEF-rækker for kohort-medlemmer

# anti_join: behold rækker i x SOM IKKE HAR match i y
bef_ikke_i_kohort <- bef %>%
  anti_join(kohort, by = "pnr")   # kun BEF-rækker for ikke-kohort-medlemmer
                                  # bruges også som diagnostik — se "Validér dit join" nedenfor
Oversigt: alle joins i én tabel
Funktion Beholder rækker fra Brugt til
left_join(x, y) Alle fra x Tilføj udfald/kovariater til kohorte — NA hvis ingen match
inner_join(x, y) Kun match i begge Join kontakter og diagnoser — vil kun have rækker med begge
right_join(x, y) Alle fra y Sjældent
full_join(x, y) Alle fra begge Sjældent
semi_join(x, y) Kun x med match i y Filtrer et register til kun kohortemedlemmer
anti_join(x, y) Kun x uden match i y Find alle i BEF der IKKE er i kohorten

Join med flere nøgler

Når to tabeller deler mere end én kolonne som nøgle:

# Join på pnr OG år
bef_akm <- bef %>%
  left_join(akm, by = c("pnr", "aar"))

Validér dit join

Joins fejler sjældent med en fejlbesked — men de kan give stille forkerte resultater. Tjek altid disse tre ting efter et left_join():

# Inden join: hvad er nrow?
nrow(kohort)                                   # fx 4.823

# Join:
kohort2 <- kohort %>% left_join(udfald, by = "pnr")

# Tjek 1: steg rækketallet? Duplikerede nøgler i udfald giver ekstra rækker.
nrow(kohort2)                                  # skal stadig være 4.823

# Tjek 2: hvor mange fik et match?
sum(!is.na(kohort2$event_date))                # antal med udfald
sum(is.na(kohort2$event_date))                 # antal uden — forventede censurerede?

# Tjek 3: hvem matchede IKKE? (diagnostisk — ikke nødvendigvis en fejl)
mangler <- anti_join(kohort, udfald, by = "pnr")   # pnr'er der ikke fandtes i udfald
nrow(mangler)                                  # 0 = alle matchede; > 0 = undersøg
Tip

anti_join() som diagnostik anti_join(kohort, udfald, by = "pnr") returnerer de kohortepersoner der ikke findes i udfald-tabellen. Bruges til “hvem i min kohorte har ingen dødsdato-record?” (alle levende — forventet) eller “hvem mangler i register X?” (uforventet — undersøg).


Sæt tabeller lodret sammen — bind_rows()

bind_rows() og left_join() løser to fundamentalt forskellige problemer:

bind_rows() left_join()
Hvad den gør Stacker tabeller lodret — tilføjer rækker Kombinerer tabeller vandret — tilføjer kolonner
Kræver At kolonnerne hedder det samme At tabellerne deler én fælles nøglekolonne
Resultat Flere rækker, samme antal kolonner Samme antal rækker, flere kolonner
Matcher på nøgle? Nej — alle rækker fra begge tabeller Ja — match på pnr eller anden nøgle
Typisk brug LPR2 + LPR3 → ét samlet register Kobl udfald eller kovariat til kohortetabel

bind_rows() er ikke et join. Den ser ikke på pnr og matcher ikke. Den sætter simpelthen tabel 2’s rækker under tabel 1’s rækker — som at klistre to Excel-ark sammen lodret.

# bind_rows: LPR2 og LPR3 har samme kolonner (pnr, date_contact, icd3)
# → sæt dem sammen lodret til ét samlet diagnoseregister
lpr2_dx    # 45.000 rækker — diagnoser frem til marts 2019
lpr3_dx    # 32.000 rækker — diagnoser fra marts 2019 og frem

alle_dx <- bind_rows(lpr2_dx, lpr3_dx)   # 77.000 rækker — begge perioder samlet

Kolonner der mangler i den ene tabel (fx en kolonne der kun findes i LPR3) fyldes automatisk med NA for rækker fra den anden tabel.

Note

Hvornår bruger du hvad? bind_rows() — når du har to versioner af det samme register (LPR2 + LPR3, eksponerede + sammenligningskohorte-medlemmer) og vil slå dem sammen til ét. left_join() — når du vil tilknytte en ny variabel (udfald, dødsdato, alder) til din kohortetabel.


Praktiske eksempler: kobl dødsdato og emigration til kohorten

Dødsdato (DODSAARS)

# Kohort-data (én række per person):
kohort <- readRDS("datasets/full_cohort.rds")

# Dødsdatoer fra DODSAARS:
dodsaars <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/dodsaars/") %>%
  rename_with(tolower) %>%               # standardisér kolonnenavne
  filter(pnr %in% !!kohort$pnr) %>%     # kun kohortens pnr'er
  select(pnr, death_date = d_dodsdto) %>%   # omdøb d_dodsdto til death_date
  collect()                              # hent ind i R

# Join: alle kohort-medlemmer bevares; de levende får death_date = NA
kohort_med_dod <- kohort %>%
  left_join(dodsaars, by = "pnr")       # tilknyt dødsdato — NA = fortsat i live

Emigrationsdato (VNDS)

Emigration censurerer ligesom død — personen forlader studiet den dag de udvandrer. VNDS indeholder én række per migrationsbegivenhed; brug kun "U" (udvandring) og tag den første dato per person.

# Emigrationsdatoer fra VNDS:
vnds <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/vnds/") %>%
  rename_with(tolower) %>%                       # standardisér kolonnenavne
  filter(pnr %in% !!kohort$pnr,
         indud_kode == "U") %>%                  # kun udvandringshændelser
  select(pnr, emigration_date = haend_dato) %>%  # omdøb haend_dato til emigration_date
  collect() %>%                                  # hent ind i R
  group_by(pnr) %>%                              # gruppér for at finde første udvandring
  arrange(emigration_date) %>%                   # ældste dato først
  slice(1) %>%                                   # første emigration per person
  ungroup()                                      # frigiv gruppering

# Join: alle kohort-medlemmer bevares; ikke-emigrerede får emigration_date = NA
kohort_med_emigration <- kohort %>%
  left_join(vnds, by = "pnr")                    # NA = aldrig emigreret i studieperioden

Beregn opfølgningstid og hændelsesvariabel

Inden du kan analysere, skal hvert kohortemedlem have en censureringsdato (hvornår opfølgningen slutter) og en hændelsesvariabel (fik de udfaldet?).

Censureringsdatoen er den tidligste af: hændelses-dato, dødsdato, emigrationsdato og studieslutningstidspunkt.

studie_slut <- as.Date("2024-12-31")   # erstat med din faktiske studieslutningstidspunkt
                                        # format: "åååå-mm-dd" (ISO 8601 — R's standard)

kohort <- kohort %>%
  mutate(
    # Censureringsdato = det tidligst indtrufne af alle mulige stopårsager
    censor_date = pmin(event_date, death_date, emigration_date,
                       studie_slut, na.rm = TRUE),

    # Opfølgningstid i år
    followup_years = as.numeric(censor_date - index_date) / 365.25,

    # Hændelsesvariabel: 1 = udfald indtruffet inden censurering, 0 = censureret
    event = as.integer(!is.na(event_date) & event_date <= censor_date)
  )
Note

pmin() sammenligner vektorer position for position og returnerer den mindste værdi per person — det er den vektoriserede version af min(). na.rm = TRUE sikrer at en manglende dødsdato (= levende) ikke gør censureringsdatoen til NA.


Byg din studiepopulation

Nu har du alle byggeklodserne — udtræk (Fase 6), registre (Fase 8), LPR/SKS (Fase 9) og joins (ovenfor) — til at samle din studiepopulation. Det sker i fire trin:

  1. Definer den eksponerede kohorte. Filtrér det register der definerer eksponeringen — fx bariatri-operationer via SKS-koder i lpr_sksopr / procedurer_kirurgi (Fase 9). Behold pnr og index-dato (operationsdatoen).
  2. Byg sammenligningskohorten. Match kontroller til de eksponerede på fx alder, køn og kalenderår. Kontrollerne tildeles den matchede eksponeredes index-dato.
  3. Anvend de samme inklusions-/eksklusionskriterier på begge grupper, og saml dem (bind_rows()).
  4. Kobl udfald og kovariater på med left_join() (én række per person), beregn opfølgningstid, og afslut med select().
Important

Afslut med select() — behold kun de kolonner analysen kræver.

kohort_final <- kohort %>%
  select(pnr, index_date, censor_date, followup_years, event,
         alder, koen, nmi_score, occupation_cat, education_cat, income_cat)

saveRDS(kohort_final, "datasets/analysis_dataset.rds")   # gem det endelige datasæt
Tip

Designvalgene (hvem er sammenligningskohorten, immortal time, matching-ratio) hører til planlægningen — se Fase 1 — Sammenligningskohorten. Selve kohorteopbygningen og matchingen med heaven::riskSetMatch() — inkl. faldgruber som immortal time — er i Fase 10 — Byg din studiepopulation.

Komplet opskrift — fra kohort til analyseklart datasæt
# Komplet opskrift: alle trin i rækkefølge

# 0. Indlæs din kohorte (pnr + index_date, opbygget i Fase 6/9)
kohort <- readRDS("datasets/full_cohort.rds")   # én række per person

# 1. Ekskluder prævalente tilfælde FØR udfaldsudtrækket (se Fase 9)
# ... kohort_renset <- kohort %>% anti_join(praevalente, by = "pnr")

# 2. Kobl udfald (fx første demensdiagnose efter index-dato)
udfald  <- readRDS("datasets/extract_demens.rds")   # pnr + event_date (NA = ingen hændelse)
kohort <- kohort_renset %>% left_join(udfald, by = "pnr")

# 3. Kobl censurering: dødsdato og emigration
deaths <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/dodsaars/") %>%
  rename_with(tolower) %>%
  filter(pnr %in% !!kohort$pnr) %>%
  select(pnr, death_date = d_dodsdto) %>% collect()

vnds_data <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/vnds/") %>%
  rename_with(tolower) %>%
  filter(pnr %in% !!kohort$pnr, indud_kode == "U") %>%
  select(pnr, emigration_date = haend_dato) %>% collect() %>%
  group_by(pnr) %>% arrange(emigration_date) %>% slice(1) %>% ungroup()

kohort <- kohort %>%
  left_join(deaths,    by = "pnr") %>%
  left_join(vnds_data, by = "pnr")

# 4. Beregn opfølgningstid og hændelsesvariabel
studie_slut <- as.Date("2024-12-31")
kohort <- kohort %>%
  mutate(
    censor_date    = pmin(event_date, death_date, emigration_date, studie_slut, na.rm = TRUE),
    followup_years = as.numeric(censor_date - index_date) / 365.25,
    event          = as.integer(!is.na(event_date) & event_date <= censor_date)
  )

# 5. Kobl kovariater (demografiske, SES, komorbiditet)
bef_data <- readRDS("datasets/extract_bef.rds")    # alder, køn fra BEF
ses_data <- readRDS("datasets/extract_ses.rds")    # uddannelse, indkomst, beskæftigelse
nmi_data <- readRDS("datasets/extract_nmi.rds")    # NMI-score

kohort <- kohort %>%
  left_join(bef_data, by = "pnr") %>%
  left_join(ses_data, by = "pnr") %>%
  left_join(nmi_data, by = "pnr")

# 6. Behold KUN de kolonner analysen kræver
kohort_final <- kohort %>%
  select(pnr, index_date, censor_date, followup_years, event,
         alder, koen, nmi_score, occupation_cat, education_cat, income_cat)

saveRDS(kohort_final, "datasets/analysis_dataset.rds")   # gem det endelige datasæt
nrow(kohort_final)                                        # verificér antal personer
names(kohort_final)                                       # verificér kolonnenavne

Næste skridt

Du kan nu forme og kombinere dine udtræk. Næste skridt er de socioøkonomiske variable:

Fase 13 — Socioøkonomiske variable

Se også

Back to top