NMI - Nordic Multimorbidity Index
Vægtet mortalitets-risikoscore fra nordiske registre
NMI (Nordic Multimorbidity Index) er en vægtet score bygget på nordiske hospitalsdiagnoser og indløste recepter. Den kombinerer 50 mortalitetsprediktorer - 29 diagnosebaserede og 21 receptbaserede - til én kontinuert score.
Artikel: Kristensen et al. Clinical Epidemiology 2022 - doi:10.2147/CLEP.S353398
Vigtigt: NMI er et mortalitetsindeks, ikke et mål for comorbiditetsbyrde. NMI blev udviklet og valideret til at forudsige 5-årig mortalitet (Kristensen et al. 2022, ~425.000 danskere ≥40 år), og prædiktorerne blev udvalgt efter, hvor meget de bidrog til mortalitetsmodellen - ikke efter at dække sygdomsbyrden. Derfor:
- Flere almindelige kroniske sygdomme er ikke inkluderet i modellen (fx hypertension og type 1-diabetes).
- Nogle items er mortalitetsproxyer, ikke selvstændige sygdomme (fx laksantia, jern).
- Nogle vægte er negative (fx statiner -3, ACE/AT-hæmmere -2), hvilket kun giver mening i en dødelighedsmodel, ikke i et mål for sygdomsbyrde.
Skal du måle comorbiditetsbyrde, er NMI det forkerte værktøj - brug i stedet et hospitals-ICD-baseret multimorbiditetsindeks. Et dedikeret et (ICD-10 + ATC-koder) er under udvikling; se Comorbiditet for den generelle tilgang. Brug kun NMI, når dit mål specifikt er mortalitetsrisiko.
Hvad NMI giver dig
En kontinuert mortalitets-risikoscore per person, valideret til at forudsige 5-årig mortalitet i nordiske populationer (og stærkere end Charlson- og Elixhauser-indekserne til netop det udfald). Brug den som:
- En kovariat, der opsummerer mortalitetsassocieret sygdom i en Cox- eller logistisk model.
- En oversigtsvariabel for mortalitetsrisiko i Table 1.
Den er ikke en ren optælling af kroniske sygdomme - vil du have comorbiditetsbyrde, se advarslen ovenfor.
Hvornår bruges det (og hvornår ikke)
Brug NMI, når dit spørgsmål handler om mortalitetsrisiko: justering for mortalitetsassocieret sygdomsbyrde i en overlevelses- eller logistisk model, eller når du vil have en valideret 5-års-mortalitets-prædiktor.
Brug ikke NMI til at måle comorbiditetsbyrde (en optælling eller et indeks over kroniske sygdomme) - det udelader almindelige tilstande by design (se ovenfor). Brug et hospitals-ICD-multimorbiditetsindeks til det.
Pas på over-justering: dit eget udfald må ikke være en af scorens prediktorer. NMI håndterer fx demensstudier ved at droppe demens-prediktoren (dx_F00_G30 og rx_N06D), da demens ellers ville være en del af den score, du justerer for.
R-implementering
Der findes ingen officiel R-pakke (SDU udgiver en Stata-implementering). R-versionen nedenfor kører i to trin - udtræk, dernæst beregn. Kodeeksemplerne bygger på Pernille Errebo Tonnesens oversættelse af SDU’s Stata-kode til R.
1. Udtræk diagnoser og recepter
Hvad NMI kræver, og over hvilken periode:
- Diagnoser, en 5-årig lookback før index, fra alle: LPR2 somatisk og psykiatrisk (frem til marts 2019) og LPR3 (fra marts 2019), på tværs af alle diagnosetyper. Et rent somatisk udtræk (fx
lpr_admalene) mangler de psykiatriske prediktorer (F00-03, F10, F17). - Recepter (ATC), en 6-måneders lookback før index, fra LMDB - kun det vindue, ikke hele registret.
Diagnoser - Du kan bruge de to tilgange fra Udtræk fra LPR: enten udtrække alle diagnoser og ATC-koder for hele kohorten (anbefales ikke - tids-, RAM- og pladskrævende, især ved en stor kohorte), eller filtrere til kun de koder NMI skal bruge før collect(), så de gemte .rds-filer holdes små.
Du kan også benytte get_lpr_diagnoses()-mønstret fra Udtræk fra LPR som i eksemplet herunder:
Se koden: udtræk NMI-diagnoser (LPR2 + LPR3)
#=============================================================
# 1. UDTRÆK diagnoser og recepter
#=============================================================
library(dplyr)
cohort <- readRDS("datasets/full_cohort.rds") # studiepopulationen fra Fase 10 (pnr + index_date)
#-------------------------------------------------------------
# NMI-diagnosekoder (3-tegns ICD, uden D-præfiks).
# paste0("C", 76:80) laver "C76".."C80" - limer bogstavet på hvert tal i
# rækken, så vi slipper for at taste hver kode manuelt.
#-------------------------------------------------------------
nmi_icd <- c(
"B18", # kronisk viral hepatitis
"C34", # lungekræft
"C50", # brystkræft
"C61", # prostatakræft
"C67", # blærekræft
# NMI-prediktor "hjerne-/hindetumor" (kun subkoderne nedenfor scorer, se beregningen):
"C70", # kræft i hjernehinderne
"C71", # hjernekræft
"C75", # kræft i andre endokrine kirtler (kun C751-753 scorer)
"D32", # godartet svulst i hjernehinder
"D33", # godartet svulst i hjerne/nervesystem (kun D330-332)
"D35", # godartet svulst i endokrine kirtler (kun D352-354)
"D42", # svulst i hjernehinder, usikker karakter
"D43", # svulst i hjerne/nervesystem, usikker karakter (kun D430-432)
"D44", # svulst i endokrine kirtler, usikker karakter (kun D443-445)
paste0("C", 76:80), # sekundær/metastatisk og dårligt defineret kræft (C76-C80)
paste0("C", 91:95), # leukæmi (C91-C95)
paste0("D", 50:64), # anæmier (D50-D64)
"E11", # type 2-diabetes
"E86", # væskemangel (dehydrering)
paste0("F0", 0:3), # demens (F00-F03, scorer sammen med G30)
"G30", # Alzheimers sygdom (demens-prediktoren)
"F10", # alkoholafhængighed/-misbrug
"F17", # tobaks-/nikotinafhængighed
paste0("G", 20:22), # Parkinsons sygdom og parkinsonisme (G20-G22)
"G35", # dissemineret sklerose
"G40", # epilepsi (G40-G41)
"G41", # status epilepticus (epilepsi-prediktoren)
# NMI-prediktor "aorta-/mitralklapsygdom" (I05/I06/I34/I35):
"I05", # reumatisk mitralklapsygdom
"I06", # reumatisk aortaklapsygdom
"I34", # ikke-reumatisk mitralklapsygdom
"I35", # ikke-reumatisk aortaklapsygdom
# NMI-prediktor "hjertesvigt" (kun subkoder I110/I13[02]/I42[06789]/I50 scorer):
"I11", # hypertensiv hjertesygdom (kun I110)
"I13", # hypertensiv hjerte- og nyresygdom (kun I130/I132)
"I42", # kardiomyopati (kun I420/I426-429)
"I50", # hjertesvigt
paste0("I", 60:69), # cerebrovaskulær sygdom / apopleksi (I60-I69)
# NMI-prediktor "aterosklerose/perifer arteriesygdom" (I70/I73/I74/I77):
"I70", # aterosklerose
"I73", # perifer karsygdom
"I74", # arteriel emboli og trombose
"I77", # andre arterielle sygdomme
"I71", # aortaaneurisme og -dissektion (I71-I72)
"I72", # andet aneurisme (aneurisme-prediktoren)
paste0("J1", 2:8), # lungebetændelse (J12-J18)
# NMI-prediktor "kronisk nedre luftvejssygdom" (J41-44, J47, J961/J969):
"J41", # simpel kronisk bronkitis
"J42", # uspecificeret kronisk bronkitis
"J43", # emfysem
"J44", # KOL (kronisk obstruktiv lungesygdom)
"J47", # bronkiektasi
"J96", # respirationssvigt (kun J961/J969)
"J84", # anden interstitiel lungesygdom
paste0("K0", c(2, 3, 4, 5, 6, 8)), # tand- og parodontalsygdom (K02-K06, K08)
# NMI-prediktor "alkoholisk leversygdom" (K70/K72/K74/K766/K767):
"K70", # alkoholisk leversygdom
"K72", # leversvigt
"K74", # leverfibrose og -cirrose
"K76", # anden leversygdom (kun K766/K767)
"L89", # tryksår
"N18", # kronisk nyresygdom (N18-N19)
"N19" # uspecificeret nyresvigt
)
# Du kan nu udtrække ICD og ATC koder ved hjælp af tilgang 1 eller 2, eller ved hjælp af get_lpr_diagnoses() hjælpefunktionen fra Fase 9 (Udtræk fra LPR). Vha. sidstnævnte vil koden se sådan ud:
nmi_diagnoses <- get_lpr_diagnoses(
pnr_vector = unique(cohort$pnr), # alle kohortens pnr
icd_codes = nmi_icd # kun NMI-koder hentes
)
saveRDS(nmi_diagnoses, "datasets/nmi_diagnoses.rds") # kolonner: pnr, date_contact, c_diag, c_diagtype, icd3Recepter - udtræk LMDB for kohorten, behold kun NMI’s ATC-klasser. read_register() kræver, at fastreg er sat op med stien til dine registre - se Fase 4, hvis du ikke selv har konverteret dem fra SAS:
Se koden: udtræk NMI-recepter (LMDB)
library(fastreg) # read_register()
library(arrow) # open_dataset() - fallback uden fastreg
library(dplyr)
pnr_vec <- unique(cohort$pnr)
#-------------------------------------------------------------
# NMI's lægemiddelklasser (ATC) som én regex. Hver klasse:
# A06A afføringsmidler (laksantia)
# A07DA antipropulsiva (midler mod diarré)
# A10A insuliner og analoger
# B01AC trombocythæmmere (blodfortyndende)
# B03A jernpræparater
# C01AA digitalisglykosider (digoxin)
# C03C loop-diuretika (kraftigt vanddrivende)
# C03EB loop- + kaliumbesparende diuretika
# C03DA aldosteronantagonister (fx spironolakton)
# C09C/C09D angiotensin II-receptorblokkere (ARB), rene og kombinationer
# C10AA statiner
# H02AB systemiske glukokortikoider (binyrebarkhormon)
# J01C penicilliner (beta-lactam-antibiotika)
# N02A opioider
# N02BE anilider (paracetamol)
# N05BA benzodiazepiner (angstdæmpende)
# N05CD/CF benzodiazepiner og beslægtede sovemidler (z-stoffer)
# N05AA-AL antipsykotika
# N05AX andre antipsykotika
# N06A antidepressiva
# N06D demensmidler
# N07BC midler mod opioidafhængighed
# R03AC02-05 korttidsvirkende inhalerede beta-2-agonister
# R03BB04-07 langtidsvirkende inhalerede anti-muskarine midler (antikolinergika)
#-------------------------------------------------------------
nmi_atc <- "^(A06A|A07DA|A10A|B01AC|B03A|C01AA|C03C|C03EB|C03DA|C09[CD]|C10AA|H02AB|J01C|N02A|N02BE|N05BA|N05C[DF]|N05A[A-L]|N05AX|N06A|N06D|N07BC|R03AC0[2-5]|R03BB0[4-7])"
nmi_lmdb <- read_register("lmdb") %>% # uden fastreg: open_dataset("sti/til/lmdb/")
rename_with(tolower) %>%
semi_join(tibble(pnr = pnr_vec), by = "pnr") %>% # kun kohorten
filter(grepl(nmi_atc, atc)) %>% # behold rækker hvor atc-kolonnen matcher nmi_atc-regexet (NMI's lægemiddelklasser) - FØR collect()
collect()
saveRDS(nmi_lmdb, "datasets/nmi_lmdb.rds") # pnr, eksd, atcDu startede fra full_cohort.rds (Fase 10: pnr + index_date) og har nu nmi_diagnoses.rds og nmi_lmdb.rds. Gem dem. Fordi udtrækket er filtreret til kun NMI-koderne, er filerne små, så du behøver sjældent rydde op. Er hukommelsen alligevel presset, så frigør den med rm() + gc() (se Fase 5) frem for at genstarte hele R-sessionen.
2. Beregn NMI-scoren
Se hele NMI-beregningen (rettet mod Stata-kilden)
#=============================================================
# 2. BEREGN NMI-scoren
#=============================================================
library(dplyr)
library(stringr)
library(lubridate)
library(tidyr)
#-------------------------------------------------------------
# 2.0 Indlæs data
#-------------------------------------------------------------
cohort <- readRDS("datasets/full_cohort.rds") # pnr, index_date
dx_raw <- readRDS("datasets/nmi_diagnoses.rds") # fra get_lpr_diagnoses(): pnr, date_contact, c_diag
rx_raw <- readRDS("datasets/nmi_lmdb.rds") # pnr, eksd, atc
#-------------------------------------------------------------
# 2.1 Diagnoser: 5-årig lookback før index (Stata: 1825 dage)
#-------------------------------------------------------------
# Fire trin, ét ad gangen - vi genbruger dx hele vejen.
# 2.1.1 Kobl index-datoen på hver diagnose (inner join, så kun kohortens personer beholdes)
dx <- dx_raw %>%
inner_join(cohort %>% select(pnr, index_date), by = "pnr")
# 2.1.2 Behold kun diagnoser i 5-års-vinduet før index
dx <- dx %>%
filter(date_contact >= index_date - days(1825), # 1825 dage = 5 år før index (date_contact = d_inddto i LPR2 / kont_starttidspunkt i LPR3, omdøbt i fase 9)
date_contact <= index_date - days(1)) # til og med dagen før index (ikke selve index-dagen)
# 2.1.3 Sæt ét TRUE/FALSE-flag per NMI-prediktor (str_detect() matcher c_diag med D-præfiks mod prediktorens ICD-koder)
dx <- dx %>%
mutate(
dx_B18 = str_detect(c_diag, "^DB18"),
dx_C34 = str_detect(c_diag, "^DC34"),
dx_C50 = str_detect(c_diag, "^DC50"),
dx_C61 = str_detect(c_diag, "^DC61"),
dx_C67 = str_detect(c_diag, "^DC67"),
dx_C70_D432 = str_detect(c_diag, "^DC7[01]|^DC75[1-3]|^DD32|^DD33[0-2]|^DD35[2-4]|^DD42|^DD43[0-2]|^DD44[3-5]"),
dx_C76_C80 = str_detect(c_diag, "^DC7[6-9]|^DC80"),
dx_C91_C95 = str_detect(c_diag, "^DC9[1-5]"),
dx_D50_D64 = str_detect(c_diag, "^DD5[0-9]|^DD6[0-4]"),
dx_E11 = str_detect(c_diag, "^DE11"),
dx_E86 = str_detect(c_diag, "^DE86"),
dx_F00_G319 = str_detect(c_diag, "^DF0[0-3]|^DG30"),
dx_F10 = str_detect(c_diag, "^DF10"),
dx_F17 = str_detect(c_diag, "^DF17"),
dx_G20_22 = str_detect(c_diag, "^DG2[0-2]"),
dx_G35 = str_detect(c_diag, "^DG35"),
dx_G40_G41 = str_detect(c_diag, "^DG4[0-1]"),
dx_I05_35 = str_detect(c_diag, "^DI0[56]|^DI3[45]"),
dx_I110_I50 = str_detect(c_diag, "^DI110|^DI13[02]|^DI42[06789]|^DI50"),
dx_I60_I69 = str_detect(c_diag, "^DI6[0-9]"),
dx_I70_I77 = str_detect(c_diag, "^DI7[0347]"), # I70, I73, I74, I77 (Stata: ^I7[0347])
dx_I71_I72 = str_detect(c_diag, "^DI7[12]"),
dx_J12_J18 = str_detect(c_diag, "^DJ1[2-8]"),
dx_J41_J47 = str_detect(c_diag, "^DJ4[12347]|^DJ96[19]"),
dx_J84 = str_detect(c_diag, "^DJ84"),
dx_K02_K08 = str_detect(c_diag, "^DK0[234568]"),
dx_K70_K767 = str_detect(c_diag, "^DK7[024]|^DK76[67]"),
dx_L89 = str_detect(c_diag, "^DL89"),
dx_N18_N19 = str_detect(c_diag, "^DN1[89]")
)
# 2.1.4 Kollaps til én række per person (én kolonne per dx_-prediktor med værdien 0/1; any() = 1 hvis mindst én matchende diagnose i 5-års-vinduet, ellers 0)
dx <- dx %>%
group_by(pnr) %>%
summarise(across(starts_with("dx_"), ~ as.integer(any(.))), .groups = "drop")
#-------------------------------------------------------------
# 2.2 Recepter: 6-måneders lookback før index
#-------------------------------------------------------------
# Samme mønster som diagnoserne (kobl på, filtrér, flag, kollaps), her samlet i én kæde.
rx <- rx_raw %>%
inner_join(cohort %>% select(pnr, index_date), by = "pnr") %>%
filter(eksd >= index_date - days(180), # 180 dage = 6 måneder før index (eksd = indløsningsdato)
eksd <= index_date - days(1)) %>% # til og med dagen før index
# Ét TRUE/FALSE-flag per lægemiddel-prediktor: matcher atc mod ATC-præfikset
mutate(
rx_A06A = str_detect(atc, "^A06A"),
rx_A07DA = str_detect(atc, "^A07DA"),
rx_A10A = str_detect(atc, "^A10A"),
rx_B01AC = str_detect(atc, "^B01AC"),
rx_B03A = str_detect(atc, "^B03A"),
rx_C01AA = str_detect(atc, "^C01AA"),
rx_C03C_C03EB = str_detect(atc, "^C03C|^C03EB"),
rx_C03DA = str_detect(atc, "^C03DA"),
rx_C09C_C09D = str_detect(atc, "^C09[CD]"),
rx_C10AA = str_detect(atc, "^C10AA"),
rx_H02AB = str_detect(atc, "^H02AB"),
rx_J01C = str_detect(atc, "^J01C"),
rx_N02A = str_detect(atc, "^N02A"),
rx_N02BE = str_detect(atc, "^N02BE"),
rx_N05BA_N05CF = str_detect(atc, "^N05BA|^N05C[DF]"),
rx_N05AA_N05AX = str_detect(atc, "^N05A[A-L]|^N05AX"),
rx_N06A = str_detect(atc, "^N06A"),
rx_N06D = str_detect(atc, "^N06D"),
rx_N07BC = str_detect(atc, "^N07BC"),
rx_R03AC02_R03AC05 = str_detect(atc, "^R03AC0[2-5]"),
rx_R03BB04_R03BB07 = str_detect(atc, "^R03BB0[4-7]")
) %>%
# Kollaps til én række per person: 1 = mindst én indløsning i 6-måneders-vinduet, ellers 0
group_by(pnr) %>%
summarise(across(starts_with("rx_"), ~ as.integer(any(.))), .groups = "drop")
#-------------------------------------------------------------
# 2.3 Kombinér og scor (vægte fra Tabel 2)
#-------------------------------------------------------------
nmi_data <- cohort %>% # indlæs din kohorte igen hvis nødvendigt (navnet kan være et andet end "cohort")
left_join(dx, by = "pnr") %>% # sæt diagnose-flag på hver person i kohorten
left_join(rx, by = "pnr") %>% # og lægemiddel-flag
# personer uden nogen diagnose/recept er ikke i dx/rx -> left_join giver NA -> sæt til 0 (de har ingen)
mutate(across(c(starts_with("dx_"), starts_with("rx_")), ~ replace_na(., 0))) %>%
mutate(
nmi =
10 * dx_B18 + 19 * dx_C34 + 4 * dx_C50 + 5 * dx_C61 + 8 * dx_C67 +
8 * dx_C70_D432 + 22 * dx_C76_C80 + 8 * dx_C91_C95 + 5 * dx_D50_D64 +
2 * dx_E11 +
6 * dx_E86 + 9 * dx_F00_G319 + 12 * dx_F10 + 4 * dx_F17 +
7 * dx_G20_22 + 7 * dx_G35 + 5 * dx_G40_G41 +
2 * dx_I05_35 + 4 * dx_I110_I50 + 4 * dx_I60_I69 +
5 * dx_I70_I77 + 4 * dx_I71_I72 +
4 * dx_J12_J18 + 4 * dx_J41_J47 + 7 * dx_J84 +
5 * dx_K02_K08 + # tandsygdom, vægt 5
13 * dx_K70_K767 + # rettet: leversygdom, vægt 13 (var 13 * dx_K02_K08)
11 * dx_L89 + 7 * dx_N18_N19 +
8 * rx_A06A + 5 * rx_A07DA +
4 * rx_A10A +
2 * rx_B01AC + 5 * rx_B03A + 4 * rx_C01AA + 5 * rx_C03C_C03EB + 3 * rx_C03DA +
(-2) * rx_C09C_C09D + (-3) * rx_C10AA + # negative vægte: beskyttende i mortalitetsmodellen (sænker scoren)
2 * rx_H02AB + 1 * rx_J01C + 2 * rx_N02A + 2 * rx_N02BE +
1 * rx_N05BA_N05CF + 7 * rx_N05AA_N05AX + 3 * rx_N06A + 11 * rx_N06D +
7 * rx_N07BC + 3 * rx_R03AC02_R03AC05 + 5 * rx_R03BB04_R03BB07
)
saveRDS(nmi_data %>% select(pnr, nmi), "datasets/extract_nmi.rds")3. Se og beskriv scoren
Scoren er ét tal per person i extract_nmi.rds. Kobl den på dit analysedatasæt (Fase 12) og kig på den - NMI er højreskæv (mange lave, få høje), så median [IKR] beskriver den ofte bedre end gennemsnittet, men gennemsnit + CI er fint til at sammenligne grupper.
Se koden: beskriv NMI-scoren
library(dplyr)
nmi <- readRDS("datasets/extract_nmi.rds") # pnr, nmi
analyse <- readRDS("datasets/analyse.rds") %>% # dit samlede datasæt (Fase 12)
left_join(nmi, by = "pnr")
# Fordeling samlet
summary(analyse$nmi)
hist(analyse$nmi)
# Per gruppe (fx eksponeret vs sammenligningskohorte): gennemsnit + 95% CI og median [IKR]
analyse %>%
group_by(gruppe) %>%
summarise(
n = n(),
mean_nmi = mean(nmi, na.rm = TRUE), # na.rm = TRUE: fjern NA (manglende værdier) før beregningen
sd_nmi = sd(nmi, na.rm = TRUE),
median_nmi = median(nmi, na.rm = TRUE),
q1 = quantile(nmi, 0.25, na.rm = TRUE),
q3 = quantile(nmi, 0.75, na.rm = TRUE)
) %>%
mutate(
# 95% CI for gennemsnittet: mean ± 1.96 * standardfejl (SE = sd / sqrt(n))
ci_low = mean_nmi - 1.96 * sd_nmi / sqrt(n),
ci_high = mean_nmi + 1.96 * sd_nmi / sqrt(n)
)Til en formateret Table 1 på tværs af grupper gør gtsummary::tbl_summary(by = gruppe) det automatisk - se Deskriptive tabeller.
Overvej om 5-års lookback skal være et inklusionskrav. NMI’s diagnosevindue kræver mindst 5 år med registerdata før index-dato. Hvis du gør fuld lookback til et inklusionskriterium (kun personer med mindst 5 års forudgående bopæl/dækning i Danmark), skal det med allerede når du bygger din kohorte, ikke først her - og det samme krav skal anvendes på en eventuel sammenligningskohorte, så begge grupper har samme datadybde før index. Tag stilling til det i Fase 1 og anvend det i Fase 10.
Eksempel: gør 5-års lookback til et inklusionskrav (konceptuelt)
Idéen er at kræve, at hver person har været bosat i Danmark (og dermed dækket af registrene) i mindst 5 år før sin index-dato. Det tjekkes mod bopæls-/migrationsdata (VNDS/BEF) og anvendes som et filter, inden du matcher en sammenligningskohorte:
# Konceptuelt - tilpas til dine bopælsdata
kohort <- kohort %>%
mutate(lookback_ok = as.numeric(index_date - bopael_start) >= 365.25 * 5) %>% # mindst 5 år før index
filter(lookback_ok) # behold kun fuld lookbackAnvend det samme krav på sammenligningskohortens tildelte index-datoer, så de to grupper er sammenlignelige.
Mere information
Se artiklen for fuld prædiktorliste og vægte. Se Register-overblik for bekræftede kolonnenavne i LPR og LMDB.