5  Import og eksport av data

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
library(here)
here() starts at /Users/havaka/R/books/byplankontoRet

Vi har en god del muligheter når det kommer til å laste inn data. Det er bra, for dataene våre finnes i mange formater:

La oss bli filosofiske et øyeblikk: hva er egentlig en datafil?

Det varierer ut fra hvilket filformat vi prater om. Hvis vi tenker oss om, vil et svar kunne dreie seg om følgende punkter: Vi er interessert i datapunkter lagra i celler. Vi kan identifisere cellene ut fra hvilken rad og kolonne de står i. Det er fint å få med variabelnavn/kolonnenavn.

Felles for mange av filformata på lista over er at de gjør mye mer enn dette. Excel inneholder fargeformateringer på celler, funksjoner som regner ut innhold i celler, og djevelens verste verk: den sammenslåtte cella. SPSS kan gi oss filer med både en verdi og en merkelapp.

Den beskjedne .csv-fila gir oss kun det vi trenger og frister oss ikke ut i uføre ved å la oss bli mer avanserte enn dette. Den holder oss ærlig ved at vi ikke kan gjøre dumme ting som ødelegger for oss seinere.

Når det er sagt, det er ikke alltid .csv strekker til. Når vi jobber med kart-data trenger vi geoinformasjon, og da må vi si farvel til fordel for formater som .gpkg.


5.1 Felles mønstre

Importering og eksportering henger sammen, så vi kan omtale dem samtidig. Det er større forskjell på de ulike formatene vi håndterer, så vi organiserer oss etter dem. Imidlertid er det noen grunnleggende mønstre vi kan diskutere felles.

De fleste importeringsfunksjoner kalles noe med read, fordi de leser inn filer. Dermed blir eksporteringsfunksjoner write, fordi de skriver filer til disken.

5.1.1 Filnavn

Vi må ofte definere et navn på fila vi skal skrive eller lese. Når vi leser filer, vil navnet ofte bety både

  1. hva heter fila og
  2. hvor ligger fila lagra

Dette er fordi et filnavn strengt tatt inkludere hele filstien til fila. Den fila jeg arbeider på nå heter f.eks. import-export.qmd. Den ligger på dette filområdet på datamaskina mi: C:/Users/HK2Q/Documents/r/dokumentasjon. Dermed blir det egentlige navnet på fila: C:/Users/HK2Q/Documents/r/dokumentasjon/import-export.qmd. Husk at også filtutvidelsen (det som kommer ettter .) er en del av filnavnet! Når en funksjon ber om file eller path betyr dette ofte at de vil ha hele det fulle filnavnet inkludert filsti.

5.1.2 Relative fillokasjoner

Man kan alltids henvise til konkrete områder på maskina, men dette er optimalt fordi det gjør at du aldri kan flytte noen filer igjen. Dessuten vil ikke koden funke på en annen persons PC med mindre de har 100 % likt oppsett på deres maskin. Derfor er det nyttig med relative filstier. Når du arbeider i Rstudio (og du er ikke en gærning som arbeider i R GUI, er du vel?) forventes det at du arbeider i såkalte prosjekter. Alle mine prosjekter ligger kopiert på M:-disken. Et prosjekt er en mappe med visse filer i seg, hvorav den viktige fila er prosjekt_navn.Rproj. Denne opprettes automatisk når du lager et prosjekt via Rstudio. Det er mange flotte ting med prosjekter, og en av dem er at alle filstier defineres ut fra prosjektets rotmappe. Rotmappa er den mappa hvor .Rproj-filer ligger, og der du putter alle mapper og filer assosiert med prosjektet. Når du arbeider i prosjekter trenger du ikke definere hele filstien til en fil, bare hvor den ligger i forhold til rotmappa. F.eks. har jeg for denne boka lagt alle bilder i en mappe som heter img. Hvis jeg vil henvise til et bilde skrive jeg bare img/bilde.png. Gjør det til en vane å bruke relative filstier når du kan!

Det går så klart ikke alltid. Når noe ligger på f.eks. M:-disken må jeg lage en full filsti. Fordelen er at M: er en delt disk, så jeg kan anta at filstien vil se likt ut for andre.

Merk at i noen tilfeller brytes antakelsen om at filstien alltid er relativt til rotmappa. I disse tilfellene er pakka here svært nyttig. Den er også nyttig på grunn av noe annet, nemlig skråstrekproblematikken

5.1.3 Skråstreker til besvær

