Introduksjon til data.table

Denne posten baserer seg på en presentasjon jeg gjorde av data.table-pakka. Den beskriver hvordan man kan bruke data.table i R for å jobbe med store datasett slik som registerdata. Posten går blant annet gjennom de ulike posisjonene i klammene [i,j,by] og viser hvordan man kan lage nye variabler i data.table med :=.

Øyvind Bugge Solheim https://www.oyvindsolheim.com (Institutt for samfunnsforskning)https://www.samfunnsforskning.no
2023-11-09

Nyhet: ConciseR.

Jeg har testet ut chatGPT sin nye GPT-bygger til å lage en data.table-spesifikk GPT. Alle som betaler for chatGPT får tilgang med denne lenken.

Hva er fordelene med data.table?1

Fordelene med data.table knytter seg primært til hastighet og effektivisering. Når man indekserer en data.table får man veldig raske oppslag i tabellen. data.table reduserer også mengden kopiering som skjer i R. I standard R overskrives objekter hver gang du endrer dem. Hver gang du lagrer med <- lages det en ny kopi av datasettet. Dette skjer også når du beholder det samme navnet for datasettet. I data.table reduseres denne typen kopier til et minimum noe som øker gjør det mye mer effektivt å bruke med store datasett. data.table har i tillegg effektiv kode for å gjennomføre grupperte utregninger.

Innlasting av pakker

Vi begynner med å laste inn data.table-pakka.

# install.packages("data.table")

library("data.table")

Innlasting av datasett

Laster inn csv-er med fread. fread er data.tables versjon av read.csv og laster veldig raskt inn store csv-filer.


alder <- fread("alder.csv")
alder[,alder:=`.`]
arbeid <- fread("arbeid.csv")
inntekt_kjonn <- fread("inntekt_kjonn.csv")
#Ikke veldig stor forskjell her, men akkurat synlig:
#as.data.table(read.csv2("inntekt_kjonn.csv"))

Syntaks i data.table

Syntaks i data.table

Som i vanlig (base) R forholder data.table seg til ulike deler av klammene, men i data.table har klammene flere posisjoner som kan brukes til ulike ting. I tillegg kjører vi som regel koden inne i klammene istedenfor rundt datasettet (base) eller kjedet med piper (tidyverse).

Klammene til en data.table har tre “posisjoner”: DT[i, j, by]. i brukes som i vanlige data.frame til å velge eller organisere tabellen. j viser som i data.frame til hvilke variabler man ønsker, men her kan man også lage nye variabler og bruke funksjoner på en variabel. by viser til grupperingen eller organiseringen (keyby) som vi ønsker å regne ut i fra.

NB: siden rekkefølgen er viktig jobber vi ofte med tomme felt. Dette gjelder særlig i som ofte bare står tom. For eksempel dt[,mean(variabel)]

Subsetting i dt[i]

Inne i en data.table kan vi omtale variablene i tabellen direkte uten gåseøyne. Dette er veldig praktisk. Vi kan subsette ved å omtale variablene direkte inne i i . Vi kan også regne ut for eksempel gjennomsnitt i j.


Kan subsette med ==

inntekt_kjonn[kjonn==1]
FALSE          inntekt kjonn    idnr
FALSE       1:   32596     1  463599
FALSE       2:   12711     1 1386844
FALSE      ---                      
FALSE  999999: 1625864     1 1215391
FALSE 1000000: 1296593     1  557819

Kan regne ut gjennomsnitt for den subsatte gruppa:

inntekt_kjonn[kjonn==0,mean(inntekt,na.rm=T)]
FALSE [1] 142562

Kan organisere tabellen:

inntekt_kjonn[order(-inntekt)]
FALSE          inntekt kjonn    idnr
FALSE       1: 1999962     1  792542
FALSE       2: 1999955     1  131002
FALSE      ---                      
FALSE 1999999:      NA     0 1519811
FALSE 2000000:      NA     0 1240485

Vi kan velge flere variabler i dt[,j] med .(var1,var2)


Den andre posisjonen i klammene er j. Der kan vi spesifisere hvilke variabler vi ønsker å se. Hvis vi vil se mer enn en variabel kan vi bruke .() som er en data.table-versjon av list(). Begge virker inne i en data.table. Vi kan også lage nye navn midlertidig inne i .(). Husk å bruke komma først for å komme til j.


Skriver ut flere variabler med .()

inntekt_kjonn[,.(inntekt,kjonn)]
FALSE          inntekt kjonn
FALSE       1:   32596     1
FALSE       2:   12711     1
FALSE      ---              
FALSE 1999999: 1822257     0
FALSE 2000000:  695376     0

Lager midlertidige variabler med .() og =

