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
}
Saml dine udtræk
Joins og pivots — klargør dit analysedatasæt
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.
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:
- Forme hvert udtræk til én række per person (de fleste registre er i langt format med mange rækker per person)
- 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().
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 |
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.
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.
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?
)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 150I 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ærdigHusk 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()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:
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" nedenforOversigt: 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øganti_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 samletKolonner 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.
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 liveEmigrationsdato (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 studieperiodenBeregn 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)
)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:
- Definer den eksponerede kohorte. Filtrér det register der definerer eksponeringen — fx bariatri-operationer via SKS-koder i
lpr_sksopr/procedurer_kirurgi(Fase 9). Beholdpnrog index-dato (operationsdatoen). - Byg sammenligningskohorten. Match kontroller til de eksponerede på fx alder, køn og kalenderår. Kontrollerne tildeles den matchede eksponeredes index-dato.
- Anvend de samme inklusions-/eksklusionskriterier på begge grupper, og saml dem (
bind_rows()). - Kobl udfald og kovariater på med
left_join()(én række per person), beregn opfølgningstid, og afslut medselect().
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ætDesignvalgene (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 kolonnenavneNæste skridt
Du kan nu forme og kombinere dine udtræk. Næste skridt er de socioøkonomiske variable:
Se også
- Fase 15 — Funktioner: oversigt — alle funktioner du bruger til at transformere data
- Fase 9 — Hospitalskontakter (LPR) — praktisk eksempel på langt registerdata og inner_join