Sesjon 10: Mer tegning: ggplot2

R-workshop 10. oktober, OsloMet

Nå skal vi tegne mer, men denne gangen skal vi bli kjent med ggplot2 pakken. R hadde allerede etablert seg som en god programvare for visuell datapresentsajon, men denne pakken har bidratt til å gjøre R uovertruffen ved å tilgjengligjøre det grafiske aspektet.

ggplot2 fungerer godt i samband med datatilrettelegging med dplyr. Den har – irriterende nok – sine egne piper. Jevnt over, krever ggplot2 flere argumenter og funksjoner for å gi enkle plot, men mulighetene for tilpasninger er også veldig store. Dermed er ggplot-grafikkene mine produsert med relativt mange kodelinjer. Jeg bruker dem i større grad når jeg skal presentere for andre, mens jeg tyr til kjappere løsninger når jeg bare skal ta en “titt”.

Arbeidsflyten min i ggplot2 er som følger:

  • jeg etablerer et eget lite datasett med informasjonen jeg ønsker å bruke i grafikken. Det betyr at jeg må tenke gjennom hvilken informasjon jeg ønsker og trenger for visualiseringen. Som oftest jobber jeg iterativt. Det vil si at jeg starter med et enkelt utgangspunkt, så går jeg heller tilbake og legger til nye elementer etter hvert.
  • pipen leses kronologisk: Jeg etablerer først et ark, så legger jeg på elementer
  • de visuelle tilpassningene (“theme”) kan baseres på en mal. Da er alt presisert. Deretter kan kan jeg “tweake” det jeg ønsker.

Google er som vanlig din venn.

library(ggplot2); library(dplyr)

Vi laster inn ESS-dataene fra forrige sesjon (med mindre vi fortsatt har dem).

load(
  url("https://siljehermansen.github.io/teaching/oslomet/kap7.rda")
)
df <- kap7

Uforsk hele datasettet

Jeg sa nettopp at jeg etablerer et eget “plotdatasett” for ggplot. Før vi gjør det, vil jeg noen ganger plotte selve analysedataene mine likevel.

Spredningsdiagram

Hovedfunksjonen ggplot() etablerer plottet, men ingen plotelementer. Om du skal bruke de samme dataene for alle de grafiske elementene, så kan du presisere dataobjektet allerede i denne funksjonen. Når du gjør det, presiserer du ikke datasettet ditt flere ganger i pipen, så du sparer tid. Fra da av, kan du referere rett til variablene som ligger i det.

Du binder alle elementer sammen med en pipe ved hjelp av “+”. Her er det fort å glemme seg, slik at man heller bruker %>%-pipen fra tidyverse. Du er ikke den eneste som vil gjøre den feilen, så meldingen du får er overraskende intuitiv: Did you use %>% instead of +?.

ggplot(data = df)
Den første funksjonen etablerer et tomt tegneark.

Den første funksjonen etablerer et tomt tegneark.

Hovedelementene i grafikken legges inn ved hjelp av en serie med geom_...() funksjoner. De presiserer “geometrien”, altså hva som skal legges inn (informasjonen fra variablene) og hvor dette skal ligge (koordinatene) .

Inni hver av dem finner du estetikkfunksjonen, aes(). Der oppgir du x/y-koordinatene som kommer fra variablene dine.

Her lager jeg et spredningsdiagram for sammenhengen mellom innvandringsskepsis og inntekt. Det betyr at jeg ønsker å tegne punkter (geom_point()).

ggplot(df) +
  geom_point(aes(y = Skepsis,
                 x = Inntekt))
Et enkelt spredningsdiagram gir inntrykk av det bivariate forholdet mellom innvandringsskepsis og inntekt.

Et enkelt spredningsdiagram gir inntrykk av det bivariate forholdet mellom innvandringsskepsis og inntekt.

Inntektsvariabelen består kun av heltall. Dermed overskriver punktene hverandre, og vi får en dårlig idé om hvor mange enheter som finnes innen de ulike verdikombinasjonene. Vi kan løse dette på to måter: Ved å gjøre punktene delvis gjennomsiktige, eller ved å flytte dem litt fra hverandre.