inntekt_kjonn[,.("Månedlig inntekt"=inntekt,"Kjønn numerisk"=kjonn)][1:2]
FALSE    Månedlig inntekt Kjønn numerisk
FALSE 1:            32596              1
FALSE 2:            12711              1

Henter ut variabler med alle mellom med :

inntekt_kjonn[,inntekt:idnr]
FALSE          inntekt kjonn    idnr
FALSE       1:   32596     1  463599
FALSE       2:   12711     1 1386844
FALSE      ---                      
FALSE 1999999: 1822257     0 1964207
FALSE 2000000:  695376     0 1168915

Nye variabler med := i j


I data.table lager vi nye variabler med :=. Dette legger til en variabel uten å kopiere datasettet. Dette er dermed mer effektivt enn for eksempel dt <- dt %>% mutate(x=0) fordi dplyr::mutate kopierer datasettet med <-. Når vi har store datasett er kopiering ineffektivt.


Lager første inntektsdesil:

inntekt_kjonn[inntekt<=32480,desil_1:=1]

inntekt_kjonn[inntekt>32480,desil_1:=0]

# Alternativ med fifelse
inntekt_kjonn[,desil_1_2:=fifelse(inntekt>32480,0,1)]
# Likt resultat: inntekt_kjonn[,table(desil_1,desil_1_2,useNA="ifany")]

Grupper i by


En sentral fordel med data.table er at det er veldig raskt å regne ut estimater for grupper i datasettet. Vi kan definere grupper både før vi bruker datasettet med setkey() og inne i klammene ved å legge gruppevariabelen i den tredje posisjonen. Hvis vi vil at gruppene skal komme i en viss rekkefølge kan vi bruke en ordnet faktor og bruke keyby= inne i den tredje posisjonen i klammene. Hvis vi vil regne ut gjennomsnittlig inntekt per kjønn kan vi gjøre det slik:


Gjennomsnittsinntekt etter kjønn:

inntekt_kjonn[,mean(inntekt,na.rm=T),kjonn]
FALSE    kjonn       V1
FALSE 1:     1 147790.8
FALSE 2:     0 142562.0

Eller slik med navn:

inntekt_kjonn[,.("inntekt_snitt"=mean(inntekt,na.rm=T)), keyby=kjonn]
FALSE    kjonn inntekt_snitt
FALSE 1:     0      142562.0
FALSE 2:     1      147790.8

Vi kan også regne ut nye variabler slik som gruppesentrert inntekt per kjønn:

inntekt_kjonn[,inntekt_sent:=inntekt-mean(inntekt,na.rm=T),kjonn]

data.table gir litt ulike typer print med .() og uten:

#Ny data.table:
#inntekt_kjonn[,.(inntekt_sent)]
#Bare verdier:
#inntekt_kjonn[,inntekt_sent][1:20] #Hvis ikke veldig mye

Spesialkodene .N, .I og .GRP

data.table har en del praktiske spesialkoder som man kan bruke. .N gir antallet observasjoner, .GRP viser hvilket gruppenummer man er i, .I er hvilket nummer observasjonen er.


.N er antall observasjoner

inntekt_kjonn[,.N]
FALSE [1] 2000000

Eller per gruppe:

inntekt_kjonn[,.N,keyby=kjonn]
FALSE    kjonn       N
FALSE 1:     0 1000000
FALSE 2:     1 1000000

Eller per gruppe under en viss inntekt:

# antall observasjoner under en viss inntekt per gruppe:
inntekt_kjonn[inntekt<10000,.N,keyby=kjonn]
FALSE    kjonn     N
FALSE 1:     0 30800
FALSE 2:     1 29129

.GRP gir gruppenummer

Mest relevant for andre utregninger eller loops:

# gruppenummer
inntekt_kjonn[,.GRP,keyby=kjonn]
FALSE    kjonn GRP
FALSE 1:     0   1
FALSE 2:     1   2

.I gir observasjonsnummer / rad

Minste observasjonsnummer per gruppe:

inntekt_kjonn[,min(.I),keyby=kjonn]
FALSE    kjonn      V1
FALSE 1:     0 1000001
FALSE 2:     1       1

.SD er nyttig og gir tilgang til flere kolonner


inntekt_kjonn[,.SD*2,.SDcols = c("inntekt","kjonn")]
FALSE          inntekt kjonn
FALSE       1:   65192     2
FALSE       2:   25422     2
FALSE      ---              
FALSE 1999999: 3644514     0
FALSE 2000000: 1390752     0

Enkel utregning av snitt for inntekt og inntekt_sent etter kjønn

inntekt_kjonn[,map(.SD,mean,na.rm=T),.SDcols = c("inntekt","inntekt_sent"),by=kjonn]
FALSE    kjonn  inntekt  inntekt_sent
FALSE 1:     1 147790.8 -1.827140e-10
FALSE 2:     0 142562.0  8.630197e-10