I Windows brukes denne skråstreket \ til å indikere ei mappe. I alle UNIX-baserte operativsystemet og programmer brukes /. Eksempler på sistnevnte er Ubuntu, macOS, og R. Når vi arbeider med R på Windows skjer det dermed en del arbeid i kulissene når vi henviser til en filplassering. Dette blir åpenbart for oss når vi for eksempel forsøker å lime inn en filsti fra Windows explorer (filutforskeren). R godtar ikke uten videre “feil” skråstrek. Det er to løsninger på dette:

  1. endre skråstrekene så de går andre vei
  2. escape skråstrekene

Det siste innebærer å bruke det som kalles escape characters. En del tegn har meninger i koden. F.eks. betyr # kommentar i et R-skript. Hvis jeg vil skrive ut emneknaggen, må jeg legge på en escape character så R skjønner at dette tegnet skal ikke skal tolkes slik det vanligvis tolkes. Hva er escape-tegnet? Det er nettopp \. For å escape # skriver vi dermed \#, og for å escape \ skriver vi altså \\.

# Ok
"C:/Users/HK2Q/Documents/r/dokumentasjon"

# Ikke ok
"C:\Users\HK2Q\Documents\r\dokumentasjon"

# Ok
"C:\\Users\\HK2Q\\Documents\\r\\dokumentasjon"

En kjapp måte å få skråstrekene etter å ha kopiert en filsti i Windows er følgende

# Skriver ut filsti med esaped `\` til konsollen. Funksjonen leser 
# innholdet i utklippstavla og limer det inn i konsollen.
paste0(readClipboard())

Alt dette for å si: here pakka løser en del av problema våre. Les mer om den på Ode to the here package.

5.1.4 Tegnkoding

Spesielt når det kommer til norske .csv-filer hender det vi får et problem med tegnkodinga (character encoding). En full gjennomgang blir for omfattende. Det holder å si at, igjen, dette er hovedsaklig et Windows-problem. Ideelt sett vil vi ha alt over i unicode (UTF-8). Noen filer er lagra i et annet format. Gjerne ISO8859-1 som er en av standardene som gir oss skandinaviske tegn. En forkludrende faktor er at det tidvis (og inntil ganske nylig, per 2023-03) har vært problemer med R og/eller Rstudio når det kommer til tegnkoding. Disse blir fiksa med tida og er kanskje allerede fiksa. Du ser problemet dukke opp dersom du forventer å se en æ. ø eller å i outputen og istedet får noe sånt som "\xe6\xf8\xe5", æøå eller <U+00C6>. Der er mange ulike faktorer som kan være årsak til dette problemet. En av dem kan være at du må sette tegnkoden spesifikt når du leser en fil. I noen av eksemplene mine vil du set at jeg har spesifisert encoding, og da er det derfor.

5.2 Tekstfiler (csv med familie)

Vi kan bruke read.csv() fra utils, en av pakkene som lastes når vi starter R. Det finnes også noen funksjoner fra readr, en del av, 100 poeng til den som gjetter rett, tidyverse. Herfra får vi read_csv(), read_csv2(), read_tsv() og read_delim(). Les dokumentasjon for å finne mer informasjon om dem. Kort fortalt er forskjellen at alle tre er implementeringer av den mer generelle _delim(). La oss skrive en fil og deretter last den inn.

# Dette datasettet ligger klart når vi laster inn R.
mtcars

# La oss lagre det som en csv-fil
mtcars %>% 
  write_csv(file = here("data", "mtcars.csv"))
# Og så laster vi den inn igjen
biler <- read_csv(file = here("data", "mtcars.csv"), name_repair = "universal")
Rows: 32 Columns: 11
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (11): mpg, cyl, disp, hp, drat, wt, qsec, vs, am, gear, carb

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
biler
# A tibble: 32 × 11
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
# ℹ 22 more rows

Jeg foretrekker readrs funksjoner fordi de har mange nyttige alternativer slik som name_repair = "universal". Denne passer på at navna i datasettet er på et format som R tolererer. F.eks. at de ikke har mellomrom i seg. Veldig nyttig. Med na = kan du fortelle R hvordan missing er lagra i fila du importerer.

5.3 SPSS

For SPSS-filer bruker vi pakka haven. Denne pakka er en del av … ja, du skjønner.

library(haven)
# Lagrer en fil som en spss-fil. (Jeg lagrer bare de første fire kolonnene).
starwars %>% 
  select(1:4) %>% 
  write_sav(here("data", "starwars.sav"))
