2  Basics 1: Import og håndtering af data

Dette kapitel introducerer til nogle basics vedrørende import og håndtering af data.

2.1 Import af data

R er bygget til at analysere data - men i udgangspunktet ikke til at skabe rå data (undtagelsen er når vi skaber datasæt ved scraping af web-data). Historikeres data eksisterer typisk i en fysisk form, på papir, og vi har efterhånden en lang række værktøjer, der kan hjælpe os til at omforme dem til tekstdata i søgbare, computervenlige (vær venlig mod din computer, tænk på alt det, den gør for dig) formater. Senest har Transkribus revolutioneret arbejdet med håndskrift. Før du kan importere data i R, skal de med andre ord eksistere i digital form.

R håndterer både tekst og tal. Ofte arbejder vi i en form, hvor data eksisterer i tabelformat. Der findes en række regnearksprogrammer, der kan bruges til at skabe tabeller - f.eks. Google Sheets, Apple Numbers og Microsoft Excel. Hver af disse har sit eget dataformat, men kan også eksportere data i universelle formater - f.eks. det ofte brugte .csv-format.

Når vi importerer data antager R i udgangspunktet, at data findes i vores projektmappe. Det kan dog være smart, at lave en mappe kaldet “data” inde i projektmappen, da denne hurtigt kan blive rodet. For at tilskynde god praksis antager denne bogs kapitler derfor, at vores data ligger i en mappe med titlen data, der altså findes i vores projektmappe.

En .csv-fil med data til brug i dette kapitel kan hentes her. Den kan du downloade og lægge ind i din data-mappe.

I R kan vi importere .csv med funktionen read_csv2 (der er også en read_csv uden 2-tallet, men den virker ikke til det format, denne fil benytter).

library(tidyverse)

df <- read_csv2("data/Slave_1741_1800_clean.csv")

head(df)
# A tibble: 6 × 16
  Navn  SLAVE…¹ Indkommen  Udkom…² Afslu…³ Fejls…⁴ Baggr…⁵ Ærlig…⁶ Relig…⁷ Alder
  <chr>   <dbl> <date>     <chr>   <chr>   <chr>   <chr>   <chr>   <chr>   <dbl>
1 Mort…    1504 1741-05-17 1741-1… Dead    No fai… Army    Uærlig  Luther…    40
2 Hove…    1505 1741-06-21 1741-0… Releas… No fai… Navy    Ærlig   Luther…    53
3 Pede…    1506 1741-06-21 1741-0… Releas… No fai… Navy    Ærlig   Luther…    26
4 Knud…    1507 1741-06-21 1741-0… Releas… No fai… Navy    Ærlig   Luther…    35
5 Lars…    1508 1742-06-12 1742-0… Releas… No fai… Civili… Ærlig   Luther…    54
6 Mads…    1509 1742-06-12 1742-0… Releas… No fai… Civili… Ærlig   Luther…    63
# … with 6 more variables: Ægteskab <chr>, Kropsstraf_kat <chr>, Livstid <chr>,
#   Tyv <chr>, Desertør <chr>, Brændemærket <chr>, and abbreviated variable
#   names ¹​SLAVE_ID, ²​Udkommen, ³​Afslutning, ⁴​Fejlslagne_flugtforsøg,
#   ⁵​Baggrund, ⁶​Ærlighed, ⁷​Religion

R kan ved hjælp af pakken readxl også importere .xlsx-filer, der er det format, der bruges af Microsoft Excel. En version af vores data i dette format, kan downloades her:

Lad os prøve at importere filen.

library(readxl)
df <- read_excel("data/Slave_1741_1800_clean.xlsx")

head(df)
# A tibble: 6 × 16
  Navn     SLAVE…¹ Indko…² Udkom…³ Afslu…⁴ Fejls…⁵ Baggr…⁶ Ærlig…⁷ Relig…⁸ Alder
  <chr>      <dbl> <chr>   <chr>   <chr>   <chr>   <chr>   <chr>   <chr>   <dbl>