Omkoding:


Data.table har noen egne funksjoner som er praktiske når man omkoder et datasett.fcase kan omkode variabler. %chin%er rask spesialversjon av %in% for character-variabler. %between% gjør det mulig å finne rader mellom to verdier (NB: inkludert verdiene).


Omkode kjønn til å være tekstvariabel med fcase()

inntekt_kjonn[,kjonn_b:=fcase(
              kjonn == 0, "Kvinne",
              kjonn > 0, "Mann")]

Subsette med tekst med %chin%:

inntekt_kjonn[kjonn_b %chin% c("Mann"),.(kjonn,kjonn_b)]
FALSE          kjonn kjonn_b
FALSE       1:     1    Mann
FALSE       2:     1    Mann
FALSE      ---              
FALSE  999999:     1    Mann
FALSE 1000000:     1    Mann

Hente alle inne i et intervall med %between%:

inntekt_kjonn[inntekt %between% c(10000,20000),":="(nobs=.N,nobs2=.N-5),by=kjonn_b]
inntekt_kjonn[!is.na(nobs)]
FALSE        inntekt kjonn    idnr desil_1 desil_1_2 inntekt_sent kjonn_b
FALSE     1:   12711     1 1386844       1         1    -135079.8    Mann
FALSE     2:   12235     1   54966       1         1    -135555.8    Mann
FALSE    ---                                                             
FALSE 60331:   17965     0 1345531       1         1    -124597.0  Kvinne
FALSE 60332:   17546     0  961034       1         1    -125016.0  Kvinne
FALSE         nobs nobs2
FALSE     1: 29481 29476
FALSE     2: 29481 29476
FALSE    ---            
FALSE 60331: 30851 30846
FALSE 60332: 30851 30846

Vi kan også kjede klammene:

inntekt_kjonn[!is.na(inntekt),.N,
              keyby=.(kjonn_b,inntekt<800000)][,
                .(inntekt,kjonn_b,Prosent=round(100*N/sum(N),2))] 
FALSE    inntekt kjonn_b Prosent
FALSE 1:   FALSE  Kvinne    3.10
FALSE 2:    TRUE  Kvinne   46.90
FALSE 3:   FALSE    Mann    3.14
FALSE 4:    TRUE    Mann   46.87

Litt mer avansert:

inntekt_kjonn[!is.na(inntekt),.N,
              keyby=.("Kjonn"=kjonn_b,"Hoy inntekt"=inntekt<700000)][,
              .(`Hoy inntekt`,`Kjonn`,Prosent=round(100*N/sum(N),2))][,
               .SD,keyby=.(`Hoy inntekt`,`Kjonn`)]
FALSE    Hoy inntekt  Kjonn Prosent
FALSE 1:       FALSE Kvinne    3.36
FALSE 2:       FALSE   Mann    3.40
FALSE 3:        TRUE Kvinne   46.64
FALSE 4:        TRUE   Mann   46.60

Eventuelt med piper og (den litt rare) funksjonen "["()

inntekt_kjonn[!is.na(inntekt),.N,
              keyby=.("Kjønn"=kjonn_b,"Høy inntekt"=inntekt<700000)] %>% 
  "["(,.(`Høy inntekt`,`Kjønn`,Prosent=round(100*N/sum(N),2))) %>% 
  "["(,.SD,keyby=.(`Høy inntekt`,`Kjønn`))
FALSE    Høy inntekt  Kjønn Prosent
FALSE 1:       FALSE Kvinne    3.36
FALSE 2:       FALSE   Mann    3.40
FALSE 3:        TRUE Kvinne   46.64
FALSE 4:        TRUE   Mann   46.60

Merging

Mer om merging

Når to data.table-r har samme key vil de kunne slås sammen enkelt. Da brukes keyen til å slå sammen observasjoner raskt. Variablene i de to datasettene trenger ikke å ha samme navn.


To datasett med samme navn, men uten key

alder_arbeid<- alder[arbeid,on="idnr"]

Lager en ny variabel og sletter den gamle:

inntekt_kjonn[,IDen:= idnr]
inntekt_kjonn[,idnr:=NULL]

Merging på key uten samme variabelnavn:

# setter keys:
setkey(inntekt_kjonn,IDen)
setkey(alder_arbeid,idnr)
inntekt_kjonn_alder_arbeid<- inntekt_kjonn[alder_arbeid]
# Vi kunne også brukt inntekt_kjonn[alder_arbeid, on=c(IDen="idnr")]

#merge(inntekt_kjonn,alder_arbeid,by.x = "IDen",by.y = "idnr") #%>% is.data.table()

Flere eksempler på bruk av grupper i data.table:

