8  Velge rader

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.2     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Vi skal diskutere utvelgelse av rader, ofte kjent som filtrering.

8.1 Filter

Filtre bruker vi svært ofte. De lar oss begrense mengden data vi ser på og arbeider på. Vi får filtra våre dplyr. Merk at det finnes en filter-funksjon i stats-pakka som lastes inn når vi starer R, og dette filteret blir overskrevet av dplyr/tidyverse. Om du får feilmelding når du bruker filter kan det være at du har glemt å laste inn dplyr, og at R forsøker å bruke statsfilter().

Filtret velger ut rader ved å sjekke ut innholdet i en kolonne. Vi kan velge ut alle rader som har en viss verdi på en kolonne. La oss se på at alle menneskene i datasettet starwars.

starwars %>% filter(species == "Human")
# A tibble: 35 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 3 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 4 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 5 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 6 Biggs D…    183    84 black      light      brown           24   male  mascu…
 7 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
 8 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
 9 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
10 Han Solo    180    80 brown      fair       brown           29   male  mascu…
# ℹ 25 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filteret velger alle radene hvor sammenlikninga vi oppgir er TRUE (sann). Mer robotisk kan vi si at den i tilfellet over velger rader hvor cella under kolonna species tilfredsstiller betingelsen “innholdet i cella er lik Human”. Dermed kan vi oppgi andre uttrykk som evalueres til enten TRUE eller FALSE. Hva med å hente ut alle som er høyere enn 170 cm?

starwars %>% filter(height > 170)
# A tibble: 54 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 3 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 4 Biggs D…    183    84 black      light      brown           24   male  mascu…
 5 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
 6 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
 7 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
 8 Chewbac…    228   112 brown      unknown    blue           200   male  mascu…
 9 Han Solo    180    80 brown      fair       brown           29   male  mascu…
10 Greedo      173    74 <NA>       green      black           44   male  mascu…
# ℹ 44 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Vi kan oppgi flere betingelser. Hva med alle kvinnelige mennesker?

starwars %>% filter(species == "Human" & sex == "female")
# A tibble: 9 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Leia Org…    150    49 brown      light      brown             19 fema… femin…
2 Beru Whi…    165    75 brown      light      blue              47 fema… femin…
3 Mon Moth…    150    NA auburn     fair       blue              48 fema… femin…
4 Shmi Sky…    163    NA black      fair       brown             72 fema… femin…
5 Cordé        157    NA brown      light      brown             NA fema… femin…
6 Dormé        165    NA brown      light      brown             NA fema… femin…
7 Jocasta …    167    NA white      fair       blue              NA fema… femin…
8 Rey           NA    NA brown      light      hazel             NA fema… femin…
9 Padmé Am…    165    45 brown      light      brown             46 fema… femin…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Ikke så veldig mange. Ikke rart Star Wars filer Bechdel-testen. Får vi med flere hvis vi ikke skiller mellom sex og gender? Altså at vi tar med dem er enten female eller feminine?

starwars %>% filter(species == "Human" & (sex == "female" | gender == "feminine"))
# A tibble: 9 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Leia Org…    150    49 brown      light      brown             19 fema… femin…
2 Beru Whi…    165    75 brown      light      blue              47 fema… femin…
3 Mon Moth…    150    NA auburn     fair       blue              48 fema… femin…
4 Shmi Sky…    163    NA black      fair       brown             72 fema… femin…
5 Cordé        157    NA brown      light      brown             NA fema… femin…
6 Dormé        165    NA brown      light      brown             NA fema… femin…
7 Jocasta …    167    NA white      fair       blue              NA fema… femin…
8 Rey           NA    NA brown      light      hazel             NA fema… femin…
9 Padmé Am…    165    45 brown      light      brown             46 fema… femin…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Nei.

Men vi fikk illustrert filteret. Vi bruker noen logiske operatorer for å kombinere ulike ledd i betingelsene våre.

  • &: og. Både x og y må være tilfredsstilt.
  • |: eller. Enten x eller y må være tilfredsstilt.
  • ==: er lik. x må være lik y.
  • !=: er ikke lik. x må være ulik y.
  • < og >: mindre enn og større enn
  • <= og >=: mindre enn eller lik og større enn eller lik.