1 Morten …    1504 1741-0… 1741-1… Dead    No fai… Army    Uærlig  Luther…    40
2 Hovel N…    1505 1741-0… 1741-0… Releas… No fai… Navy    Ærlig   Luther…    53
3 Peder L…    1506 1741-0… 1741-0… Releas… No fai… Navy    Ærlig   Luther…    26
4 Knud Ol…    1507 1741-0… 1741-0… Releas… No fai… Navy    Ærlig   Luther…    35
5 Lars La…    1508 1742-0… 1742-0… Releas… No fai… Civili… Ærlig   Luther…    54
6 Mads Je…    1509 1742-0… 1742-0… Releas… No fai… Civili… Ærlig   Luther…    63
# … with 6 more variables: Ægteskab <chr>, Kropsstraf_kat <chr>, Livstid <chr>,
#   Tyv <chr>, Desertør <chr>, Brændemærket <chr>, and abbreviated variable
#   names ¹​SLAVE_ID, ²​Indkommen, ³​Udkommen, ⁴​Afslutning,
#   ⁵​Fejlslagne_flugtforsøg, ⁶​Baggrund, ⁷​Ærlighed, ⁸​Religion

Som det fremgår er resultatet det samme, uanset om vi bruger read_excel eller read_csv2. Det vi får kaldes en “dataframe” og er den mest almindelige form for tabel i R.

Når vi importerer, er det også muligt at bruge R-studios indbyggede import-funktion. Den virker essentielt ved at konstruere et tilsvarende kodestykke for os, men gør det muligt for os at klikke os frem, hvis den ligger gemt i en afkrog på harddisken.

<-

I kodestykkerne ovenfor brugte jeg en mærkværdig pil <-. Du kan tænke denne pil som en genstand, der definerer noget. Det den definerer står på venstre side (det som pilen peger på). Dette defineres så af det, der står på højre side af pilen. Ovenfor definerer vi derfor et objekt: df (for “data frame”, et hyppigt anvendt tabelformat i R) som resultatet af funktionen read_excel. Du laver pilen med genvejen options plus - (Mac) eller alt plus - (Windows).

De data vi bruger i dette kapitel stammer fra Københavns såkaldte “Stokhusslaveri”, der var et fængsel for mænd. Det åbnede i 1741 og vores data løber til 1800. Dataene bygger på fængslets mandtalsprotokoller, men er “rensede” - forstået på den måde, at de er standardiseret. De fremstår derfor ikke længere som i de oprindelige kilder.

2.2 Grundverbum # 1 - select

I dette kapitel vil jeg introducere nogle verber, du kommer til at bruge igen og igen, når du arbejder i R. Det er verber, der gør os i stand til at håndtere data.

Det første verbum er select. Dets funktion er simpel: Select udvælger enkelte kolonner og fravælger resten. Vi bruger typisk select, når vi i et datasæt med mange kolonner blot ønsker at arbejde med nogle få af dem.

library(tidyverse)

df <- df %>% 
  select(Indkommen, Udkommen, Livstid, Baggrund, Alder)

head(df)
# A tibble: 6 × 5
  Indkommen  Udkommen   Livstid               Baggrund Alder
  <chr>      <chr>      <chr>                 <chr>    <dbl>
1 1741-05-17 1741-10-07 Lifetime sentence     Army        40
2 1741-06-21 1741-07-21 Set duration sentence Navy        53
3 1741-06-21 1741-07-21 Set duration sentence Navy        26
4 1741-06-21 1741-07-21 Set duration sentence Navy        35
5 1742-06-12 1742-08-27 Set duration sentence Civilian    54
6 1742-06-12 1742-08-27 Set duration sentence Civilian    63

Her går vi fra en tabel med 16 kolonner til et datasæt, der blot har de kolonner vi har udvalgt.

%>%

Ovenfor brugte jeg yderligere et mærkeligt sæt af symboler (bare rolig, det var sidste gang for nu). Dette mærkværdige udtryk kaldes for “piben”. Det er en del af tidyverse-pakken. Det sender data videre til de efterfølgende funktioner. Uden piben ville vi være tvunget til at placere parenteser inde i parenteser. Den gør det derfor meget mere læseligt. Man får den frem med genvejen m plus shift plus command (Mac) eller m plus shift plus ctrl (Windows).

2.3 Grundverbum # 2 - mutate

Det næste verbum er mutate. Mutate-funktionen skaber en eller flere nye kolonne, typisk på baggrund af data i allerede eksisterende kolonner. Den gør det oftest ved brug af yderligere fuktioner.

library(lubridate)