inntekt_kjonn_alder_arbeid[,alder_grupper:=fcase(
  alder < 30, "Under 30",
  alder >= 30&alder<45, "30 til 45",
  alder >= 45&alder <60, "45 til 60",
  alder >= 60&alder <70, "60 til 70",
  alder >= 70, "Over 70")]

Regner ut gjennomsnittlig inntekt etter aldersgruppe og kjønn:

inntekt_kjonn_alder_arbeid[,
     mean(inntekt),keyby=list(alder_grupper,kjonn_b)] %>% print(10)
FALSE     alder_grupper kjonn_b V1
FALSE  1:     30 til 45  Kvinne NA
FALSE  2:     30 til 45    Mann NA
FALSE  3:     45 til 60  Kvinne NA
FALSE  4:     45 til 60    Mann NA
FALSE  5:     60 til 70  Kvinne NA
FALSE  6:     60 til 70    Mann NA
FALSE  7:       Over 70  Kvinne NA
FALSE  8:       Over 70    Mann NA
FALSE  9:      Under 30  Kvinne NA
FALSE 10:      Under 30    Mann NA

Regner ut et estimat for hver gruppe:


Standard R er slik:

library(fixest)

coeftable(lm(inntekt~kjonn_b,data=inntekt_kjonn_alder_arbeid)) %>% 
  knitr::kable(digits=c(0,1,1,2))
Estimate Std. Error t value Pr(>|t|)
(Intercept) 142562 345.9 412.1 0
kjonn_bMann 5229 489.2 10.7 0

For å kjøre det inne i data.table må vi spesifisere data=.SD

inntekt_kjonn_alder_arbeid[,coeftable(lm(inntekt~kjonn_b,data = .SD))] %>% 
  knitr::kable(digits=c(0,1,1,2))
Estimate Std. Error t value Pr(>|t|)
(Intercept) 142562 345.9 412.1 0
kjonn_bMann 5229 489.2 10.7 0

For å kjøre for alle grupper:


inntekt_kjonn_alder_arbeid[,
    as.list(coeftable(lm(inntekt~kjonn_b,data = .SD))[2,]),
    keyby=list(alder_grupper,arbeid)] %>% 
  kable(digits =c(0,0,0,0,1,2))
alder_grupper arbeid Estimate Std. Error t value Pr(>|t|)
30 til 45 Arbeid 5974 1217 4.9 0.00
30 til 45 Student 7698 2917 2.6 0.01
30 til 45 Trygd 6690 2239 3.0 0.00
45 til 60 Arbeid 6214 1188 5.2 0.00
45 til 60 Student 4605 4203 1.1 0.27
45 til 60 Trygd 4280 2058 2.1 0.04
60 til 70 Arbeid 2396 1748 1.4 0.17
60 til 70 Pensjon 4897 2244 2.2 0.03
60 til 70 Student -6910 5968 -1.2 0.25
60 til 70 Trygd 8661 3012 2.9 0.00
Over 70 Pensjon 4876 1172 4.2 0.00
Under 30 Arbeid 4601 1590 2.9 0.00
Under 30 Student 5084 1585 3.2 0.00

Samme kode med en loop:

lista <- list()
# looper over alder
for(i in unique(inntekt_kjonn_alder_arbeid$alder_grupper)){
  #looper over arbeid
  for(j in unique(inntekt_kjonn_alder_arbeid$arbeid)){
    #Sjekker at det finnes observasjoner med alder*arbeid
    if(any(inntekt_kjonn_alder_arbeid[,alder_grupper==i&arbeid==j])){
      #Regner ut:
    lista[[i]] <- flatten(list(Aldersgruppe=i,Arbeid=j,
                               as.list(coeftable(lm(inntekt~kjonn_b,
    data=inntekt_kjonn_alder_arbeid[alder_grupper==i&arbeid==j,]))[2,])))
    }
  }
}

Gjør om til tabell

rbindlist(lista) %>%   kable(digits =c(0,0,0,0,2,3))
Aldersgruppe Arbeid Estimate Std. Error t value Pr(>|t|)
30 til 45 Trygd 6690 2239 2.99 0.003
Under 30 Student 5084 1585 3.21 0.001
45 til 60 Trygd 4280 2058 2.08 0.038
Over 70 Pensjon 4876 1172 4.16 0.000
60 til 70 Pensjon 4897 2244 2.18 0.029

  1. Alle illustrasjonene er lagd av OpenAIs chatgpt og Dall-e.↩︎

Citation

For attribution, please cite this work as

Solheim (2023, Nov. 9). Solheim: Introduksjon til `data.table`. Retrieved from https://www.oyvindsolheim.com/code/data.table/

BibTeX citation

@misc{solheim2023introduksjon,
  author = {Solheim, Øyvind Bugge},
  title = {Solheim: Introduksjon til `data.table`},
  url = {https://www.oyvindsolheim.com/code/data.table/},
  year = {2023}
}