# Les inn en spss-fil
stjernekrig <- read_sav(here("data", "starwars.sav"), .name_repair = "universal")
stjernekrig %>% head()
# A tibble: 6 × 4
  name           height  mass hair_color   
  <chr>           <dbl> <dbl> <chr>        
1 Luke Skywalker    172    77 "blond"      
2 C-3PO             167    75 ""           
3 R2-D2              96    32 ""           
4 Darth Vader       202   136 "none"       
5 Leia Organa       150    49 "brown"      
6 Owen Lars         178   120 "brown, grey"

Legger du merke til noe med fila over? Hva om jeg printer ut de tilsvarende kolonner fra det opprinnelige datasettet vårt?

starwars %>% 
  select(1:4) %>% 
  head()
# A tibble: 6 × 4
  name           height  mass hair_color 
  <chr>           <int> <dbl> <chr>      
1 Luke Skywalker    172    77 blond      
2 C-3PO             167    75 <NA>       
3 R2-D2              96    32 <NA>       
4 Darth Vader       202   136 none       
5 Leia Organa       150    49 brown      
6 Owen Lars         178   120 brown, grey

Hårfarge har mista NA-designasjonen. Nå er de som før var missing bare tomme. Dette kan skape hodebry for oss seinere, så det er bra vi oppdaga det nå.

For å være helt ærlig er jeg ikke sikker på hvordan man løser dette direkte. Feilen oppstår enten når vi eksporterer til .sav eller importerer tilbake til R. Kanskje finnes det et svar i Havens dokumentasjon. Imidlertid er det lett å omgå problemet i ettertid:

# Lag ny versjon av hårfarge. Hvis hårfarge er tom (""), bli missing. Ellers, 
# bli det du allerede er. 
stjernekrig <- stjernekrig %>% 
  mutate(
    hair_color = if_else(
      hair_color == "", 
      NA_character_, 
      hair_color)
    )
stjernekrig %>% head()
# A tibble: 6 × 4
  name           height  mass hair_color 
  <chr>           <dbl> <dbl> <chr>      
1 Luke Skywalker    172    77 blond      
2 C-3PO             167    75 <NA>       
3 R2-D2              96    32 <NA>       
4 Darth Vader       202   136 none       
5 Leia Organa       150    49 brown      
6 Owen Lars         178   120 brown, grey

Hvorfor NA_character_ og ikke bare NA? if_else forventer at alle argumentene skal være av samme type/klasse. Derfor må til og med NA være en spesiell type NA. Siden hair_color er en strengvektor, må NA være en streng-NA.

Når vi laster inn SPSS-filer vil vi ofte få med merkelappene (labels) derfra også, i form av attributter. tidyverse-pakker talker ofte dette og viser dem når vi printer objektene. Noen ganger har jeg opplevd, med andre pakker, at attributtene ikke kan leses. I så fall kan man bare fjerne dem.

5.4 Excel

Jeg er unødvendig streng mot Excel fordi jeg gjerne vil ha ut en enkel datastruktur fra Excels filer, mens Excel tillater oss kompliserte strukturer som ikke uten videre kan puttes inn en en vanlig tabell. Som sagt er vi her avhengig av den enkelte person som lagde Excelfila når det kommer til hvor lett det er for oss å laste den inn. Det å være konsekvent er viktigere enn å etterlikne en “vanlig tabell”. Til dette arbeidet har vi to pakker som har ulike bruksområder:

  • openxlsx: Kilde. Brukes for å skrive Excel-filer.
  • readxl: Kilde. Brukes for å lese Excel-filer.

5.4.1 Skrive til Excel: openxlsx

Pakkas egen introduksjon er en god guide til hvordan dette funker. Sjekk den ut. I korte drag:

library(openxlsx)

# Kjapp lagring av fil
starwars %>% write.xlsx()

# Hvis du vil ha det som en tabell 
starwars %>% write.xlsx(asTable = TRUE)

Her ser vi forøvrig en demonstrasjon av hvordan et argument er valgfritt fordi det er definert en default-verdi. I definisjonen av write.xlsx() står det at argumentet asTable er satt til FALSE. Dermed trenger vi ikke spesifisere dette med mindre vi vil endre den til noe annet, slik vi gjør i siste linje.

Man kan også bygge opp en excelfil mer gradvis

library(openxlsx)

# Start med å lage et workbook-objekt
wb <- createWorkbook()

# Legg til (tomme) arkfaner
addWorksheet(wb, sheetName = "Motor Trend Car Road Tests", gridLines = FALSE)
addWorksheet(wb, sheetName = "Iris", gridLines = FALSE)