df <- df %>% 
  mutate(Ankomstdato = ymd(Indkommen),
         Slutdato = ymd(Udkommen),
         Varighed = Slutdato - Ankomstdato)
Warning: There was 1 warning in `mutate()`.
ℹ In argument: `Slutdato = ymd(Udkommen)`.
Caused by warning:
!  5 failed to parse.

Koden ovenfor skaber 3 nye kolonner i vores tabel. Alle tre skabes med udgangspunkt i data, der allerede eksisterer i kolonner. Ankomstdato skaber en kolonne med en dato af den allerede eksisterende kolonne Indkommen, hvor datoen står som en tekststreng i formatet år-måned-dag. Den skaber denne datokolonne med funktionen ymd (der står year-month-date, og har søskende-funktioner med samme bogstaver i andre kombinationer). Slutdato skabes på samme måde på baggrund af tekststrengen i kolonnen Udkommen. Til sidst udregnes en Varighed ved at trække den ene dato fra den anden. Læg mærke til advarselsbeskeden: i 75 tilfælde manglede der en slutdato, fordi 1700-tals protokoller sjældent er fuldstændigt systematiske og ikke altid helt lette at tyde. Funktionen ymd( ) (der laver datasættets angivelser af dato om til noget R forstår som en faktisk dato) kan i sagens natur ikke lave en NA-værdi om til en dato. Fordi Varighed er udregnet med udgangspunkt i slutdatoen arver denne kolonne den samme manglende data.

Mutate er vores go-to værktøj, når vi skaber nye kolonner i tabeller. Kigger vi igen på vores tabel, vil vi se, at den har den samme struktur, men blot har fået tre nye kolonner.

head(df)
# A tibble: 6 × 8
  Indkommen  Udkommen   Livstid      Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³
  <chr>      <chr>      <chr>        <chr>   <dbl> <date>     <date>     <drtn> 
1 1741-05-17 1741-10-07 Lifetime se… Army       40 1741-05-17 1741-10-07 143 da…
2 1741-06-21 1741-07-21 Set duratio… Navy       53 1741-06-21 1741-07-21  30 da…
3 1741-06-21 1741-07-21 Set duratio… Navy       26 1741-06-21 1741-07-21  30 da…
4 1741-06-21 1741-07-21 Set duratio… Navy       35 1741-06-21 1741-07-21  30 da…
5 1742-06-12 1742-08-27 Set duratio… Civili…    54 1742-06-12 1742-08-27  76 da…
6 1742-06-12 1742-08-27 Set duratio… Civili…    63 1742-06-12 1742-08-27  76 da…
# … with abbreviated variable names ¹​Baggrund, ²​Ankomstdato, ³​Varighed

2.4 Grundverber # 3 og 4 - group_by og summarise

Når vi arbejder med dataene har vi igen og igen brug for at gruppere dem efter værdier i dataene. Dette gør vi vha. af group_by-funktionen. Denne kombineres stort set altid med andre funktioner, for vi grupperer som regel med det formål at bearbejde vores data ifht. enkelte grupper. Et simpelt eksempel kunne være, at vi ønskede at gruppere vores data efter en simpel kategorisk variabel og tælle, hvor mange observationer, der er for hver kategori.

df_grouped <- df %>% 
  group_by(Livstid) %>% 
  summarise(antal = n())

df_grouped
# A tibble: 3 × 2
  Livstid               antal
  <chr>                 <int>
1 Lifetime sentence      1342
2 Set duration sentence  1843
3 <NA>                      5

Her grupperes vores data efter kategorierne i kolonnen Livstid. Næste bid af kode skaber så en ny kolonne, på samme måde som mutate gjorde det. Her udregner vi et antal ved at bruge en funktion, der er designet netop til dette formål, og som sjovt nok netop hedder n.

Summarise funktionen betyder, at vi beholder den gruppering, vi har skabt. Dermed får datasættet en ny struktur med et antal af rækker, der svarer til antallet af kategorier i den kolonne, vi har grupperet efter. Smart. Hvis vi havde brugt mutate, var vores gruppering derimod blevet ophævet og n havde optrådt som en kolonne i den oprindelige datastruktur. Til sammenligning:

df %>% 
  group_by(Livstid) %>% 
  mutate(antal = n())
