Introduksjon til data.table

R data.table Introduksjon Kurs

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.

Andre R-poster

Jeg har skrevet noen introduksjoner til ulike R-temaer. Mens denne posten handler om syntaksen i data.table har jeg også skrevet en innføring til tidyverse og en litt kronglete innføring til R generelt. For folk som er helt nybegynnere og ikke skal jobbe med veldig store datasett anbefaler jeg å se på tidyverse. data.table er veldig praktisk og raskt for de som jobber med større datasett. Til vanlig blander jeg de to.

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.

Dataene:

Jeg har laget data for å fungere med denne posten. Du kan generere de samme dataene med denne R-fila.

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            <int> <int>   <int>
FALSE       1:   19770     1   90682
FALSE       2:   33979     1 1705183
FALSE      ---                      
FALSE  999999:  283271     1  805674
FALSE 1000000:  326666     1 1654109

Kan regne ut gjennomsnitt for den subsatte gruppa:

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

Kan organisere tabellen:

inntekt_kjonn[order(-inntekt)]
FALSE          inntekt kjonn    idnr
FALSE            <int> <int>   <int>
FALSE       1: 1999999     1 1996730
FALSE       2: 1999989     1  425563
FALSE      ---                      
FALSE 1999999:      NA     0 1609632
FALSE 2000000:      NA     0 1434244

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            <int> <int>
FALSE       1:   19770     1
FALSE       2:   33979     1
FALSE      ---              
FALSE 1999999:  445860     0
FALSE 2000000: 1990291     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               <int>          <int>
FALSE 1:            19770              1
FALSE 2:            33979              1

Henter ut variabler med alle mellom med :

inntekt_kjonn[,inntekt:idnr]
FALSE          inntekt kjonn    idnr
FALSE            <int> <int>   <int>
FALSE       1:   19770     1   90682
FALSE       2:   33979     1 1705183
FALSE      ---                      
FALSE 1999999:  445860     0 1531342
FALSE 2000000: 1990291     0  146168

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    <int>    <num>
FALSE 1:     1 147897.4
FALSE 2:     0 142684.4

Eller slik med navn:

inntekt_kjonn[,.("inntekt_snitt"=mean(inntekt,na.rm=T)), keyby=kjonn]
FALSE Key: <kjonn>
FALSE    kjonn inntekt_snitt
FALSE    <int>         <num>
FALSE 1:     0      142684.4
FALSE 2:     1      147897.4

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 Key: <kjonn>
FALSE    kjonn       N
FALSE    <int>   <int>
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 Key: <kjonn>
FALSE    kjonn     N
FALSE    <int> <int>
FALSE 1:     0 30798
FALSE 2:     1 28896

.GRP gir gruppenummer

Mest relevant for andre utregninger eller loops:

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

.I gir observasjonsnummer / rad

Minste observasjonsnummer per gruppe:

inntekt_kjonn[,min(.I),keyby=kjonn]
FALSE Key: <kjonn>
FALSE    kjonn      V1
FALSE    <int>   <int>
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            <num> <num>
FALSE       1:   39540     2
FALSE       2:   67958     2
FALSE      ---              
FALSE 1999999:  891720     0
FALSE 2000000: 3980582     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    <int>    <num>         <num>
FALSE 1:     1 147897.4  5.485833e-10
FALSE 2:     0 142684.4 -5.310004e-11

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          <int>  <char>
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          <int> <int>   <int>   <num>     <num>        <num>  <char>
FALSE     1:   19770     1   90682       1         1    -128127.4    Mann
FALSE     2:   16886     1 1856172       1         1    -131011.4    Mann
FALSE    ---                                                             
FALSE 60525:   11058     0 1304793       1         1    -131626.4  Kvinne
FALSE 60526:   15233     0     768       1         1    -127451.4  Kvinne
FALSE         nobs nobs2
FALSE        <int> <num>
FALSE     1: 29586 29581
FALSE     2: 29586 29581
FALSE    ---            
FALSE 60525: 30940 30935
FALSE 60526: 30940 30935

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 Key: <kjonn_b, inntekt>
FALSE    inntekt kjonn_b Prosent
FALSE     <lgcl>  <char>   <num>
FALSE 1:   FALSE  Kvinne    3.10
FALSE 2:    TRUE  Kvinne   46.90
FALSE 3:   FALSE    Mann    3.14
FALSE 4:    TRUE    Mann   46.86

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 Key: <Hoy inntekt, Kjonn>
FALSE    Hoy inntekt  Kjonn Prosent
FALSE         <lgcl> <char>   <num>
FALSE 1:       FALSE Kvinne    3.37
FALSE 2:       FALSE   Mann    3.40
FALSE 3:        TRUE Kvinne   46.63
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 Key: <Høy inntekt, Kjønn>
FALSE    Høy inntekt  Kjønn Prosent
FALSE         <lgcl> <char>   <num>
FALSE 1:       FALSE Kvinne    3.37
FALSE 2:       FALSE   Mann    3.40
FALSE 3:        TRUE Kvinne   46.63
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 Key: <alder_grupper, kjonn_b>
FALSE     alder_grupper kjonn_b    V1
FALSE            <char>  <char> <num>
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) 142684 346.1 412.2 0
kjonn_bMann 5213 489.5 10.6 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) 142684 346.1 412.2 0
kjonn_bMann 5213 489.5 10.6 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 3168 1219 2.6 0.01
30 til 45 Student 730 2925 0.2 0.80
30 til 45 Trygd 4856 2221 2.2 0.03
45 til 60 Arbeid 7167 1200 6.0 0.00
45 til 60 Student 9350 4087 2.3 0.02
45 til 60 Trygd 2076 2054 1.0 0.31
60 til 70 Arbeid 5780 1759 3.3 0.00
60 til 70 Pensjon 4446 2231 2.0 0.05
60 til 70 Student -3535 6005 -0.6 0.56
60 til 70 Trygd 9298 3053 3.0 0.00
Over 70 Pensjon 5705 1170 4.9 0.00
Under 30 Arbeid 5416 1585 3.4 0.00
Under 30 Student 6268 1577 4.0 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|)
45 til 60 Student 9350 4087 2.29 0.022
30 til 45 Student 730 2925 0.25 0.803
Under 30 Student 6268 1577 3.97 0.000
60 til 70 Student -3535 6005 -0.59 0.556
Over 70 Pensjon 5705 1170 4.88 0.000

  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}
}