Statistik
eksempler i R

Neurologi Neuroanatomi Statistik Home

Deskriptiv


Kvantitative data
Kategoriske data
Intervaller

Analytisk


Sandsynligheder

Kategoriske udfald

Kategoriske eksponeringer
Logistisk regression

Kvantitative udfald

Kvantitative udfald
Linær regression
Korrelationer
Overlevelse
Poisson regression

Tilfældighed


Randomisering

Forskning


PhD thesis



Jacob Liljehult
Klinisk sygeplejespecialist
cand.scient.san, Ph.d.

Neurologisk afdeling
Nordsjællands Hospital

Randomiseringssekvenser

I randomiserede kontrollerede forsøg vil man som regel bruge en computer-genereret randomiseringssekvens som grundlag for allokeringen af forsøgsdeltagerne. Formålet med randomisering er at gøre interventionen i forsøget uafhængigt af potentielle confounds (non-kausale associationer), så der ikke kommer skævvridninger i hvilke patienter, der får interventionen, og hvilke der ikke får forsøgsinterventionen (kontrolgruppen). Hvis det er helt tilfældigt hvilke deltagere, som får interventionen, hvilke der ikke får - vil alle forskelle mellem de to grupper være udtryk for enten tilfældig variation eller en reel behandlingseffekt (eller potentielt en skadevirkning hvis der er forskel i mængden af adverse events).

Simple randomisation

I forsøg med to behandlingsgrupper kan man i princippet nøjes med en række af tilfældigt fordelte tal af enten 0/1 eller 1/2. Sådanne sekvenser kan laves i R med funktionerne rbinom(n, s, p) eller sample(pulje, n, replace). Hvis man ønsker at gøre randomiseringen reproducerbar kan man bruge set.seed()-funktionen til at definere startpunktet for randomiseringsalgoritmen. Bruger man samme seed-tal, vil funktionen også returnere samme sekvens, hver gang man kører den.

Eksempel: Sekvens med en længde på 20; med 1 gruppe over 0; og en sandsynlighed på 50%

set.seed(123)
rbinom(20, 1, 0.5)

[1] 0 1 0 1 1 0 1 1 1 0 1 0 1 1 0 1 0 0 0 1

Eksempel: Tilfældig sampling fra puljen c(1,2); på en længde af 20; og med tilbagelægning (replace = TRUE).

set.seed(123)
sample(c(1,2), 20, replace = TRUE)

[1] 1 1 1 2 1 2 2 2 1 1 2 2 2 1 2 1 2 1 1 1

Fordelen ved sample()-funktionen er at man både kan sample fra en pulje af tal eller kategorier; så man vil også kunne bruge en pulje som c("Intervention","Control").

Blokrandomisering (med ens blokstørrelser)

Ved simple randomisering er der en sandsynlighed for at der kommer 'klumper' af allokeringer - hvilket vil sige at der kommer en række af enten interventions- eller kontroldeltagere efter hinanden. I store studier gør det i princippet ikke noget, så længe at både patienter og kvaliteten af interventionen er ens hele tiden.
Men disse 'klumper' af allokering kan i nogle tilfælde blive et problem, fx:

Hvis der er variation blandt de personer man rekruttere fra over tid, fx pga. sæsonvariation. Man kunne fx forestille sig en intervention, hvor der potentielt kunne være en interaktion med pollenallergi eller med allergimedicin. I dette tilfælde vil det kunne påvirke resultatet af studiet, hvis der allokeres mange til interventionsgruppen i løbet af foråret.

Hvis interventionen eller opfølgningen i interventionsgruppen er mere ressourcekrævende end i kontrolgruppen. Det kan fx være en intervention, som tager nogle timer at udføre/administrere og kræver at der er personale tilstede. Her vil der være en praktisk begrænsning i hvor mange interventioner der kan udføres pr dag eller pr uge.

Løsningen i alle disse tilfælde vil være at fordele allokeringerne mere jævnt - hvilket man gør ved at lave randomiseringen i mindre blokke, fx med en størrelse af 6 eller 8, ad gangen. Sekvensen bygges derefter op af at disse blokke sættes sammen til en sekvens.

Når man bruger sample()-funktionen til at lave en simple sekvens, er man ikke sikker på at der kommer lige mange af hver gruppe indenfor blokken.

sample(c(1,2), 8, replace = TRUE)

[1] 1 2 2 2 2 1 2 1