Og vi bruker () for å gruppere betingelser sammen. La oss se på hva som skjer med antall rader som blir tatt med når vi fjerner ().

# Med () rundt female og feminine
starwars %>% 
  filter(species == "Human" & (sex == "female" | gender == "feminine")) %>% 
  nrow()
[1] 9
# Uten ()
starwars %>% 
  filter(species == "Human" & sex == "female" | gender == "feminine") %>% 
  nrow()
[1] 17

I det første eksemplet må du være 1) menneske og 2) enten female eller feminine. I det andre eksemplet må du være 1) menneske og female eller 2) feminine. Dermed får vi med oss en del roboter og romvesener som er feminine i det andre eksemplet.

Vi bruker ofte filtre for å fjerne deler av et datasett, for eksempel dersom vi bare vil se på Trondheim. Da filtrer vi kanskje med ei kolonne som inneholder

  • kommunnummeret til Trondheim (5001),
  • det gamle kommunenummeret til Trondheim (1601),
  • en tekststreng med navet til byen (“Trondheim”),
  • en tekststreng med det gamle navnet på byen (“Trondhjem”),
  • en tekststreng med navnet på byen uten stor forbokstav (“Trondheim”), eller
  • en tekststreng med navnet på byen feilstava (“Trodnheim”).
  • en tekststreng med navnet på byen og noe tilleggstekst som vi ikke trenger (“Trondheim by”)

Antakelig ikke alle på en gang, men det er greit å vite hvordan man gjør en delvis match. Spesielt når vi får en liste med navn på ting som må matches med våre egne data er det typisk at de skriver navna noe annerledes enn oss. Dette er gjerne fordi det ikke egentlig er noen standard måte å skrive navna på, eller fordi navna endres. Barnehagedatasett er et typisk eksempel her.

Å matche på ulike numre handler vanligvis om å sette sammen en serie med eller betingelser via |. Vi fokuserer derfor på tekststrenger. Til dette finner vi en svært nyttig pakke som heter stringr.

Pakka handler ikke om å selge dop i Baltimore, men om å håndtere tekststrenger.

8.2 Stringr

stringr har drøssevis av nyttige funksjoner for oss, og vi har så klart ikke tid å gå innom alt. En del av funksjonene baserer seg på noe som kalles regulære uttrykk (regular expressions), ofte henvist til som regex. Regex er serier med tegn som spesifiserer et spesifikt mønster. Det brukes når vi vil ha treff på flere varianter.

Under bruker jeg regex for å treffe på to skrivemåter av Trondheim. Vi bruker str_detect() for å detektere strenger i en kolonne som matcher et mønster. Mønsteret er altså Trondheim eller Trondhjem. Siden den eneste forskjellen på disse to er om vi bruker ei eller je i slutten, kan vi skrive dette som "Trondh(ei|je)m".

# Lager et fiktivt lite datasett
byer <- tibble(
  by = c("Trondheim", "Trondhjem", "Drammen"),
  valuta = c("NOK", "riksdaler", "NOK")
)

# Filter ved hjelp av regex
byer %>% 
  filter(str_detect(by, "Trondh(ei|je)m"))
# A tibble: 2 × 2
  by        valuta   
  <chr>     <chr>    
1 Trondheim NOK      
2 Trondhjem riksdaler
# En mindre effektiv måte å gjøre dette på ville vært
byer %>% 
  filter(by == "Trondheim" | by == "Trondhjem")
# A tibble: 2 × 2
  by        valuta   
  <chr>     <chr>    
1 Trondheim NOK      
2 Trondhjem riksdaler

Du kan spørre deg hvorfor det andre eksemplet er mindre effektivt når det bare koster oss noen få ekstra tegn. Fordi jeg må gjenta meg sjøl når jeg skriver by == og Trondh. Akkurat i dette tilfellet er det ikke særlig alvorlig. Men når vi utvider og får større datasett og mer avanserte søkemønstre vil det begynne å gjøre seg gjeldende. En ting er at vi sparer tid på skrive mindre. En annen ting er at det blir lettere å gjøre endringer seinere. Fordi vi ikke må endre en serie med eller-betingelser, men kun den ene regex-en.