# A tibble: 3,190 × 9
# Groups:   Livstid [3]
   Indkommen  Udkommen Livstid Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³ antal
   <chr>      <chr>    <chr>   <chr>   <dbl> <date>     <date>     <drtn>  <int>
 1 1741-05-17 1741-10… Lifeti… Army       40 1741-05-17 1741-10-07  143 d…  1342
 2 1741-06-21 1741-07… Set du… Navy       53 1741-06-21 1741-07-21   30 d…  1843
 3 1741-06-21 1741-07… Set du… Navy       26 1741-06-21 1741-07-21   30 d…  1843
 4 1741-06-21 1741-07… Set du… Navy       35 1741-06-21 1741-07-21   30 d…  1843
 5 1742-06-12 1742-08… Set du… Civili…    54 1742-06-12 1742-08-27   76 d…  1843
 6 1742-06-12 1742-08… Set du… Civili…    63 1742-06-12 1742-08-27   76 d…  1843
 7 1742-06-12 1742-09… Set du… Civili…    49 1742-06-12 1742-09-11   91 d…  1843
 8 1742-06-12 1742-12… Set du… Civili…    60 1742-06-12 1742-12-10  181 d…  1843
 9 1742-07-07 1742-10… Set du… Army       40 1742-07-07 1742-10-07   92 d…  1843
10 1742-07-11 1747-09… Lifeti… Army       30 1742-07-11 1747-09-01 1878 d…  1342
# … with 3,180 more rows, and abbreviated variable names ¹​Baggrund,
#   ²​Ankomstdato, ³​Varighed

Prøv at klikke dig helt ud til den yderste kolonne. Her finder du n. Men udregningen i kolonnen svarer til den grupperede værdi.

Summarise og mutate virker med andre ord ved at skabe en kolonne. Forskellen er, at summarise resulterer i en tabel, med en række for hver gruppe, imens mutate blot skaber den nye kolonne i den allerede eksisterende tabel.

Øvelse: Ovenfor skabte vi en ny kolonne. Men hvis vi igen kigger på dataene er den væk. Kan du regne ud, hvorfor den er forsvundet?

head(df)
# A tibble: 6 × 8
  Indkommen  Udkommen   Livstid      Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³
  <chr>      <chr>      <chr>        <chr>   <dbl> <date>     <date>     <drtn> 
1 1741-05-17 1741-10-07 Lifetime se… Army       40 1741-05-17 1741-10-07 143 da…
2 1741-06-21 1741-07-21 Set duratio… Navy       53 1741-06-21 1741-07-21  30 da…
3 1741-06-21 1741-07-21 Set duratio… Navy       26 1741-06-21 1741-07-21  30 da…
4 1741-06-21 1741-07-21 Set duratio… Navy       35 1741-06-21 1741-07-21  30 da…
5 1742-06-12 1742-08-27 Set duratio… Civili…    54 1742-06-12 1742-08-27  76 da…
6 1742-06-12 1742-08-27 Set duratio… Civili…    63 1742-06-12 1742-08-27  76 da…
# … with abbreviated variable names ¹​Baggrund, ²​Ankomstdato, ³​Varighed
count

Ovenfor brugte vi en kombination af group_by(), summarise() og n() til at lave en optælling over antal observationer pr. gruppe. Er en simpel optælling med n() vores eneste mål med en gruppering, findes der en genvej i form af funktionen count(). Vi kunne derfor have fået samme resultat sådan her:

df %>% 
  count(Livstid)
# A tibble: 3 × 2
  Livstid                   n
  <chr>                 <int>
1 Lifetime sentence      1342
2 Set duration sentence  1843
3 <NA>                      5

Når jeg således viser en mere omstændig vej til samme resultat, er det for at vise et workflow, du kommer til at bruge igen og igen. Ofte vil vi nemlig ikke kun lave en simpel optælling, men udregne flere variable på en og samme tid.

2.5 Grundverber # 5 - na.omit

Vi er allerede flere gange stødt på det problem, at der er manglende værdier - NA-værdier - i vores data. Laver vi eksempelvis et kald med summarise som vi har gjort ovenfor optræder disse værdier som en række i vores sammenfattede tabel. Vi kan imidlertid skabe samme tabel, men sortere disse værdier fra. Det gør vi ved først at udvælge kolonnerne og derefter bruge funktinen na.omit.