For at sikre at der kommer lige mange af begge allokeringer i hver blok, laver man en pulje, som indeholder alle de allokeringer, man ønsker - og definere replace-argumentet til FALSE. Derved laver man en tilfældig trækning fra puljen uden tilbagelægning.

block = c(1,1,1,1,2,2,2,2)
sample(block, 8, replace = FALSE)
# kan også skrives som: sample( c(1,1,1,1,2,2,2,2), 8, replace = FALSE )

[1] 1 2 2 2 1 1 1 2

For at lave en længere sekvens kan man bruge replicate(n, FUN)-funktionen til at gentage samme funktion et n antal gange.

Eksempel: replicate()-funktionen bruges til at lave 4 blokke af hver en længde på 8. Funktionen returnere en tabel med dimensionerne 8x4.

replicate(4, sample(block, 8, replace = FALSE))

[,1][,2][,3][,4]
[1,] 2212
[2,] 1121
[3,] 1211
[4,] 1111
[5,] 2222
[6,] 2112
[7,] 2122
[8,] 1221

Hvis man formattere tabellen til en liste kan man bruge combine funktionen c():

c( replicate(4, sample(block, 8, replace = FALSE)) )

[1] 1 2 2 1 1 2 1 2 1 2 2 2 1 1 1 2 1 1 2 1 2 1 2 2 1 1 2 2 2 1 1 2

Funktioner

Når man skriver kode, som bruger flere trin til at løse et problem, kan det være en fordel at bruge en funktionel tilgang til at automatisering. Derved bliver det muligt at genbruge koden, så man ikke skal skrive det sammen flere gang, hvis man skal gentage noget flere gange (fx at man skal bruge to randomiseringssekvenser til test og til produktion). Derudover gør det også ens kode mere læsevenlig, fordi man kan se hvilke elementer, der høre sammen og man kan følge beregningen fra input til output.

Syntaksen når man deklarerer en funktion er:

funktionsNavn <- function(input){ indhold }

En funktion er som regel kun meningsfuld, hvis den giver et output. Det kan gøres på to måder: Funktionen return() vil give et console-output, mens deklarationen af et element med dobbeltpil <<- vil give et output med globalt scope.

Vi har brug for en funktion, der kan lave en blok-randomiseret sekvens med et n antal blokke og med en blokstørrelse af b, samt mulighed for at definere set.seed().

Funktionen if(missing( x )){ x = værdi } giver mulighed for at man kan definere default værdier for hvert input. Dette er ikke strengt nødvendigt - men det gør at funktionen ikke bare returnere en fejlmeddelelse hvis man glemmer et input. I denne funktion defineres default værdier for n og b til 8; mens seed gøres variabel - hvilket vil sige at hvis man ikke skriver et specifikt seed-tal til set.seed()-funktionen, så vælges der automatisk et tilfældigt heltal mellem 10 og 99999.

randomBlockEven <- function(n, b, seed){
 if(missing(n)){ n = 8 } # Number of blocks
 if(missing(b)){ b = 8 } # Block sizes
 if(missing(seed)){ seed = round( runif(10, min = 10, max = 99999),0 ) }
 set.seed(seed)

 # Definition of the blocks
 groups <- c(rep(1,(b/2)),rep(2,(b/2)))

 # Construction of the sequence
 randomlist <- c(replicate(n, sample( groups, b, replace = FALSE ) ))
 return(randomlist)
 }

Funktionen anvendes således:

randomBlockEven(4,8, seed = 123)

[1] 2 2 1 2 1 1 2 1 2 1 2 1 1 1 2 2 1 2 1 1 2 2 2 1 2 1 2 1 2 1 1 2

Vil man bruge outputtet som et element, frem for et console-output, deklarerer man bare elementet påledes:

randomSekvens <- randomBlockEven(4,8, seed = 123)

Blokrandomisering (med varierende blokstørrelser)

Et vigtigt element i randomiserede kontrollerede forsøg er det vigtigt at personalet, der rekrutterer deltagere ikke kan forudsige hvilken gruppe den næste deltagere bliver allokeret til, fordi det potentielt kan påvirke deres beslutning om inklusion eller ej, eller deltagerens beslutning om at deltage. Sikringen af at fremtidige allokeringer skjult kaldes concealment

I forsøg, hvor allokeringen ikke er blindet overfor personalet, vil personalet potentielt kunne huske allokeringen af tidligere deltagere. Hvis personalet samtidigt ved at der er randomiseret i blokke af otte og kan huske at der lige har været tre deltagere i intervention og fire deltagere i kontrolgruppen, så kan de regne ud at den næste deltagere vil komme i interventionsgruppen.