Alle funksjonene i stringr starter med str_, som gjør dem lett å søke opp. Nyttig, for det er mange av dem! De jeg bruker oftest er

  • str_sub() for å hente ut en del av en streng
  • str_detect() i kombinasjon med filter() for å finne en delvis match i en kolonne
  • str_replace() for å erstatte del av en streng med noe annet
  • str_to_lower() og str_to_upper() for å fjerne feilkilder når jeg søker. Spesielt den første er nyttig. Hvis jeg skal matche på navn vil jeg unngå at jeg får ikke-match bare fordi noen navn har store bokstaver enkelte steder mens andre ikke. Jeg vil at “Byåsen barnehage” skal matche med Byåsen Barnehage. Merk at det finnes varianter av disse i base R også.

La oss se noen eksempler på bruk av disse

# Hent ut de fire første bokstavene av en kolonne
byer %>% 
  mutate(by4 = str_sub(by, 1, 4))
# A tibble: 3 × 3
  by        valuta    by4  
  <chr>     <chr>     <chr>
1 Trondheim NOK       Tron 
2 Trondhjem riksdaler Tron 
3 Drammen   NOK       Dram 
# Bytt ut deler av en streng
byer %>% 
  mutate(nytt_bynavn = str_replace(by, "Trond", "Trønder"))
# A tibble: 3 × 3
  by        valuta    nytt_bynavn
  <chr>     <chr>     <chr>      
1 Trondheim NOK       Trønderheim
2 Trondhjem riksdaler Trønderhjem
3 Drammen   NOK       Drammen    
# Gjøre om en kolonne til små bokstaver. 
byer %>% 
  mutate(valuta_lower = str_to_lower(valuta))
# A tibble: 3 × 3
  by        valuta    valuta_lower
  <chr>     <chr>     <chr>       
1 Trondheim NOK       nok         
2 Trondhjem riksdaler riksdaler   
3 Drammen   NOK       nok         

stringr har mange muligheter i seg. Spesielt bruken av regex er svært nyttig, som nevnt over. Men læringskurva er bratt. Og særlig det å søke etter tall i en tekststreng. Noen nyttige funksjoner her er å kombinere tegntype og kvantitet. Sjekk ut side to av stringr-jukselappen til Posit.

Her lager jeg et rart datasett for å vise hvordan vi kan bruke disse regex-kommandoene. Datasettet blir laga ved å sette sammen tilfeldige serier med tall og bokstaver som vi seinere kan søke på. Her bruker jeg set.seed() for å sørge for at de påfølgende randomiserte prosessene blir like hver gang de kjøres. Slik at du og jeg ser de samme talla hver gang.

set.seed(123)

# En funksjon som lager en serie av siffer og bokstaver av ulik lengde.
lag_streng <- function() {
  tall <- seq(1:10) %>% as.character()
  bokstaver <- letters[1:10]
  tall_bokstaver <- c(tall, bokstaver)
  
  paste0(sample(tall_bokstaver, size = runif(1, 3, 5), replace = TRUE), collapse = "")
}

set.seed(123)

# Setter det sammen i et datasett.  
utvalg <- tibble(
  id = seq(1:200), 
  name = replicate(200, lag_streng())
)

# Slik ser det ut. 
utvalg
# A tibble: 200 × 2
      id name 
   <int> <chr>
 1     1 eid  
 2     2 10ha5
 3     3 d5i9 
 4     4 38710
 5     5 i4dg 
 6     6 7be10
 7     7 799  
 8     8 762  
 9     9 58b  
10    10 ch1  
# ℹ 190 more rows
# La oss finne de navna hvor det er to bokstaver etterfulgt av to nummer
utvalg %>% 
  filter(str_detect(name, "[[:alpha:]]{2}[[:digit:]]{2}"))
# A tibble: 12 × 2
      id name  
   <int> <chr> 
 1     6 7be10 
 2    16 jd38  
 3    32 bg10  
 4    43 dd61  
 5    49 ch106 
 6    52 hg12  
 7    56 ff34  
 8    77 bc1010
 9    97 2fi10 
10   155 hhj10 
11   162 af54  
12   189 cd29  