Gjennomsiktig farge Jeg kan oppgi at jeg ønsker gjennomsiktige farger. Dette gjør jeg med argumentet alpha=. Verdien jeg oppgir rangerer fra 0 (helt gjennomsiktig/usynlig) til 1 (ikke-transparent).

ggplot(df) +
  geom_point(aes(y = Skepsis,
                 x = Inntekt),
             #Gjennomsiktighet: for alle observasjoner
             alpha = 0.2)
Ved å bruke gjennomsiktig farge, kan jeg vise hvor de fleste observasjonene befinner seg i fordelingen.

Ved å bruke gjennomsiktig farge, kan jeg vise hvor de fleste observasjonene befinner seg i fordelingen.

Notér at når vi skal tilrettelegge punktene uten at tilretteleggingen er avhengig av informasjon fra dataene, så foregår dette utenfor aes() men innenfor geom_...() funksjonen.

Riste data Et annet alternative er å “riste” datapunktene litt. På engelsk kaller vi dette “jitter”. Da legger R til en tilfeldig variasjon i punktenes koordinater. Selve datapunktene blir mer upresise, men poenget her er ikke en presis analyse, men å presentere sammenhenger i dataene slik at det menneskelige øyet får en intuitiv forståelse av hva som foregår.

ggplot(df) +
  geom_jitter(aes(y = Skepsis,
                 x = Inntekt),
              #Riste data horisontalt, men ikke vertikalt
              height = 0, width = 0.3)
Ved å 'riste' datapunktene, kan jeg også gi et bedre inntrykk av hvor observasjonene befinner seg.

Ved å ‘riste’ datapunktene, kan jeg også gi et bedre inntrykk av hvor observasjonene befinner seg.

Tilleggsargumentene width= og height= presiserer hvor mye punktene skal ristes horisontalt og vertikalt, respektivt. Her sier jeg at jeg ønsker presise data langs y-aksen (“i høyden”), men lar punktene variere tilfeldig med 0.3 enheter langs x-aksen (“i bredden”).

Trivariate grafikker: Gruppér dataene.

Jeg kan enkelt legge til flere grupperinger. Her fargelegger jeg punktene etter verdien på en tredje variabel: Den variabelen kan være kontinuerlig eller kategorisk.

Kontinuerlig gruppering “Subjektiv inntekt” rapporterer hvor rik respondenten føler seg i fire kategorier fra 0 (“lav tilfredshet”) til 3 (“høy tilfredshet”).

ggplot(df) +
  geom_point(aes(y = Skepsis,
                 x = Inntekt,
                 #Gruppering
                 color = SubjektivInntekt))

Nå får jeg en “varmeskala” for respondentens oppfattelse av egen inntekt. ggplot gir oss også automatisk en tegnforklaring.

Her ser vi at de fleste respondentene som rapporterer lav tilfredshet (mørk farge) befinner seg til venstre i plottet. De har lav (objektiv) inntekt.

Kategorisk gruppering Skal jeg ha en kategorisk gruppering, må jeg fôre funksjonen inn med en kategorisk variabel (“character” eller “factor”). Det beste er å gjøre dette i plotdataene, ikke i ggplot()-funksjonen. Akkurat her, fraviker jeg fra denne regelen med (as.factor())

ggplot(df) +
  geom_point(aes(y = Skepsis,
                 x = Inntekt,
                 #Gruppering
                 color = as.factor(SubjektivInntekt)))

Én løsning forhindrer ikke en annen. Jeg kan selvsagt kombinere alle disse løsningene fram til jeg får et plot som forteller historien jeg ønsker å formidle.

ggplot(df) +
  geom_jitter(aes(y = Skepsis,
                 x = Inntekt,
                 #Kontinuerlig tredje/modererende variabel
                 color = SubjektivInntekt),
              #Riste data horisontalt og vertikalt
              height = 0.1, width = 0.4,
              #Gjennomsiktig farge
              alpha = 0.7)

Bivariate regresjonslinjer

Jeg kan også vise sammenhengen mellom to variabler ved hjelp av regresjonslinjer. ggplot rapporterer ikke regresjonskoeffisientene, men illustrerer i stedet regresjonslinjen og usikkerheten rundt den. Dette er spesielt relevant hvis minst en av variablene (x-variabelen) er kontinuerlig.