df %>% 
  select(Livstid) %>% 
  na.omit() %>% 
  group_by(Livstid) %>% 
  summarise(antal = n())
# A tibble: 2 × 2
  Livstid               antal
  <chr>                 <int>
1 Lifetime sentence      1342
2 Set duration sentence  1843

2.6 Grundverbum # 6 - filter

Vi har lært at udvælge kolonner baseret på deres navn. Men hvad hvis jeg gerne vil udvælge rækker/observationer baseret på hvad de indeholder? Her bruger vi filter.

df_soldater <- df %>%
  filter(Baggrund == "Army")

head(df_soldater)
# A tibble: 6 × 8
  Indkommen  Udkommen   Livstid      Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³
  <chr>      <chr>      <chr>        <chr>   <dbl> <date>     <date>     <drtn> 
1 1741-05-17 1741-10-07 Lifetime se… Army       40 1741-05-17 1741-10-07  143 d…
2 1742-07-07 1742-10-07 Set duratio… Army       40 1742-07-07 1742-10-07   92 d…
3 1742-07-11 1747-09-01 Lifetime se… Army       30 1742-07-11 1747-09-01 1878 d…
4 1742-08-11 1748-08-27 Lifetime se… Army       41 1742-08-11 1748-08-27 2208 d…
5 1742-08-18 1742-11-18 Set duratio… Army       32 1742-08-18 1742-11-18   92 d…
6 1742-09-03 1745-07-07 Lifetime se… Army       36 1742-09-03 1745-07-07 1038 d…
# … with abbreviated variable names ¹​Baggrund, ²​Ankomstdato, ³​Varighed

Vi kan i et og samme kald filtrere på flere parametre samtidigt:

df_soldater_med_livstid <- df %>% 
  filter(Baggrund == "Army" & Livstid == "Lifetime sentence")

head(df_soldater_med_livstid)
# A tibble: 6 × 8
  Indkommen  Udkommen   Livstid      Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³
  <chr>      <chr>      <chr>        <chr>   <dbl> <date>     <date>     <drtn> 
1 1741-05-17 1741-10-07 Lifetime se… Army       40 1741-05-17 1741-10-07  143 d…
2 1742-07-11 1747-09-01 Lifetime se… Army       30 1742-07-11 1747-09-01 1878 d…
3 1742-08-11 1748-08-27 Lifetime se… Army       41 1742-08-11 1748-08-27 2208 d…
4 1742-09-03 1745-07-07 Lifetime se… Army       36 1742-09-03 1745-07-07 1038 d…
5 1742-09-03 1743-03-29 Lifetime se… Army       28 1742-09-03 1743-03-29  207 d…
6 1742-09-04 1754-08-16 Lifetime se… Army       30 1742-09-04 1754-08-16 4364 d…
# … with abbreviated variable names ¹​Baggrund, ²​Ankomstdato, ³​Varighed

Her får vi alle hits, der matcher både første og andet kriterium. Hvis jeg i stedet vil filtrere på en måde, så jeg blot kræver et match på et af kriterierne, er det også muligt:

df_soldater_og_søfolk <- df %>% 
  filter(Baggrund == "Army" | Baggrund == "Navy")

head(df_soldater_og_søfolk)
# A tibble: 6 × 8
  Indkommen  Udkommen   Livstid      Baggr…¹ Alder Ankomstd…² Slutdato   Varig…³
  <chr>      <chr>      <chr>        <chr>   <dbl> <date>     <date>     <drtn> 
1 1741-05-17 1741-10-07 Lifetime se… Army       40 1741-05-17 1741-10-07  143 d…
2 1741-06-21 1741-07-21 Set duratio… Navy       53 1741-06-21 1741-07-21   30 d…
3 1741-06-21 1741-07-21 Set duratio… Navy       26 1741-06-21 1741-07-21   30 d…
4 1741-06-21 1741-07-21 Set duratio… Navy       35 1741-06-21 1741-07-21   30 d…
5 1742-07-07 1742-10-07 Set duratio… Army       40 1742-07-07 1742-10-07   92 d…
6 1742-07-11 1747-09-01 Lifetime se… Army       30 1742-07-11 1747-09-01 1878 d…
# … with abbreviated variable names ¹​Baggrund, ²​Ankomstdato, ³​Varighed

2.7