Dette mønstret ser overveldende ut, så la oss pakke det ut: "[[:alpha:]]{2}[[:digit:]]{2}"

  • [[:alpha:]] treffer alle bokstaver.
  • [[:digit:]] treffer alle tall.
  • {2} betyr nøyaktig to forekomster av det som kom før meg. Vi bruker den både for bokstaver og tall. Alternativet ville vært å skrevet f.eks. [[:alpha:]][[:alpha:]]

Det er verdt å tenke litt på tegnkoding igjen. Hvordan lagres informasjon om tall og bokstaver på pc-en? Hvordan behandles tall og bokstaver (og symboler) annerledes av programmeringsspråk som R? En tallserie vil f.eks. sorteres annerledes avhengig av om den er koda som numerisk eller streng.

# En vektor som inneholder en serie fra 1 til 24 i tilfeldig rekkefølge
tall <- sample(c(1:24), 24)

# Vi sortere den først når den er numerisk og deretter når den er en streng.
tall %>% sort()
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
tall %>% as.character() %>% sort()
 [1] "1"  "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2"  "20" "21" "22"
[16] "23" "24" "3"  "4"  "5"  "6"  "7"  "8"  "9" 

I neste kodeblokk lager jeg to datasett. Det første er likt det vi hadde i sted, foruten at jeg kun henter ut de første ti radene. Det andre datasettet er likt dette, foruten at jeg har bedt om kun ett siffer (digit) på slutten. For å vise tydeligere hva som skjer binder jeg de to radene sammen.

foo <- utvalg %>% 
  filter(str_detect(name, "[[:alpha:]]{2}[[:digit:]]{2}")) %>% 
  slice(1:10)

bar <- utvalg %>% 
  filter(str_detect(name, "[[:alpha:]]{2}[[:digit:]]{1}")) %>% 
  slice(1:10)

bind_cols(foo, bar)
New names:
• `id` -> `id...1`
• `name` -> `name...2`
• `id` -> `id...3`
• `name` -> `name...4`
# A tibble: 10 × 4
   id...1 name...2 id...3 name...4
    <int> <chr>     <int> <chr>   
 1      6 7be10         2 10ha5   
 2     16 jd38          6 7be10   
 3     32 bg10         10 ch1     
 4     43 dd61         12 fj6     
 5     49 ch106        14 hg2     
 6     52 hg12         16 jd38    
 7     56 ff34         17 bd3d    
 8     77 bc1010       21 gd3     
 9     97 2fi10        23 ga7     
10    155 hhj10        26 fa4b    

Legg merke til kolonna helt til høyre. Vi ba om kun ett siffer, likevel får vi “7be10” og “jd38”. Hvorfor? Fordi de inneholder, nettopp “ett siffer”. Dette sifferet er tilfeldigvis etterfulgt av et annet siffer, men det sa vi ikke spesifikt at vi skulle unngå. Her gjorde jeg en antakelse som viste seg å ikke stemme. Det var en antakelse jeg ikke var helt bevisst at jeg gjorde, og det var at koden min skulle vise meg rader som inneholder to bokstaver og ett, og kun ett, ikke to eller flere, siffer.

Vi ser det samme skjer med den første kolonna også: her får vi treff på to bokstaver etterfulgt av tre og fire siffer. Hvis vi skulle fiksa koden til å treffe på “to bokstaver etterfulgt av kun to og ikke flere siffer” kunne vi endra det slik:

utvalg %>% 
  filter(str_detect(name, "[[:alpha:]]{2}[[:digit:]]{2}(?![[:digit:]])"))
# A tibble: 10 × 2
      id name 
   <int> <chr>
 1     6 7be10
 2    16 jd38 
 3    32 bg10 
 4    43 dd61 
 5    52 hg12 
 6    56 ff34 
 7    97 2fi10
 8   155 hhj10
 9   162 af54 
10   189 cd29 

Det jeg håper på å få fram her er at vi må stoppe opp innimellom og teste våre egne antakelser og arbeid.

8.3 Select

