Funktioner: oversigt
Hvad hver enkelt funktion gør - forklaret til dem, der aldrig har kodet før
Kender du funktionens navn? Brug Ctrl+F (Windows) eller Cmd+F (Mac) og søg direkte på siden.
| Emne | Sektion |
|---|---|
Røret %>% |
Røret |
| Indlæs register / åbn parquet-fil | Dataindlæsning |
| Filtrer rækker | Rækker: filtrer og deduplikér |
| Vælg eller tilføj kolonner | Kolonner: vælg, tilføj, omdan |
| Omdøb kolonner | Kolonnenavne |
| Grupper, tæl og aggregér | Grupper og aggregering |
| Join to tabeller | Join - sammensæt tabeller |
| Pivot (bredt ↔︎ langt format) | Omformning |
collect() og doven evaluering |
Doven forespørgsel |
| Datoer og tekst | Tekstoperationer · Tal og datoer |
| Fejlsøgning | Diagnostik og fejlsøgning |
Forstår du de 6 funktioner herunder, kan du læse de fleste linjer registerdata-kode.
| Funktion | Hvad den gør |
|---|---|
open_dataset("sti") |
Åbner en doven forbindelse til et register (i DARTER: read_register("navn")) |
filter(betingelse) |
Beholder kun de rækker, der opfylder betingelsen |
select(kol1, kol2) |
Beholder kun de angivne kolonner |
mutate(ny_kol = ...) |
Tilføjer eller ændrer en kolonne |
left_join(anden_df, by = "nøgle") |
Joiner to tabeller på en fælles nøgle |
collect() |
Henter data fra parquet ind i R-hukommelsen |
Rækkefølgen er intentionel: open_dataset → filter → select → collect er det mønster, der optræder i næsten hvert script.
Hvad er en funktion?
Tænk på en funktion som en maskine på et samlebånd. Du sender noget ind i den ene ende, den gør noget ved det, og du får noget nyt ud i den anden ende. filter() er for eksempel en si: du sender en stor tabel ind, angiver hvilke rækker du vil beholde, og får en mindre tabel ud.
Alle funktioner i R skrives med parenteser: funktionsnavn(hvad_der_sendes_ind).
Det, du skriver inde i parentesen, kaldes argumenter. Mange funktioner har navngivne argumenter på formen navn = værdi: til venstre for = står argumentets navn (fastlagt af funktionen, kan ikke laves om), og til højre den værdi, du selv giver det, fx ratio = 5 eller na.rm = TRUE. Du behøver ikke kende dem udenad: ?funktionsnavn viser, hvilke argumenter en funktion tager, og hvad de gør.
Vil du se hvad en funktion gør? Sæt markøren midt i funktionsnavnet og tryk F1 - hjælpesiden åbner med beskrivelse, argumenter og eksempler direkte i RStudios Help-panel. Du kan også skrive ?funktionsnavn eller help(funktionsnavn) i konsollen, eller args(funktionsnavn) for en hurtig liste over argumenter.
For pakker på CRAN ligger der en online-side med dokumentation og vignetter (fx https://cran.r-project.org/package=MatchIt). Nogle DST-pakker (fx heaven) ligger på GitHub i stedet - brug ?funktionsnavn for argumenterne og bekræft pakkens tilgængelighed på dit eget projekt.
Røret - %>%
Det vigtigste symbol i al koden er %>%, “the pipe”.
lpr_adm %>%
semi_join(tibble(pnr = mine_pnrs), by = "pnr") %>%
select(pnr, recnum) %>%
collect()Hvad det gør: røret sender resultatet fra linjen til venstre videre som første argument til funktionen til højre.
De to skrivemåder gør det samme:
# Uden røret - indefra og ud, som russiske dukker:
collect(select(semi_join(lpr_adm, tibble(pnr = mine_pnrs), by = "pnr"), pnr, recnum))
# Med røret - oppefra og ned, som en opskrift:
lpr_adm %>%
semi_join(tibble(pnr = mine_pnrs), by = "pnr") %>%
select(pnr, recnum) %>%
collect()Begge versioner giver nøjagtigt det samme resultat. Pipe-versionen er lettere at læse fordi du kan følge trinene i rækkefølge - og lettere at fejlfinde fordi du kan tilføje eller fjerne ét trin ad gangen.
Linjeskift efter %>% er ikke et krav - kun for læsbarhed. Du kan skrive hele kæden på én linje (bef %>% filter(...) %>% collect()) eller dele den op med ét trin per linje. Den eneste regel: sætter du linjeskift, skal %>% stå i slutningen af linjen, ikke i starten af den næste. R læser linje for linje, så et %>% til sidst signalerer “der kommer mere”:
# Virker - %>% til sidst på linjen:
bef <- bef %>%
filter(year == 2015) %>%
collect()
# Fejler - R tror udtrykket sluttede efter "bef":
bef <- bef
%>% filter(year == 2015)Det samme gælder |> og + i ggplot2.
Anekdote: Forestil dig at du laver mad. Du skærer løgene - og sender dem videre til gryden - som sender indholdet videre til tallerkenen. Røret gør præcis det: det kæder trin sammen, så du kan læse koden oppefra og ned som en opskrift.
%>% og |> er det samme - bare to forskellige måder at skrive røret på.
%>% kommer fra pakken magrittr og er tilgængeligt via library(dplyr). |> er en indbygget version introduceret i R 4.1 - den kræver ingen pakke.
De to virker identisk i næsten alle situationer. Du vil se begge i R-kode på nettet. Projektet bruger %>%, men hvis du skriver |> er det helt fint.
Dataindlæsning
open_dataset("sti") - arrow
Hvad det gør: åbner en doven forbindelse til en parquet-fil eller -mappe.
Anekdote: Forestil dig at du ringer til biblioteket og beder dem om at finde alle bøger om hjertekirurgi fra 1990 til 2020. Bibliotekaren siger “ja, dem finder jeg” - men de er ikke ankommet endnu. Det er præcis hvad open_dataset() gør: det fortæller computeren hvad du vil have, men data er ikke hentet ind i hukommelsen endnu. Resten af dine kommandoer (filter, select) tilføjer yderligere instruktioner, inden du til sidst siger “send dem nu” - det er collect().
library(fastreg)
bef <- read_register("bef") %>% rename_with(tolower) # via navn - fastreg kender stienread_register() finder stien fra din projekt-config (sat én gang) - se Fase 4. Den returnerer en DuckDB-forbindelse.
bef <- open_dataset("E:/workdata/[projektnummer]/cleaned-data/parquet-registers/bef/") %>%
rename_with(tolower) # standardisér kolonnenavne til lowercaseDe bekræftede stier til dit projekt finder du i Register-overblik.
bef <- open_dataset("synth_data/bef/") %>% rename_with(tolower) # sti til lokalt gemt syntetisk registerGenerer og gem syntetiske data lokalt inden brug - se Fase 6 - Første udtræk.
Bruges til alle registre der er gemt som parquet-filer: bef, lpr_adm, lpr_diag, lmdb, dodsaars, vnds, udda, faik, akm, t_psyk_adm, t_psyk_diag, lpr_a_kontakt, lpr_a_diagnose med flere.
Arrow eller DuckDB? open_dataset() giver dig en Arrow-forbindelse. Arrow er hurtig, men understøtter ikke alle dplyr-funktioner: fejler et trin med en “unsupported function”-fejl, løser det sig som regel ved at skifte til DuckDB, der understøtter næsten alle dplyr-verber. Send data gennem to_duckdb():
library(arrow) # open_dataset, to_duckdb
bef <- open_dataset("sti/til/bef/") %>%
to_duckdb() %>% # giv data videre til DuckDB
rename_with(tolower)Det skal du ikke med read_register() (fastreg) - de giver dig allerede en DuckDB-forbindelse, så to_duckdb()-konverteringen er indbygget. Hvornår du bruger hvad, kan du læse i Fase 5 - Arrow vs. DuckDB.
collect()
Hvad det gør: udfører den dovne forespørgsel og trækker data ind i R-hukommelsen.
Det er det punkt, hvor bibliotekaren rent faktisk bringer bøgerne hen til dig. Kald det sent - efter alle filter() og select() trin - så kun de nødvendige data flyttes.
resultat <- stort_register %>%
semi_join(tibble(pnr = kohort_pnrs), by = "pnr") %>% # kun kohortens pnr'er
select(pnr, d_inddto) %>% # kun de to kolonner vi bruger
collect() # data trækkes ind i R-hukommelsenKonceptet med doven evaluering - hvorfor data ikke er i hukommelsen før collect() - er forklaret grundigt på siden Udtræk trin for trin.
readRDS("sti/fil.rds")
Hvad det gør: læser en gemt R-fil fra disk ind i hukommelsen.
Anekdote: Det er som at åbne en Word-fil, du gemte sidst. .rds-formatet er R’s eget gemmingsformat - hurtigere og mere kompakt end CSV. Det bruges til at sende data videre fra ét script til det næste i pipeline’en.
full_cohort <- readRDS("sti/til/full_cohort.rds") # hent gemt datasæt fra disksaveRDS(objekt, "sti/fil.rds")
Hvad det gør: gemmer et R-objekt til disk.
Det modsatte af readRDS(). Hvert pipeline-script gemmer sit resultat med saveRDS(), så det næste script kan hente det.
saveRDS(full_cohort, "sti/til/full_cohort.rds") # gem datasæt til diskhaven::read_sas("fil.sas7bdat")
Hvad det gør: læser en SAS-datafil ind i R.
Bruges til at indlæse SAS-filer - fx DST’s formateringstabeller eller registre der ikke er konverteret til parquet. Se Filformater og Formateringstabeller for praktiske eksempler.
arrow::write_parquet(df, "sti/fil.parquet")
Hvad det gør: gemmer en data frame som en parquet-fil.
Parquet er et særligt effektivt filformat til store datasæt - det er langt hurtigere at læse end CSV. Bruges når du vil gemme et R-datasæt som parquet-fil, fx til brug med open_dataset() eller read_register() i et andet script.
file.exists("sti")
Hvad det gør: returnerer TRUE eller FALSE - eksisterer filen?
Bruges til at give en forståelig fejlbesked, inden koden forsøger at åbne en fil, der måske ikke er der.
dir.create(sti, showWarnings = FALSE, recursive = TRUE)
Hvad det gør: opretter en mappe, hvis den ikke allerede eksisterer.
recursive = TRUE opretter også eventuelle overordnede mapper. showWarnings = FALSE undertrykker den harmløse advarsel, du ellers ville få, hvis mappen allerede fandtes.
Kolonner: vælg, tilføj, omdan
select(kol1, kol2, nyt = gammelt, -fjern)
Hvad det gør: beholder kun de kolonner, du nævner.
Anekdote: Du har en stor Excel-fil med 50 kolonner. Du skal kun bruge 3. select() er som at gemme et udsnit med kun de tre kolonner du har brug for. Det reducerer mængden af data, der trækkes fra serveren, og er en af grundene til at koden er hurtig.
select(pnr, recnum, dato = d_inddto) # beholder tre kolonner; d_inddto omdøbes til dato
select(-aar) # fjerner kolonnen aar; beholder alle andreNår du omdøber inde i select(), er retningen nyt_navn = gammelt_navn - det nye navn til venstre, det eksisterende kolonnenavn til højre.
mutate(ny_kolonne = udtryk)
Hvad det gør: tilføjer en ny kolonne (eller ændrer en eksisterende), beregnet ud fra de kolonner du allerede har. Antallet af rækker er det samme - mutate() tilføjer information, fjerner ikke rækker. (df er bare navnet på din data frame - kald den, hvad du vil.)
library(dplyr)
df <- df %>%
mutate(
bmi = vaegt / (hoejde^2), # ny kolonne beregnet fra to eksisterende
obesity = bmi >= 30 # TRUE/FALSE - en nyligt lavet kolonne kan bruges med det samme
)Du kan lave flere variable i ét kald, og en variabel du lige har lavet, er straks tilgængelig længere nede (her bruger obesity_class den netop beregnede bmi):
df <- df %>%
mutate(
bmi = vaegt / (hoejde^2),
obesity_class = case_when( # case_when: flere betingelser; den FØRSTE sande bestemmer værdien
bmi < 25 ~ "Normal",
bmi < 30 ~ "Overvægt",
TRUE ~ "Svær overvægt" # TRUE = "alt andet"
)
)Tjek enhederne først. BMI = vægt (kg) / højde (m)². Er højden i cm, bliver vaegt / (hoejde^2) forkert. Lav højden om til meter i et mutate()-trin, før du beregner BMI:
df <- df %>%
mutate(hoejde_m = hoejde_cm / 100) %>% # cm -> m FØRST
mutate(bmi = vaegt / (hoejde_m^2)) # ... så er BMI korrektcase_when() har sit eget opslag nedenfor. Flere eksempler på mutate():
mutate(icd3 = substr(c_diag, 2, 4)) # ny kolonne: ICD-kode uden D-præfikset, 3 tegn
mutate(birth_year = year(foed_dag)) # fødselsår ud fra fødselsdatotransmute(ny_kolonne = udtryk)
Hvad det gør: som mutate(), men beholder kun de kolonner, du navngiver - resten smides væk. Praktisk, når du vil bygge en ren tabel med præcis de kolonner, en funktion skal bruge.
df %>% mutate(event = 1L) # beholder ALLE kolonner + den nye 'event'
df %>% transmute(pnr, event = 1L) # beholder KUN 'pnr' og 'event'case_when(betingelse1 ~ værdi1, betingelse2 ~ værdi2, TRUE ~ standard)
Hvad det gør: en avanceret if-else med mange betingelser.
Anekdote: Tænk på trafiklyset: er det rødt → stop, er det gult → vær forsigtig, er det grønt → kør. case_when() fungerer på samme måde: betingelserne evalueres i rækkefølge, og den første betingelse, der er sand, bestemmer resultatet. TRUE ~ standard er “i alle andre tilfælde”-armen.
mutate(uddannelse_kat = case_when(
edu_level == 1 ~ "Grundskole",
edu_level == 2 ~ "Gymnasial",
edu_level == 3 ~ "Videregaaende",
TRUE ~ "Ukendt"
))if_else(betingelse, sand_værdi, falsk_værdi)
Hvad det gør: en simpel to-vejs betingelse.
if_else(is.na(death_date), 0L, 1L) # 1 hvis personen er død, 0 hvis ikkeStrengere end basis-R’s ifelse() - begge værdier skal have samme type.
0L og 1L er heltal (integers) i R. L-suffikset er R’s måde at angive at et tal er et heltal frem for et decimaltal (0 og 1 uden L er double som standard). if_else() kræver at begge værdier har præcis samme type - brug enten 0L/1L (integer) eller 0/1 (double), men ikke en blanding.
coalesce(x, erstatning)
Hvad det gør: erstatter NA-værdier med en anden værdi.
Anekdote: Efter et left join vil mange personer have NA i flag-kolonner, fordi de ikke havde den tilstand. coalesce(mi_flag, 0L) siger: “hvis mi_flag er NA, sæt det til 0 i stedet”. Bruges systematisk efter hvert left join, der producerer flag-kolonner.
coalesce(mi_flag, 0L) # NA (= ikke fundet) -> 0 (= fraværende)across(kolonner, funktion)
Hvad det gør: anvender den samme funktion på mange kolonner på én gang inde i mutate().
mutate(across(all_of(nmi_variabler), ~ coalesce(.x, 0L))) # erstat NA med 0 i alle nmi-flagkolonner~ coalesce(.x, 0L) er en anonym funktion: ~ betyder “funktion af”, og .x er den aktuelle kolonne. Det svarer til function(x) coalesce(x, 0L) - men kortere. across() kalder denne funktion én gang per kolonne i nmi_variabler.
rowSums(matrix, na.rm = TRUE)
Hvad det gør: lægger værdierne i hver række sammen på tværs af kolonner.
Bruges to steder:
- NMI-score (Nordic Multimorbidity Index): summerer produktet af 0/1-flag-kolonner og deres individuelle vægte → én vægtet comorbiditetsscore per person. En patient med hjertekarsygdom og cancer scorer højere end en patient med to mildere tilstande.
- Multimorbiditetstælling: summerer alle 0/1-flag → simpel optælling af antal tilstande per person.
Rækker: filtrer og deduplikér
filter(betingelse1, betingelse2, ...)
Hvad det gør: beholder kun de rækker, der opfylder betingelserne.
Anekdote: Du har en patientliste og vil kun se kvinder over 50 år. filter(koen == 2, alder > 50) er din si - alt andet ryger ud.
Tegn til at sammenligne en kolonne med en værdi:
| Tegn | Betyder | Eksempel |
|---|---|---|
== |
lig med (to lighedstegn - ét = er tildeling) |
filter(year == 2015) |
!= |
forskellig fra | filter(koen != 2) |
> · >= |
større end · større end eller lig med | filter(alder >= 18) |
< · <= |
mindre end · mindre end eller lig med | filter(alder < 65) |
%in% |
er med i listen (se nedenfor) | filter(icd3 %in% c("F00","F03")) |
Tegn til at kombinere flere betingelser:
| Tegn | Betyder | Eksempel |
|---|---|---|
, eller & |
OG - begge skal være sande | filter(koen == 2, alder > 50) → kvinder og over 50 |
\| |
ELLER - mindst én sand | filter(icd3 == "F00" \| icd3 == "F03") → F00 eller F03 |
! |
IKKE - vend betingelsen | filter(!is.na(alder)) → behold rækker hvor alder ikke mangler |
Faldgrube - bland aldrig OG og ELLER uden parenteser. & binder før |, så filter(alder > 50 | alder < 18 & koen == 2) læses som alder > 50 | (alder < 18 & koen == 2). Mener du “(over 50 eller under 18) og kvinde”, skal du sætte parenteser: filter((alder > 50 | alder < 18) & koen == 2).
filter(c_diagtype %in% c("A", "B")) # kun aktions- og bidiagnoser
filter(date_contact >= surgery_date) # kun post-operative kontakter
filter(icd3 %in% c("G30", "F00", "F03")) # kun demenskoder%in% - “er med i listen”
Hvad det gør: tjekker om hvert element på venstre side optræder i vektoren på højre side. Returnerer TRUE eller FALSE for hvert element.
%in% er valgfrit: det er blot ét af filter()’s sammenligningstegn (ved siden af ==, >, < osv.). Du bruger det kun, når du vil matche mod en liste af værdier i stedet for en enkelt værdi - fx en kodeliste. Skal du matche pnr mod kohorten, så brug semi_join (se ovenfor).
Anekdote: Forestil dig en gæsteliste til en fest. icd3 %in% c("G30", "F00", "F03") er som at stille sig ved indgangen og tjekke: “er denne diagnosekode på listen?”
icd3 %in% c("G30", "F00", "F03") # TRUE for disse tre koder, FALSE for alt andet
atc %in% !!mine_atc # TRUE for alle ATC-koder på din lokale liste (!! - se nedenfor)Du vil se %in% i næsten alle filter()-kald i projektet.
Skal du filtrere på pnr mod hele kohorten, så brug semi_join(tibble(pnr = kohort_pnrs), by = "pnr") i stedet for filter(pnr %in% ...) - det skubbes mere effektivt ned i databasen og kræver ikke !!.
distinct(kol1, kol2)
Hvad det gør: fjerner duplikater - beholder kun unikke kombinationer.
Anekdote: En person kan have fået diagnosen F00 ti gange. Du behøver kun at vide, om de nogensinde har haft den. distinct(pnr, icd3) reducerer det til én række per person per kode. distinct(pnr) giver dig bare listen over unikke person-ID’er.
slice(n)
Hvad det gør: beholder kun den n’te række inden for hver gruppe (bruges efter group_by()).
group_by(pnr) %>% # gruppér per person
arrange(desc(aar)) %>% # nyeste år først
slice(1) # beholder den nyeste post per personGrupper og aggregering
group_by(kol1, kol2)
Hvad det gør: opdeler data i grupper, så efterfølgende operationer sker separat inden for hver gruppe.
Anekdote: Forestil dig at du har en stak patientkurver og sorterer dem i bunker efter CPR-nummer. group_by(pnr) gør præcis det - men kun i hukommelsen. Alle efterfølgende trin (arrange, slice, summarise) sker nu én bunke ad gangen.
group_by(pnr) %>% # gruppér per person
arrange(date_contact) %>% # ældste dato først
slice(1) %>% # tidligste kontakt per person
ungroup() # fjern grupperingen igen bagefterungroup()
Hvad det gør: fjerner grupperingen.
Vigtigt: kald altid ungroup() efter du er færdig med group_by(). Glemmer du det, forbliver data grupperet, og senere operationer kan opføre sig uventet.
arrange(kolonne) / arrange(desc(kolonne))
Hvad det gør: sorterer rækkerne stigende (standard) eller faldende (desc()).
Bruges typisk med group_by() %>% slice(1) for at finde den første eller nyeste post per person.
summarise(ny_kol = funktion(kol), .groups = "drop")
Hvad det gør: reducerer hver gruppe til én opsummeringsrække.
group_by(pnr) %>% # gruppér per person
summarise(foerste_e66 = min(date_contact, na.rm = TRUE)) # tidligste E66-dato per person.groups = "drop" fjerner grupperingen automatisk bagefter.
ntile(x, n)
Hvad det gør: inddeler rækker i n lige store grupper (kvantiler).
Bruges ikke til indkomstkvintiler i registerbaserede studier der følger SEPLINE-retningslinjerne. SEPLINE anbefaler at sammenligne med populationsspecifikke grænseværdier (Q20/Q40/Q60/Q80) stratificeret på køn × 5-årsaldersgruppe × referenceår - ikke ntile() på din kohorte alene. Se SEPLINE.
Join - sammensæt tabeller
Det er en af de ting, der tager længst tid at forstå, men som er afgørende for alt registerarbejde. En join sætter to tabeller sammen baseret på en fælles nøgle - typisk pnr.
inner_join(y, by = "nøgle")
Hvad det gør: beholder kun rækker, der eksisterer i BEGGE tabeller.
Anekdote: Det er som en VIP-liste ved indgangen. Du skal stå på BEGGE lister for at komme ind. Bruges når en match er meningsfuld - f.eks. inner_join(bs_kohort) beholder kun hospitalkontakter for personer, der faktisk er i studiet.
inner_join(bs_kohort %>% select(pnr, surgery_date), by = "pnr") # kun kontakter fra BS-kohortens medlemmersemi_join(y, by = "nøgle")
Hvad det gør: beholder de rækker fra den venstre tabel, der har en match i y - men tilføjer ingen kolonner fra y. Det er altså et filter, hvor listen af tilladte værdier ligger i en anden tabel.
Anekdote: Du har hele LPR og en liste over din kohorte. semi_join beholder kun de LPR-rækker, hvis pnr står på kohorte-listen - samme resultat som filter(pnr %in% ...), men hurtigere og mere pålideligt på dovne (Arrow/DuckDB) tabeller.
lpr_adm %>%
semi_join(tibble(pnr = kohort_pnrs), by = "pnr") # behold kun kohortens rækkerHvor ligger den lokale liste? Selve listen er den tibble, du sender som andet argument (y); by = "pnr" siger kun, hvilken kolonne der skal matches på - ikke hvor listen er. Fordi y er en almindelig tabel, behøver du ikke !!. Det er forskellen fra filter(pnr %in% !!kohort_pnrs), hvor vektoren står direkte i betingelsen og !! er det, der injicerer den. Brug semi_join til at filtrere et register ned til kohorten.
Hvad det gør: beholder alle rækker fra den venstre tabel. Rækker uden match i den højre tabel får NA for de kolonner, der kom fra højre.
Anekdote: Det er som at tjekke, om dine patienter har et bestemt fund, uden at smide nogen væk. Alle patienter er der stadig - dem med funnet har en dato, dem uden har NA. Bruges overalt, når du tilføjer flag og kovariater til kohorten.
full_cohort %>% # start med alle kohortmedlemmer
left_join(dementia, by = "pnr") # alle bevares; kun dem med demens får en datobind_rows(df1, df2, ...)
Hvad det gør: stakker data frames ovenpå hinanden (samme kolonner, flere rækker).
Anekdote: Som at tage tre bunker papir og lægge dem i én stak. Bruges fx til at kombinere LPR2 + psykiatrisk LPR2 + LPR3 til én samlet diagnosetabel.
bind_rows(lpr2_resultater, lpr2_psyk_resultater, lpr3_resultater) # kombinér alle tre kildetabellerOmformning
pivot_wider(names_from = kol, values_from = kol)
Hvad det gør: omdanner et langt format (én række per besøg) til et bredt format (én række per person med én kolonne per tidspunkt).
Anekdote: Forestil dig en patient med fem vejebesøg - alle i samme kolonne med fem rækker. pivot_wider() omdanner det til én enkelt række med fem kolonner: vaegt_3mo, vaegt_6mo, vaegt_12mo, osv. Bruges i udtræk af vægt- og insulinudkomme.
Doven forespørgsel
!! (bang-bang, to udråbstegn)
Hvad det gør: injicerer en lokal R-variabel ind i en DuckDB/dplyr-forespørgsel.
Anekdote: Forestil dig at du beder en assistent om at finde alle rækker med en kode fra en liste. Hvis du siger “find alle med en kode fra listen mine_atc”, vil assistenten kigge efter en kolonne i databasen med det navn - og den eksisterer ikke. Du skal sige: “find alle med en kode fra denne liste” og holde listen frem. !! er det, der svarer til at holde listen frem.
filter(atc %in% !!mine_atc) # !! siger: "mine_atc er en R-vektor, ikke et kolonnenavn"Du vil se !! foran lokale R-variabler (typisk kode- og år-lister) inde i filter()-kald.
Til pnr-filtrering mod kohorten bruger du semi_join(tibble(pnr = kohort_pnrs), by = "pnr"), som tager den lokale tabel direkte og ikke kræver !!.
Tekstoperationer
substr(streng, start, slut)
Hvad det gør: udtrækker en del af en tekststreng baseret på tegnnumre.
Anekdote: Du har ICD-koden "DG30". DST har tilføjet et “D” foran - det hører ikke til i standard ICD-10. substr("DG30", 2, 4) siger: “giv mig tegnene fra position 2 til 4” og returnerer "G30".
substr(c_diag, 2, 4) # 3-tegns kode: "DG30" -> "G30"
substr(c_diag, 2, 5) # 4-tegns kode: "DI110" -> "I110"paste0(x, y)
Hvad det gør: sætter tekststrenge sammen uden mellemrum.
paste0("C", 10:43) # laver "C10", "C11", "C12", ..., "C43"Bruges til kompakt opbygning af ICD-kodelister.
paste(x, y, sep = "_")
Hvad det gør: sætter tekststrenge sammen med et selvvalgt adskillelsestegn.
paste(koen, foedselsaar, sep = "_") # laver f.eks. "1_1975" som matchingnøgletoupper(x) / tolower(x)
Hvad det gør: konverterer tekst til hhv. store eller små bogstaver.
toupper(c_opr) # sikrer at operationskoder matcher uanset bogstavsstørrelsegrepl(monster, x)
Hvad det gør: returnerer TRUE/FALSE for hvert element i x, der matcher et søgemønster (regulært udtryk).
Anekdote: Det er som “Ctrl+F” på et tekstdokument, men anvendt på hele kolonner på én gang. grepl("^C34", icd4) finder alle 4-tegns koder, der starter med C34.
grepl("^C34", icd4) # TRUE for "C340", "C341", "C342", osv.Bruges fx til at matche ICD-koder mod diagnosemønstre i comorbiditetsmål som NMI (Nordic Multimorbidity Index).
Tal og datoer
as.Date(x)
Hvad det gør: konverterer tekst eller datetime til et simpelt datoobjekt.
DST gemmer nogle dato-tidsangivelser som "2021-03-15 14:32:00". as.Date() fjerner tidsdelene og giver en ren kalenderdato.
as.Date(kont_starttidspunkt) # "2021-03-15 14:32:00" -> 2021-03-15ymd(x) / dmy(x) (fra lubridate)
Hvad det gør: læser datoer der ikke står på ISO-formen ("2021-03-15"). as.Date() antager ISO og giver NA på fx "15/03/2021" eller "15-03-2021"; de forgivende lubridate-parsere gætter rækkefølgen ud fra navnet (ymd = år-måned-dag, dmy = dag-måned-år, mdy = måned-dag-år).
ymd("2021-03-15") # år-måned-dag -> 2021-03-15
dmy("15/03/2021") # dag-måned-år -> 2021-03-15 (as.Date ville give NA)year(dato) (fra lubridate)
Hvad det gør: trækker årstallet ud af en dato.
year(surgery_date) # 2021-03-15 -> 2021difftime(dato1, dato2, units = "days")
Hvad det gør: beregner forskellen mellem to datoer.
as.numeric(difftime(surgery_date, foed_dag, units = "days")) / 365.25 # alder ved operation i årmin(x, na.rm = TRUE) / max(x, na.rm = TRUE)
Hvad det gør: finder det mindste/største element i en vektor og ignorerer NA.
summarise(foerste_dato = min(date_contact, na.rm = TRUE)) # tidligste kontaktdato per personpmin(x, y) / pmax(x, y)
Hvad det gør: sammenligner to vektorer position for position og returnerer det mindste/største for hvert element.
Anekdote: Forestil dig to lister med datoer - dødsdato og studieslutningstidspunkt. pmin(death_date, study_end) vælger for hver person, hvad der kom først.
pmin(death_date, as.Date("2024-12-31")) # censureringsdato: enten dødsdato eller studieslutningstidspunktFunktionerne ovenfor er byggeklodserne. De konkrete dato-opgaver er vist dér, hvor de hører til i forløbet:
- Alder ved index (
/365.25, skudår): Sammenligningskohorte - Baseline-år for årsregistre (
year(index_date) - 1): Socioøkonomiske variable - SAS-heltalsdatoer (
origin = "1960-01-01"): Faldgruber på DST - Opfølgningstid og hændelsesvariabel (
pmin()): Joins - Start/stop-format (tidsvarierende kovariat): Tidsvarierende variable
as.integer(x) / 1L
Hvad det gør: konverterer til heltal.
L-suffikset (f.eks. 1L, 0L) angiver at det er et heltal og ikke et decimaltal. Flag-kolonner gemmes som heltal (1L/0L) for at spare hukommelse.
is.na(x)
Hvad det gør: returnerer TRUE for NA-værdier (manglende værdier).
filter(!is.na(pnr)) # fjern rækker uden person-ID
filter(!is.na(date_dementia)) # fjern rækker uden demensdatoset.seed(n)
Hvad det gør: fastsætter startpunktet for tilfældig talgeneration.
Anekdote: Forestil dig at du blander et kortspil. Uden set.seed() vil du blande forskelligt hver gang. Med set.seed(42) blander du altid på den samme måde - og kan dermed reproducere dine resultater præcist. Kald det altid inden matching-løkker for at sikre reproducerbarhed.
set.seed(42) # fastsæt tilfældighedsfrø for reproducerbarhed
sample(pool_pnrs, size = 5) # vælger altid de samme 5 tilfældige pnrssample(x, størrelse)
Hvad det gør: trækker tilfældige elementer fra en vektor.
Bruges i matchningslogik til at udvælge tilfældige kontrolpersoner fra en pool.
Lister og løkker
split(df, gruppevektor)
Hvad det gør: opdeler en data frame i en liste af sub-data-frames, én per unik gruppe.
Anekdote: Forestil dig at du sorterer patientkort i bunker efter år og køn. split(pool, paste(koen, foedselsaar, sep = "_")) giver dig én bunke per kombination, så matchning-koden kan arbejde hurtigt inden for én bunke ad gangen.
vector("list", n)
Hvad det gør: opretter en tom liste med plads til n elementer.
Forudallokering er hurtigere end at lade R udvide listen ét element ad gangen i en løkke.
seq_len(n)
Hvad det gør: genererer talrækken 1, 2, …, n.
Sikrere end 1:n i løkker, fordi den håndterer tilfældet n = 0 korrekt.
unlist(liste, use.names = FALSE)
Hvad det gør: fladtrykker en liste af vektorer til én lang vektor.
Diagnostik og fejlsøgning
class(x)
Hvad det gør: fortæller dig, hvilken type objekt x er.
class(mit_objekt)
# "tbl_df" "data.frame" -> data er i R-hukommelsen
# "tbl_duckdb_connection" -> doven DuckDB-forespørgsel, ikke hentet endnu
# "Table" "ArrowObject" -> doven Arrow-forbindelse, ikke hentet endnuTjek altid class() som det første, hvis du får en mærkelig fejl.
nrow(df)
Hvad det gør: returnerer antallet af rækker.
Bruges til at udskrive kohorte-størrelser og kontrollere, at eksklusioner har virket.
cat("tekst\n")
Hvad det gør: udskriver tekst til konsollen uden anførselstegn. \n er linjeskift.
Bruges til fremskridtsbeskeder: cat("Udtrækker NMI-score...\n").
stop("besked")
Hvad det gør: stopper koden med en fejlbesked.
Bruges til at give en forståelig fejl, hvis en påkrævet fil mangler.
stopifnot(betingelse)
Hvad det gør: et sikkerhedstjek. Er betingelsen TRUE, sker der ingenting, og koden fortsætter. Er den FALSE, stopper koden med en fejl. Brug det til antagelser, der SKAL holde, fx “én række per person”:
stopifnot(n_distinct(df$pnr) == nrow(df)) # fejler hvis der er dubletterHvad gør du, hvis det stopper? Så holder antagelsen ikke - fx er der mere end én række per person. Find dubletterne og ret årsagen:
df %>% count(pnr) %>% filter(n > 1) # se hvilke pnr der går igen
df <- df %>% distinct(pnr, .keep_all = TRUE) # behold én række per person (hvis det er rigtigt)Ofte stammer dubletter fra et join, der gangede rækker op, eller fra dobbelt person-år-rækker i kildedata.
gc()
Hvad det gør: frigiver ubrugt hukommelse tilbage til operativsystemet.
rm(stort_register) # fjern objektet fra R
gc() # frigiv hukommelsenBrug det efter du er færdig med store registre - I deler RAM med alle andre på DST-serveren.
Pakkeoversigt
Se Register-overblik for bekræftede kolonnenavne på alle registre disse pakker arbejder mod.
| Pakke | Hvad den leverer |
|---|---|
fastreg |
convert(), read_register() - SAS → parquet, og læs derefter registre via navn (CRAN) |
dplyr |
%>%, filter, select, mutate, join, group_by, arrange, osv. |
tidyr |
pivot_wider() - omformning fra langt til bredt format |
lubridate |
year(), as.Date(), datoberegninger |
arrow |
read_parquet(), write_parquet() - parquet-filhåndtering |
haven |
read_sas() - læsning af SAS-datafiler (kun 00_prepare_dbso.R) |
Generel uddybning i The Epidemiologist R Handbook (på engelsk):