Nå kan vi velge mellom lokal regresjon og vanlig lineær regresjon. Hvilken vi velger, avhenger av hva vi vil oppnå.

Lokal regresjon

Lokal regresjon er – i mine øyne – dronningen av alle ggplot-funksjoner. Jeg bruker den hele tiden når jeg utforsker dataene mine. Den tegner en “myk” linje (lokalt, glidene gjennomsnitt over de ulike x-verdiene). Helt perfekt for tidstrender eller for å utforske ikke-lineære sammenhenger, for eksempel.

Funksjonen som gir oss bivariate regresjonslinjer er geom_smooth(). Default-instillingen er lokal regresjon.

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Alder))
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'
Lokal regresjon: En enkelt måte å presentere empiriske sammenhenger mellom to variabler.

Lokal regresjon: En enkelt måte å presentere empiriske sammenhenger mellom to variabler.

Her illustrerer jeg sammenhengen mellom alder og innvandringsskepsis. Figuren demonstrerer den største fordelen med lokal regresjon: Det finnes ingen lineær sammenheng mellom alder og innvandringsskepsis. I stedet ser det ut til at yngre og eldre respondenter er mer skeptiske.

R tilpasser “vinduet” automatisk for oss, men vi kan velge størrelsen selv. Da anvender vi tilleggsargumentet span =. Hvor stort spenn av variabelen skal brukes for det lokale gjennomsnittet? Lave verdier gir en mer krøllete strek, høye verdier gir mer generalisering.

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Alder),
              span = 20)
## `geom_smooth()` using method = 'gam' and formula 'y ~ s(x, bs = "cs")'
Lokal regresjon: En enkelt måte å presentere bivariat sammenhenger.

Lokal regresjon: En enkelt måte å presentere bivariat sammenhenger.

Gruppert: Er sammenhengen lik innenfor ulike kategorier?

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Inntekt,
                  color = as.factor(SubjektivInntekt)))
## `geom_smooth()` using method = 'loess' and formula 'y ~ x'
Lokal regresjon: Dronningen av bivariate plot?

Lokal regresjon: Dronningen av bivariate plot?

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Inntekt,
                  color = as.factor(Kvinne)))
## `geom_smooth()` using method = 'loess' and formula 'y ~ x'

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Inntekt,
                  color = as.factor(Kvinne)))
## `geom_smooth()` using method = 'loess' and formula 'y ~ x'

Vil du bytte ut spaghettien med rette linjer? Be om en lineær modell.

ggplot(df) +
  geom_smooth(aes(y = Skepsis,
                  x = Inntekt,
                  color = as.factor(SubjektivInntekt)),
              method = "lm")
## `geom_smooth()` using formula 'y ~ x'

Her ser vi klart at sammenhengen mellom innvandringsskepsis og inntekt først og fremst er tilstede for respondenter som selv føler at de mangler penger (kategoriene 0 og 1).

Boksplot

ggplot(df %>%
         filter(!is.na(SubjektivInntekt))) +
  geom_boxplot(aes(y = Skepsis,
                   x = as.factor(SubjektivInntekt),
                   fill = SubjektivInntekt),
                   notch = T)
## notch went outside hinges. Try setting notch=FALSE.
Boksplot gir et inntrykk av fordelingen internt i grupper av data

Boksplot gir et inntrykk av fordelingen internt i grupper av data

Fiolinplot

ggplot(df %>% 
         filter(!is.na(SubjektivInntekt))) +
  geom_violin(aes(y = Skepsis,
                  x = as.factor(SubjektivInntekt)))

Tetthetsgrafikker

library(ggridges)
ggplot(df %>% 
         filter(!is.na(SubjektivInntekt))) +
  geom_density_ridges(aes(x = Skepsis,
                          y = as.factor(SubjektivInntekt)),
                      fill = "white") +
  coord_flip()
## Picking joint bandwidth of 0.558

Søylediagram/histogram

Søylediagrammet og histogrammet tar minst ett argument: Vi oppgir hvorvidt høyden på søylene skal defineres langs y-aksen (vertikale søyler) ller x-aksen (horisontale søyler). ggplot teller antall observasjoner for oss.

ggplot(df) +
  geom_bar(aes(x = Skepsis))

Histogrammet følger samme regler.