# Skriv data til disse arkfanene. `mtcars` og `iris` er datasett som ligger i R.
writeDataTable(wb, sheet = 1, x = mtcars, colNames = TRUE, rowNames = TRUE, 
               tableStyle = "TableStyleLight9")
writeDataTable(wb, sheet = 2, iris, startCol = "K", startRow = 2)

# Lagre fila som excel-fil.
saveWorkbook(wb, here("data", "basics.xlsx"), overwrite = TRUE)

Dette lar deg spesifisere flere av de grafiske elementa i excel-fila, blant annet.

En apropos, dersom du har mestra pipa (%>%): Man kan ikke uten videre pipe sammen de forskjellige kommandoene i denne pakka slik man kan med datasett. Dette gir feil:

# Start med å lage et workbook-objekt
wb <- createWorkbook()

# Legg til (tomme) arkfaner
addWorksheet(wb, sheetName = "Motor Trend Car Road Tests", gridLines = FALSE) %>% 
  addWorksheet(sheetName = "Iris", gridLines = FALSE)
Error: wb must be a Workbok

Her ser vi altså en begrensing ved tidyverse: Når du bruker pakker som ikke er en del av universet deres kan vi måtte gjøre endringer i arbeidsflyten vår.

5.4.2 Lese Excel-filer: readxl

Denne pakka er en del av tidyverse, så her er det bare å stappe pipa.

La oss ta et steg tilbake og tenke på vi må gjøre når vi leser inn Excel-filer. Det er en del konsepter i Excel som ikke finnes eller brukes i R:

  • tomme rader og kolonner som rammer: Det vi ser som en tom celle i Excel er ikke nødvendigvis at den eksisterer. La oss ikke bli for filosofisk her. Pakkas tekst om regnearkgeometri forklarer dette bedre enn jeg kan.
  • farger som indikerer et eller annet om en rad, kolonne eller celle: Dette pleier jeg se bort fra. Viktig informasjon om rader kan heller lagres i tekstformat, i f.eks. en .Rmd/.qmd-fil.
  • funksjoner: i R definerer vi funksjonene og kjører dem en gang. Funksjonene blir liggende som objekter i miljøet/skriptet, mens verdiene de produserer blir putta i datasettet. I Excel blir funksjonen og resultatet liggende i samme celle, oppå hverandre. Når vi laster inn fila er vi bare interessert i sjølve resultatene av funksjonen heller enn funksjonen i seg sjøl.
  • sammenslåtte celler: Disse er spesielt vanskelig. Ifg. denne posten på StackOverflow kan man bruke openxlsx for å lese slike filer. Hvis det gjelder noen få celler, f.eks. i overskrifter, ville jeg vurdert å heller manuelt gå inn og dele dem opp igjen.

Vi kan bruke read_excel() fra readxl til å lese inn Excel-filer. Den lar oss definere en hel del nyttige ting. Her har jeg limt inn funksjonen med alle argumentene, så forkalrer jeg i en kommentar hva de gjør

library(readxl)

read_excel(
  path, # Filsti + navn på fila du skal lese
  sheet = NULL, # Hvilke(t) regneark. Enten navn eller indeks
  range = NULL, # Celler du vil lese. I Excels format, f.eks. "B3:D87"
  col_names = TRUE, # Er første linje kolonnenavn?
  col_types = NULL, # Definer hvilke klasser/typer hver kolonne skal lagres som
  na = "", # Hvis NA er lagra som noe annet enn en tom celle, skriv det her
  trim_ws = TRUE, # Automatisk fjerning av whitespace
  skip = 0, 
  n_max = Inf,
  guess_max = min(1000, n_max), # Se ned
  progress = readxl_progress(),
  .name_repair = "unique" 
)

Om guess_max: Hvis du ikke definerer col_types vil funksjonen gjette på hvilken type data hver kolonne inneholder. På generelt plan er Excel god på dette. Den sliter hvis:

  1. ei kolonne inneholder flere enn en type og
  2. det er mange tomme celler i starten av ei kolonne

Det står mer om gjettinga i Cell and Column Types. Angående punkt 2: dette var et problem i barnehagekapasitetsarbeidet. Her lasta jeg inn noen områder i ei Excel-fil som hadde mange tomme rader før det dukka opp en verdi. I disse tilfellene kunne jeg få feilmelding fordi funksjonen forventa en annen type verdi enn det den fant. Løsninga blei å spesifisere kolonnetypen med col_types.