Vi har brukt select() før, se sec-select. Den er nyttig å ta opp igjen her. Med select() kan vi velge ut kolonner og med filter() kan vi velge ut rader. Dermed er det ofte nyttig å bruke begge sammen. Jeg bruker dem spesielt mye når jeg gjør en første gjennomgang av et skript. Da velger jeg ut kun de radene og kolonnene som jeg er interessert i og arbeider på dem. Dette gjør det lettere å se om koden min funker, uten å måtte se for mye urelevant. Når jeg veit at skriptet funker, kan jeg gå tilbake og fjerne select() og filter() slik at hele datasettet kjøres gjennom skriptet.

Det er nyttig å vite at vi kan bruke noen liknende streng-teknikker på select() som vi brukte på filter(). Vi henter inn et nytt, simulert datasett hvor den del av kolonnene våre har navn på samme form.

prognose <- tibble(
  plansone = rep(seq(5001001, 5001004), each = 2),
  kjonn = rep(c("M", "K"), 4),
  aar2023 = round(runif(8, 400, 800)),
  aar2024 = round(runif(8, 400, 800)),
  aar2025 = round(runif(8, 400, 800))
) %>% 
  tibble(
    faktisk2023 = round(aar2023 + aar2023 * (rnorm(1, 0, 10)/100)),
    faktisk2024 = round(aar2024 + aar2024 * (rnorm(1, 0, 10)/100)),
    faktisk2025 = round(aar2025 + aar2025 * (rnorm(1, 0, 10)/100))
  )
prognose
# A tibble: 8 × 8
  plansone kjonn aar2023 aar2024 aar2025 faktisk2023 faktisk2024 faktisk2025
     <int> <chr>   <dbl>   <dbl>   <dbl>       <dbl>       <dbl>       <dbl>
1  5001001 M         460     550     753         453         545         785
2  5001001 K         472     781     475         464         773         495
3  5001002 M         536     408     486         527         404         506
4  5001002 K         474     645     748         466         639         779
5  5001003 M         562     585     424         553         579         442
6  5001003 K         645     542     494         635         537         515
7  5001004 M         674     753     797         663         746         831
8  5001004 K         729     430     671         717         426         699

Her har vi kun tre av hver kolonne, men se for dere at vi hadde hatt tredve. Da ville det vært nyttig å slippe å skrive inn navnet på hver enkelt.

La oss si at vi vil ha tak i kun de faktiske befolkningstalla, altså de kolonnene som har “faktisk” i navnet. Vi kan bruke contains() eller matches() inni select().

prognose %>% 
  select(contains("faktisk"))
# A tibble: 8 × 3
  faktisk2023 faktisk2024 faktisk2025
        <dbl>       <dbl>       <dbl>
1         453         545         785
2         464         773         495
3         527         404         506
4         466         639         779
5         553         579         442
6         635         537         515
7         663         746         831
8         717         426         699

Hva er forskjellen på dem? matches() lar oss bruke en regex. Si at vi vil ha kolonner med “faktisk” i navnet, etterfulgt av en eller flere siffer, og som slutter på 5.

prognose %>% 
  select(matches("faktisk[[:digit:]]+5$"))
# A tibble: 8 × 1
  faktisk2025
        <dbl>
1         785
2         495
3         506
4         779
5         442
6         515
7         831
8         699

Jeg bruker ofte select() til å endre rekkefølgen på kolonner. Ofte vil jeg ha den kolonna jeg nettopp lagde fremst. Da er det nyttig å huske på noen av triksa fra tidy evaluation: everything(). Den lar meg slippe å skrive opp alle kolonnene i datasettet:

prognose %>% 
  select(plansone, faktisk2025, everything())
# A tibble: 8 × 8
  plansone faktisk2025 kjonn aar2023 aar2024 aar2025 faktisk2023 faktisk2024
     <int>       <dbl> <chr>   <dbl>   <dbl>   <dbl>       <dbl>       <dbl>
1  5001001         785 M         460     550     753         453         545
2  5001001         495 K         472     781     475         464         773
3  5001002         506 M         536     408     486         527         404
4  5001002         779 K         474     645     748         466         639
5  5001003         442 M         562     585     424         553         579
6  5001003         515 K         645     542     494         635         537
7  5001004         831 M         674     753     797         663         746
8  5001004         699 K         729     430     671         717         426