ggplot(df) +
  geom_histogram(aes(x = Skepsis))
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Histogrammer viser den univariate fordelingen til en kontinuerlig variabel.

Histogrammer viser den univariate fordelingen til en kontinuerlig variabel.

Vi kan lage et tetthetsdiagram med samme logikk.

ggplot(df) +
  geom_density(aes(x = Skepsis))

ggplot(df %>% filter(!is.na(SubjektivInntekt))) +
  geom_density(aes(x = Skepsis,
                   color = as.factor(Utdanning),
                   fill = as.factor(Utdanning)),
               position = "stack",
               alpha = 0.5)

library(ggridges)
ggplot(df %>% filter(!is.na(SubjektivInntekt))) +
  geom_density_ridges(aes(x = Skepsis,
                          y = as.factor(Utdanning),
                          color = as.factor(Utdanning),
                          fill = as.factor(Utdanning)),
                      alpha = 0.7) +
  theme_minimal()
## Picking joint bandwidth of 0.566

Flere plot:

ggplot(df %>% 
         filter(!is.na(SubjektivInntekt))) +
  geom_smooth(aes(x = Inntekt,
                  y = Skepsis,
                  group = as.factor(SubjektivInntekt)),
              method = "lm") +
  facet_wrap(facets = vars(SubjektivInntekt))
## `geom_smooth()` using formula 'y ~ x'

Lag et grafisk datasett først

La oss følge en alternativ arbeidsflyt og etablere et eget lite datasett for plotting. Her er jeg interessert i innvandringsskepsis blant respondenter med ulik oppfatning av egen inntekt.

dfp <- df %>%
  #Filtrer ut NA
  filter(!is.na(SubjektivInntekt)) %>%
  #Grupper
  group_by(SubjektivInntekt) %>%
  #Gruppegjennomsnitt
  summarize(Innvandringsskepsis = mean(Skepsis, na.rm = T))

#Lag en ny kategorisk variabel med selve survey spørsmålet 
dfp$`Hvor godt lever du på inntekten din?` <- dfp$SubjektivInntekt %>% 
  as.factor
#Omdefiner kategoriene til intuitive svar
levels(dfp$`Hvor godt lever du på inntekten din?`) <- c("Svært vanskelig",
                                                        "Vanskelig",
                                                        "Endene møtes", 
                                                        "Komfortabelt")

Her kan du be

ggplot(dfp) +
  #Klassisk definisjon av både x og y koordinater
  geom_bar(aes(y = Innvandringsskepsis,
               x = `Hvor godt lever du på inntekten din?`),
           #Bruker tallene du oppgir som søylehøyde uten å telle selv.
           stat = "identity")

dfp <- df %>%
  #Filtrer ut NA
  filter(!is.na(SubjektivInntekt) & 
           !is.na(Prekaritet)) %>%
  #Grupper
  group_by(Prekaritet, SubjektivInntekt) %>%
  #Gruppegjennomsnitt
  summarize(Innvandringsskepsis = mean(Skepsis, na.rm = T)) 
## `summarise()` has grouped output by 'Prekaritet'. You can override using the
## `.groups` argument.
#Omkod to modrererende variabler
#Lag en ny kategorisk variabel med selve survey spørsmålet 
dfp$`Hvor godt lever du på inntekten din?` <- dfp$SubjektivInntekt %>% 
  as.factor
#Omdefiner kategoriene til intuitive svar
levels(dfp$`Hvor godt lever du på inntekten din?`) <- c("Svært vanskelig",
                                                        "Vanskelig",
                                                        "Endene møtes", 
                                                        "Komfortabelt")
dfp$Arbeidssituasjon = ifelse(dfp$Prekaritet == 1,
                        "Præker", "Trygg")
dfp

Her kan du be

p <- 
  ggplot(dfp) +
  #Klassisk definisjon av både x og y koordinater
  geom_bar(aes(y = Innvandringsskepsis,
               x = `Hvor godt lever du på inntekten din?`,
               #Fyll ut med en farge ut fra grupperingen
               fill = Arbeidssituasjon),
           #Legg soylene ved siden av hverandre
           position = "dodge",
           #Bruker tallene du oppgir som søylehøyde uten å telle selv.
           stat = "identity")

Merk deg hvordan jeg har tatt vare på plottet mitt ved å lagre det i et objekt: p <- ggplot(). For å se plottet ber jeg om det.