Løsningen på dette er at lave tilfældig variation i størrelsen af blokke, hvilket vil gøre at personalet ikke ved hvornår den en blok stopper og den næste starter. Typisk vil man lade blokstørrelsen skifte mellem 4, 6 og 8.

I funktionen, som vi brugte til at lave sekvensen med ens blokstørrelser, kunne vi bare bruge replicate()-funktionen til at lave en tabel med dimensionerne n x b. Men det kan vi ikke når blokstørrelserne varierer, fordi dimensionerne i en tabel skal være ens.
En løsning på dette er at bruge et for-loop, der bruger en c()-funktion til at kombinere hver ny blok (som liste) med den eksisterende sekvens (som også er en liste).

Funktionen skal derved bruge tre elementer:

  1. En sample()-funktion, der tilfældigt vælger en blokstørrelser a enten 4, 6 eller 8;
  2. En funktion (randomseq), der kan tage blokstørrelse som input og herefter bruger en sample()-funktion til tilfældigt at fordele lige mange af hver allokeringer indenfor blokken; samt
  3. et for-loop udfører randomseq()-funktionen n antal gange og for hver gentagelse 'klistre' den nye blok sammen med de tidligere i en samlet liste.

randomBlockVar <- function(n, seed){
  if (missing(n)){ n = 10 } # n defines the number of blocks
  if(missing(seed)){ seed = round(runif(10, min = 10, max = 99999),0)}
  set.seed(seed)

  # random sampling of c(1,2) of b length
  randomseq <- function(b){
   sample( c(rep(1,b/2),rep(2,b/2)), b, replace=FALSE )
# Alternative:
# sample( c(rep("Intervention",b/2),rep("Control",b/2)), b, replace=FALSE )
   }

  # list of the size of each block
  blocks <- sample(c(4,6,8),n,replace = T)

  # Construction of the sequence
  randomlist <- NULL
  for (i in 1:n){
   randomlist <- c(randomlist, randomseq(blocks[i]))
   }
  return(randomlist)
 }
randomBlockVar(5)

[1] 1 2 2 1 1 2 2 1 1 1 2 2 1 1 2 2 1 1 1 2 2 2 1 2 1 2 2 1 2 1


Gem sekvens som fil

Når randomiseringssekvensen skal bruges i praksis, fx hvis den skal implementeres i RedCap, er man nødt til at gemme den som en fil - fx som en komma-separeret fil (*.csv). I RedCap er det et krav at sekvensen er struktureret som en tabel, hvor den første kolonne har samme navn som den variabel, der bruges til randomisering (I nedenstående eksempel bruges variabelnavnet allocation).

write.csv(data.frame(allocation = randomBlockVar(10)), "randomseq.csv", row.names=FALSE)

Det er vigtigt at argumentet row.names sættes til FALSE, så det er allocation, der er første kolonne.


Stratificering

Hvis der er kendte eller mistænkte interaktioner mellem interventionen og en faktor hos deltagerne vil vi helst have at fordelingen af denne faktor bliver nogenlunde ens i allokeringsgrupperne. I teorien vil dette fordele sig ligeligt hvis bare studiet er stort nok i forhold til forekomsten af faktoren. Men fordi vi ofte vil begrænse antallet af deltagere i et randomiseret forsøg, kan det være en fordel af 'tvinge' fordelen til at være ens ved at stratificere randomiseringen, så man har en randomiseringssekvens for hver strata.

Det kan fx være at vi på forhånd har en formodning om at effekten af vores intervention vil være mindre, hvis patienten samtidigt er i behandling med en beta-blokker. Her laves en randomiseringssekvens til deltagere med beta-blokker behandling (med en længde på 100) og en til deltagere uden (med en længde på 100).

Fordi vi bruger en funktion med varierende blokstørrelse ved vi ikke på forhånd hvor lang sekvens den vil returnere. Men det kan vi komme udenom ved at lave en der med sikkerhed er længere end det ønskede og derefter begrænse den til det ønskede antal (i eksemplet til en længde a 100 for hvert strata).

Derefter tilføjes en ny kolonne med navn betablokker (I RedCap skal den have sammen navn som stratificerings variablen), hvor rep()-funktionen bruges til at lave en liste med først 100 gange '1' og derefter 100 gange '2'.

write.csv(
 data.frame( allocation = c(randomBlockVar(100)[1:100], randomBlockVar(100)[1:100]),
  betablokker = c(rep(1,100),rep(2,100)
 ),
 "randomseq.csv", row.names=FALSE
 )