Kobl udtræk sammen

Joins, bind_rows() og beregning af opfølgningstid - saml dine udtræk til ét datasæt

Published

July 2, 2026

Dine udtræk fra Fase 6, Udtræk fra LPR og Fase 11 ligger som separate RDS-filer. Når hvert udtræk er nede på én række per person (Langt ↔︎ bredt format), kobler du dem sammen på pnr til ét analyseklart datasæt. Til sidst beregner du opfølgningstid og hændelsesvariabel.

Note

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

Important

Få først hvert udtræk ned til én række per person. left_join() virker kun forudsigeligt, når begge tabeller har én række per pnr. Mange registerudtræk har flere rækker per person (langt format). Reducér dem til én række per person med group_by() + slice() eller pivot_wider() - det er forklaret i Langt ↔︎ bredt format. Denne side antager, at dine udtræk allerede er på den form.


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).

Når du kobler dine færdige person-niveau udtræk på kohorten, er nøglen som regel pnr. Inde i et register er det andre nøgler: kobler du fx kontakt og diagnose i LPR, sker det på recnum eller dw_ek_kontakt, ikke pnr (se oversigten over join-nøgler i LPR). ER-diagrammet nedenfor viser den almindelige kobling på pnr - udtræk koblet på kohorten:

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. Symbolet ||--o| er standard ER-notation (“crow’s foot”): || = præcis én, o| = nul eller én. Linjen læses altså: hver person i kohorten matcher nul eller én række i udtrækket.

Det beskriver den form, du selv skal sikre inden join - ikke noget left_join() tjekker eller retter. Har et udtræk flere rækker per pnr, kobler left_join() glad videre og duplikerer kohorte-rækkerne (række-eksplosion). Derfor callout’en ovenfor om at få hvert udtræk ned til én række per person først, og derfor tjek 1 i Validér dit join bagefter (steg rækketallet?).

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

Flere nøgler bliver kun relevant, når mindst én af tabellerne har flere rækker per person - ellers er pnr alene nok. Med flere nøgler skal alle de angivne kolonner stemme overens, før to rækker tælles som et match (pnr og aar, ikke pnr eller aar).

Det typiske tilfælde er panel-data: registre med én række per person per år. BEF, AKM, FAIK osv. har en pnr-række for hvert kalenderår. Kobler du to af dem på pnr alene, parres hver af personens rækker med alle personens rækker i den anden tabel: har personen 5 år i BEF og 5 år i AKM, får du alle kombinationer, 5 × 5 = 25 rækker i stedet for 5. Tilføjer du året som ekstra nøgle, filtreres de 25 ned til de 5, hvor året også stemmer - 2018 mod 2018:

# Join på pnr OG år: en række matcher kun når BEGGE kolonner er ens
bef_akm <- bef %>%
  left_join(akm, by = c("pnr", "aar"))   # 2018-BEF kobles kun til 2018-AKM

Kobler du derimod to udtræk der hver har én række per person (fx færdige person-niveau udtræk på kohorten), er pnr alene nok - der er ingen ekstra dimension at holde styr på.


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)

#=====================================================
# Kobl dødsdato (DODSAARS) til kohorten
#=====================================================
# Kohort-data (én række per person):
kohort <- readRDS("sti/til/full_cohort.rds")

# Dødsdatoer fra DODSAARS:
dodsaars <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/dodsaars/") %>%
  rename_with(tolower) %>%               # standardisér kolonnenavne
  semi_join(tibble(pnr = kohort$pnr), by = "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.

#=====================================================
# Kobl emigrationsdato (VNDS) til kohorten
#=====================================================
# Emigrationsdatoer fra VNDS:
vnds <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/vnds/") %>%
  rename_with(tolower) %>%                       # standardisér kolonnenavne
  semi_join(tibble(pnr = kohort$pnr), by = "pnr") %>%   # kun kohortens pnr'er
  filter(indud_kode == "U") %>%                  # "U" = udvandring (VNDS har både ind- og udvandring)
  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.

#=====================================================
# Beregn opfølgningstid og hændelsesvariabel
#=====================================================
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.

Konkurrerende risici? Skal en konkurrerende hændelse (typisk død) håndteres særskilt (se Time-to-event), så lav i stedet for event (0/1) en status med tre værdier - hvad der skete først:

#=====================================================
# Konkurrerende risici: status med tre værdier
#=====================================================
kohort <- kohort %>%
  mutate(
    # status_kode: hvilken dato "vandt" pmin() ovenfor?
    status_kode = case_when(
      !is.na(event_date) & event_date <= censor_date ~ 1,   # udfald først
      !is.na(death_date) & death_date <= censor_date ~ 2,   # død først (konkurrerende risiko)
      TRUE                                           ~ 0     # ellers censureret (emigration/studieslut)
    )
  )

Saml det endelige analysedatasæt

Nu er din kohorte bygget i Fase 10: en tabel med én række per person, pnr og index-dato (og exposed/case, hvis du har en sammenligningsgruppe). Dine udfald, kovariater og censurerings-variable er trukket ud som separate RDS-filer (Fase 9, Fase 11). Dette sidste trin kobler dem på kohorten med left_join(), beregner opfølgningstid og beholder kun de kolonner analysen kræver.

Note

Kohorteopbygningen - inkl. matchingen - hører til Fase 10. Her antager vi, at kohorten allerede findes; denne side samler kun analysedatasættet rundt om den. Designvalgene (sammenligningskohorte, immortal time, matching-ratio) afgøres tilbage i Fase 1.

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, "sti/til/analysis_dataset.rds")   # gem det endelige datasæt
Komplet opskrift - fra kohort til analyseklart datasæt
#=====================================================
# Komplet opskrift: kohort -> analyseklart datasæt
#=====================================================

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

#-----------------------------------------------------
# 1. Ekskludér prævalente tilfælde (se Fase 9)
#-----------------------------------------------------
# ... kohort_renset <- kohort %>% anti_join(praevalente, by = "pnr")

#-----------------------------------------------------
# 2. Kobl udfald (fx første demensdiagnose efter index)
#-----------------------------------------------------
udfald  <- readRDS("sti/til/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) %>%
  semi_join(tibble(pnr = kohort$pnr), by = "pnr") %>%
  select(pnr, death_date = d_dodsdto) %>% collect()

vnds_data <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/vnds/") %>%
  rename_with(tolower) %>%
  semi_join(tibble(pnr = kohort$pnr), by = "pnr") %>%
  filter(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 (demografi, SES, komorbiditet)
#-----------------------------------------------------
bef_data <- readRDS("sti/til/extract_bef.rds")    # alder, køn fra BEF
ses_data <- readRDS("sti/til/extract_ses.rds")    # uddannelse, indkomst, beskæftigelse
nmi_data <- readRDS("sti/til/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, "sti/til/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 har nu ét analyseklart datasæt med én række per person. Næste skridt er analysen:

Fase 13 - Analyse

Se også

TipLæs mere

Generel uddybning (på engelsk):

  • Joining data i The Epidemiologist R Handbook.
  • Joins i R for Data Science: nøgler, mutating vs. filtering joins og hvad der sker, når en nøgle har dubletter.
Back to top