p
Søylediagram laget av et eget datasett hvor jeg har aggregert og tilrettelagt informasjonen.

Søylediagram laget av et eget datasett hvor jeg har aggregert og tilrettelagt informasjonen.

Det er nyttig å lagre objektet på denne måten fordi jeg i det videre skal endre på R-objektet mitt. Det kan jeg gjøre via piper.

Tilrettelegging.

Når du har lagt inn de grafiske elementene med dataene dine, kan du bruke uendelig tid på å finpusse resten av plottet.

Fortell R hvilket språk du snakker (og hvor du er)

R er ikke skrevet av nordmenn, så de norske bokstavene kan av og til skape problemer. Vi må forholde oss aktivt til tegnsettingsvalg i to settinger: Når vi henter inn data og når vi lagrer data; inkludert i grafikker.

Datamaskiner leser og gir fra seg informasjon som en serie med 0-er og 1-ere. For å lese og skrive tekst, opererer vi med tegnsetting (“encoding”); en oversettelse av binære koder til alfabetet vi kjenner. Tradisjonelt har hvert land og språk hatt hvert sitt tegnsettingssystem. Det er tungvint, og idag følger det meste av tekst som utveksles på internett, “utf-8”-tegnsetting. Der finnes de aller fleste tegn som vi har lært å lese. Sys.setlocale() forteller R hvor du er i verden: Her forteller jeg at jeg ønsker norsk språk med utf-8 tegn.

Sys.setlocale(category = "LC_ALL",
              locale = "nb_NO.UTF-8")

Titler og aksenavn

p <- 
  p +
  #Legg til tittel og undertittel
  ggtitle(label = "Sammenhengen mellom innvandringsskepsis og opplevd okonomisk situasjon",
          subtitle = "Data fra ESS (2014)") +
  #Navnet på x-aksen
  xlab("Opplevd inntekt (lav-hoy)") +
  #Hva er grenseverdiene for y-aksen?
  ylim(c(0,6))

p

Definer farger

Du kan velge farger selv. Dette gjøres på mange måter. Om du har linket data med fargevalg, kan det være lurt å definere en “palett”.

Selve teksten (“Prekær” og “Trykk”) er en del av dataene jeg oppga, så disse befinner seg i variabelen “dfp$Arbeidssituasjon”. Jeg kan endre dem i ggplot hvis jeg vil. Dette gjør jeg når jeg definerer fargene.

p <- 
  p +  
  #Definer fargene
  scale_color_manual(
    #Hva er fargene?
    values = c("purple", "magenta"),
    #Fargelegg hele søylen
    aesthetics = "fill",
    #Hva er tittelen for tegnforklaringen?
    name = "Tilknytning til arbeidslivet",
    #Hva er kategorinavnene?
    labels = c("Midlertidig/uten arbeid",
               "Fast arbeid")
    ) 
p

Bakgrunnen

Du kan definere ulike maler for de estetike delene av plottet med theme_...().

p <- 
  p +
  #Definer et tema
  theme_minimal()
p
Her bruker jeg ferdig-funksjoner for å endre det visuelle uttrykket til grafikken.

Her bruker jeg ferdig-funksjoner for å endre det visuelle uttrykket til grafikken.

Disse kan du så endre på, hvis du vil.

p <-
  p +
  #Endre på minimal-temaet etter å ha definert det
  theme(
    #Flytt tegnforklaringen ned til under plottet
    legend.position = "bottom",
    #Fet skrift for tittelen i tegnforklaringen
    legend.title = element_text(face = "bold"),
    #Kursiv for akseverdiene
    axis.text = element_text(face = "italic"),
    #Fjern støttelinjene
    panel.grid = element_blank())
p
Her har jeg endret bakgrunnen og andre tematiske elementer i grafikken.

Her har jeg endret bakgrunnen og andre tematiske elementer i grafikken.

Funksjonene element_...() brukes inni theme() funksjonen. De oppgir alt unntatt datainnholdet/informasjonen til et element: Skal det være et blankt element element_blank()? Spesifikk skrift? element_text() … osv.

Eksempelvis, kan jeg fortelle at teksten som brukes for aksenavnene skal vises som kursiv element_text(face = "italic").