14  Vizualizace dat s balíkem ggplot2

V R existuje celá řada nástrojů pro vizualizaci dat, které jsou opravdu velmi heterogenní. Z úvodních lekcí znáte příklady tzv. base grafiky, která přichází s každou instalací R. Příklad může být Vám známá funkce plot(). Tato základní implementace nástrojů pro vizualizaci dat sleduje podobnou logiku jako je obvyklá v ostatních systémech: uživatel explicitně sděluje systému co a jak má vykreslit: “Na souřadnice [X,Y] nakresli zelený křížek”.

Součástí tidyverse je balík ggplot2, který přistupuje k vizualizaci zcela jiným způsobem – a to na mnoha úrovních. ggplot2 je založen na teorii “grammar of graphics” (Wilkinson 2005, 2010) a pracuje na mnohem vyšší úrovni abstrakce. Uživatel vlastně předává funkci jako vstup popis dat a konkrétní provedení vizualizace nechává na ni. Tento způsob práce je poměrně neobvyklý, ale po jeho zvládnutí už neexistuje cesta zpět.

ggplot2 má jedno základní omezení – kreslí pouze 2D obrázky. Na konci kapitoly však uvidíte, že nic jiného pravděpodobně ani nepotřebujete.

Co se v této lekci naučíte?

Složitější úkoly přijdou později.

Let’s lock and load…

library(tidyverse)
library(hexbin)
Poznámka

ggplot2 je tak populární, že existují i jeho porty pro jiné jazyky – například pro Python. I v samotném světě R existuje balík, který s ggplot2 sdílí nejen filosofii, ale i tvůrce. Balík ggvis obsahuje oproti ggplot2 některá koncepční vylepšení. Celkově je však orientován spíše na interaktivní grafiku a proto pro naše účely není tak vhodný. Nicméně kdo pochopí ggplot2 zvládne lehce i ggvis.

Výhodou popularity ggplot2 je i obrovské množství balíků, které možnosti ggplot2 doplňují nebo rozšiřují. Pokud tedy něco neumí samotný ggplot2 je rozumné porozhlédnout se po balících, které hledanou funkcionalitu doplňují. Neúplnou galerii “rozšíření” můžete najít například zde: https://exts.ggplot2.tidyverse.org/

Poznámka 2

Podobně jako ostatní součásti tidyverse se i ggplot2 dramaticky vyvíjí. Tento materiál byl například původně vytvořen s verzí 2.x. Nyní (září 2019) je aktuální verze 3.4.0. Nové verze přinesly zásadní změny, které se však v drtivě většině odehrály “pod povrchem” a pro uživatele nejsou patrné.

Velkou změnou pro uživatele je uvedení geom_sf(), který radikálním způsobem zjednodušuje kreslení map. Nejnovější verze ggplot2 také mění jména estetik u geom_line(). Tloušťka linky se nyní logicky jmenuje linewidth a nikoliv size.

14.1 Logika fungování ggplot2

Pro vysvětlení fungování ggplot2 použijeme schématický obrázek:

Schéma obrázku vytvořeného v ggplot2

ggplot2 je postaven na teorii layered grammar of graphics, každý obrázek vytvořený za jeho pomoci se skládá z několika prvků:

Od dat k vizualizaci

Vizualizace dat, která se skládá z jedné, nebo mnoha překrývajících se vrstev (layers). Každá vrstva přidává do výsledného obrázku jednu dodatečnou vizualizaci. Schématický obrázek má dvě vrstvy obsahující vizualizaci dat. První vrstva (bodový graf) obsahuje zobrazení surových dat – tedy dat, jak jsou. Druhá vrstva vykresluje statistickou transformaci surových dat – proloženou křivku. Obrázek je tedy konstruován podobně, jako byste přes sebe překládaly průsvitné fólie pokreslené fixem. Výsledný obrázek by se postupně rozšiřoval o další a další prvky. Nicméně prvky přidané později překrývají prvky přidané dříve.

Jako příklad vizualizace dat můžeme zkonstruovat následující bodový graf, který obsahuje dvě pozorování:

Vizualizace dat s ggplot2

Z tohoto obrázku vidíme, že pozorování jsou dvě a že nejsou identická. Nic dalšího říci nemůžeme.

Zpět od vizualizace k datům

Aby byl obrázek srozumitelný, musí být k vizualizaci dat připojeny prvky, které ji umožňují porozumět. Tedy umožňují uživateli převést vizualizaci zpět do dat. Takové prvky jsou škály, legendy, osy, atp. Viz obrázek:

Prvky umožňující převod vizualizace zpět do dat

Vzhled obrázku

Poslední skupina prvků upravuje celkový vzhled obrázku. Nemá žádný vztah ke dvěma předchozím kategoriím. Tyto prvky upravují pouze celkový vzhled obrázku (velikost písma, barvu pozadí,…).

Následující příklad ukazuje možný vzhled obrázku:

Změny vzhledu obrázků

V ggplot2 se všechny tři skupiny prvků ovládají nezávisle na sobě. Pro vytvoření každého prvku existují speciální funkce.

14.2 Základní vizualizace: vrstva po vrstvě

Celkovou konstrukci obrázku můžeme ilustrovat na tabulce diamonds, která je součástí balíku ggplot2. Tabulka obsahuje informace o jednotlivých kamenech (viz ?diamonds). Tabulka obsahuje přes 50 tisíc pozorování. Jejich vykreslování by zejména na pomalejších počítačích trvalo velmi dlouho. Proto budeme pracovat pouze s 500 náhodně vybranými řádky:

diamonds <- diamonds %>% sample_n(500)
print(diamonds)
# A tibble: 500 × 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  1.5  Very Good H     VVS1     63.8  57   11654  7.17  7.21  4.59
 2  0.32 Ideal     D     VS1      61.8  56     736  4.39  4.41  2.72
 3  0.3  Ideal     F     VVS1     62.4  56     868  4.28  4.31  2.68
 4  0.71 Ideal     F     VS1      61.4  56    3710  5.75  5.78  3.54
 5  0.83 Very Good D     VS1      61.3  57.2  3984  6.02  6.11  3.72
 6  0.32 Ideal     D     VVS1     61    57    1064  4.45  4.43  2.71
 7  0.92 Ideal     E     SI2      62.2  56    4011  6.2   6.24  3.87
 8  1.24 Good      H     VS2      58.3  58    7210  7.09  7.15  4.15
 9  0.44 Ideal     I     VVS1     60.9  56    1145  4.9   4.96  3   
10  1.26 Very Good I     SI1      61.9  58    6318  6.97  6.92  4.3 
# ℹ 490 more rows

Našim cílem je vytvořit podobný obrázek, který byl použit pro schématickou ilustraci fungování ggplot2: Vykreslit závislost ceny (price), váhy (carat) a kvality řezu (cut).

Základ každého obrázku vytvoříme pomocí funkce ggplot():

ggplot(data = NULL, mapping = aes())

Funkce má dva základní argumenty: data obsahují tabulku s daty, která se mají vizualizovat. Parametr mapping pak přijímá základní pravidla, podle kterých se má vizualizace řídit. Přesněji řečeno pravidla vytvořená funkcí aes(), která určují, jaká proměnná se má mapovat na kterou estetiku.

Pro pochopení těchto pojmů je užitečné zamyslet se nad tím, jak vlastně funguje bodový graf. Jeho podstatou je to, že jedno pozorování, je reprezentováno jedním bodem. Jaké vlastnosti (estetiky) však tento bod může mít?

  1. Pozici v rovině obrázku: souřadnice na ose x (x) a y (y)
  2. Tvar (shape): kolečko, čtvereček, srdíčko, listy, káry,…
  3. Velikost (size)
  4. Barvu (color/colour)
  5. Barvu výplně (fill)
  6. Typ ohraničující linky (stroke)
  7. Průhlednost (alpha)

Všechny estetiky je možné nastavit (set) nebo namapovat (map). V případě nastavení bude estetika u všech pozorování stejná. V případě namapování se hodnota estetiky bude řídit podle hodnoty přiřazené proměnné.

Pokud tedy chceme vykreslit bodový graf závislosti, ceny a váhy, potom tyto proměnné chceme namapovat na vybrané estetiky. Stejného výsledku jako u úvodního obrázku dosáhneme pomocí aes(x = carat, y = price). Základní volání ggplot() tedy může vypadat následovně:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    )

Výsledkem je prázdná formátované plocha. Volání ggplot() totiž pouze vytvoří prázdné “plátno”, na které je potřeba přidávat jednotlivé vrstvy s obsahem.

Nové vrstvy se typicky přidávají voláním funkcí geom_*(). Tyto funkce nejsou nic jiného, než aliasy pro různé nastavení funkce layer():

layer(geom = NULL, stat = NULL, data = NULL, mapping = NULL,
  position = NULL, params = list(), inherit.aes = TRUE,
  check.aes = TRUE, check.param = TRUE, subset = NULL, show.legend = NA)

Funkce layer() má mnoho parametrů. Zásadní jsou tyto:

  • data…tabulka s daty, která se má použít v dané vrstvě. Pokud tento parametr není zadán, potom se použije tabulka dat specifikované ve volání ggpplot(). Každá vrstva tak může pracovat s jinými daty. (A také ggplot() nemusí obsahovat specifikaci dat.)
  • geom…typ geometrického objektu, který se má použít pro vizualizaci dat. Může se jednat o bod (point), polygon (polygon), úsečku (line) a mnoho dalších.
  • mapping…obsahuje namapování proměnných na estetiky pomocí funkce aes(). Pokud není parametr specifikován, potom se použije specifikace z ggplot(). Pokud je specifikace v konfliktu se specifikací v ggplot(), potom se použije specifikace z layer(). Pokud je specifikována estetika, která není přiřazena v ggplot(), potom se použije jako doplnění ke specifikaci z ggplot().
  • stat…statistická transformace dat, která se má provést před vykreslením. Vykreslují se až transformované data.
  • position…umožňuje upravit pozici hrubých dat před jejich vykreslením

Funkce layer() není zpravidla nikdy volána přímo. V podstatě vždy se používají funkce geom_*(). Pokud například chceme přidat vrstvu s bodovým grafem, potom použijeme geom_point(). Tato funkce je přesným ekvivalentem volání:

layer(
  data = NULL,
  mapping = NULL,
  geom = "point",
  stat = "identity",
  position = "identity"
)

Pokračujme v příkladu konstrukcí bodového grafu:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point()

Vrstvu obsahující body jsme vytvořili voláním funkce geom_point(). Tato nová vrstva je k původnímu volání ggplot() připojena funkcí +. Pomocí této funkce můžeme do obrázku přidávat další vrstvy – například vrstvu, která vykreslí proloženou křivku:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point() +
    geom_smooth()
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Jak vlastně funguje ggplot2 vevnitř? Základní volání ggplot() vytvoří “prázdnou” datovou strukturu. Funkce + potom R říká, že má tuto datovou strukturu modifikovat. Jakým způsobem se to má stát určuje funkce volaná za +. Například + geom_point() přidá k původní datové struktuře vrstvu s body, atd.

Primárním výsledkem volání ggplot() je tedy datová struktura. Tu můžeme přiřadit do proměnné:

p <- diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point()

…a dále modifikovat…

p <- p + geom_smooth()

…nebo “vytisknout”:

p
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

14.3 Mapování a nastavování estetik

V předchozím textu byl vysvětlen pojem estetika a mapování. Nyní se tomuto tématu budeme věnovat podrobněji. Každý geom má několik estetik – dimenzí, jejichž podobu lze řídit podle určité proměnné. V případě bodového grafu (point) jde o: x, y, shape, size, color/colour, fill, stroke a alpha. Například geom line (spojnicový graf) má estetiky jiné – nerozumí stroke, shape a fill. Namísto toho umí pracovat s linetype.

Výše uvedený příklad demonstruje mechanismus mapování. S pomocí funkce aes() je estetikám přiřazena proměnná. Některé estetiky je v závislosti na geomu nutné přiřadit. V případě geom_point() je to například x a y:

diamonds %>% 
    ggplot(
        aes(x = carat)
    ) +
    geom_point()
Error in `geom_point()`:
! Problem while setting up geom.
ℹ Error occurred in the 1st layer.
Caused by error in `compute_geom_1()`:
! `geom_point()` requires the following missing aesthetics: y

Pomocí aes() je možné mapovat i další estetiky:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price, size = x, color = cut)
    ) +
    geom_point()

Pro každou namapovanou estetiku ggplot2 vytvoří vodítko, které čtenáři umožní překlad z vizualizace do dat. V případě x a y vykreslí osy grafu. U zbývajících estetik vykreslil legendu.

Jednu proměnnou jde mapovat i na více estetik:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price, size = cut, color = cut)
    ) +
    geom_point()

ggplot2 takovou kombinaci zohlední i v legendě. Tento příklad ukazuje i další vlastnost ggplot2. Pokud uživatel požaduje provedení něčeho, co Tvůrce (H. Wickham) nepovažuje za dobrý nápad, vrátí ggplot2 upozornění (via message()).

ggplot2 poskytuje obrovské množství možností a částečně uživateli radí. Nicméně je na uživateli, aby vše nastavil tak, aby výsledné obrázky splňovaly svůj účel – jasně čtenáři předávaly určitou informaci. Obrázek použitý v příkladu například trpí zásadní vadou, která se říká “overplotting”. Pozorování se vzájemně překrývají a ve výsledný obrázek čtenáři mnoho neřekne. Nemůže vědět, zda se v některém místě vzájemně překrývá málo, nebo mnoho pozorování. Jednoduchou a často dostačující cestou jak bojovat s tímto problémem je nastavení průhlednosti (estetiky alpha). Tuto estetiku lze samozřejmě mapovat na proměnnou, ale v tomto případě ji použijeme pro ilustraci nastavení estetiky:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point(alpha = 0.1)

Pokud chce uživatel estetiku nastavit, je potřeba ji deklarovat v příslušné geom_*() funkci a mimo funkci aes(). Příklad ukazuje, že nastavení průhlednosti problém s overplottingem minimálně zmírnilo.

Kontrola mapování

Ve všech příkladech jsme dosud pouze mapovali estetiku na určité proměnné. Mapování předává ggplot2 informaci typu: “barvu puntíků urči podle proměnné cut”. Jak konkrétně to ggplot2 provede (t.j. bude mít špatný řez červenou, modrou, zelenou nebo jinou barvu) jsme zatím nijak neovlivňovali.

V této kapitole si ukážeme, jak lze s pomocí funkcí scale_*_*() měnit škálování a prvky, které pomáhají čtenáři převést vizualizaci zpět do dat.

Pro tyto účely budeme používat jednoduchou datovou tabulku se 3 pozorováními a 4 sloupci x, y, z a w

xtable <- tibble(
    x = c(1,2,3),
    y = c(1,1,1),
    z = c("A","B","C"),
    w = c(-1,0,1)
)

…a jednoduchý bodový graf:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point()

ggplot2 namapoval data na estetiky (pozice) a vykreslil osy, které čtenáři umožňují číst data. Obrázek je takový, jaký asi všichni očekávají. Nicméně to nemusí být to, co uživatel chce. Co může chtít změnit? Například:

  • jméno škály (name)
  • popisky na ose (labels)
  • pozice “hlavních” popisků na ose (breaks)
  • pozice “pomocných” popisků/čar na ose (minor_breaks)
  • interval zobrazený na obrázku – “jak dlouhá” má být osa (limits)
  • pozici osy (position)
  • transformaci hodnot na ose – například zobrazení logaritmu pozorovaných hodnot (trans)
  • zobrazení sekundární osy (sec.axis)

Úplný výčet možností se samozřejmě liší podle estetiky.

Mapování se řídí pomocí funkcí scale_*_*(), která se podobně jako geom_() připojují k volání ggplot() pomocí funkce +. Jména funkcí scale_*_*() se mohou zdá komplikovaná, ale opak je pravdou. Ve jejích jménech je totiž systém. Jméno funkce se sestává ze tří částí, spojených znakem “_“:

  1. Všechna jména začínají scale
  2. Druhá část jména je jméno estetiky, kterou chceme kontrolovat
  3. Poslední část jména je jméno konkrétní škály (to je potřeba si občas pamatovat nebo najít)

Pokud bychom například chtěli změnit jméno osy x (estetika x) na našem obrázku, potom budeme chtít použít funkci:

scale_x_continuous()

První část jména je daná, druhá je jasná a třetí odkazuje na povahu proměnná x v tabulce xtable, která je spojitá. (Takto pěkně ale jména škály nefungují vždy. Občas je třeba hledat.)

Pokud známe jméno, je z poloviny vyhráno:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    scale_x_continuous(name = "Tohle je osa x")

Jak bylo zmíněno výše, pomocí škál je možné nastavit velké množství parametrů:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    scale_x_continuous(
        name = "Tohle je osa x",
        breaks = c(1,2,3),
        labels = c("málo","víc","málo"),
        limits = c(0,4),
        position = "top"
        )

Poněkud specifické je nastavení transformace. V praxi se často používá logaritmická transformace:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    scale_x_continuous(trans = "log10")

Řada transformací je pro uživatele připravená (viz ?scale_x_continuous()). Uživatel má navíc možnost vytvořit vlastní transformace. Pro nejčastější transformace má ggplot2 předpřipravené škály – například log10 nebo reverse:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    scale_x_log10()

V předchozích příkladech jsme volali funkci scale_x_*() pro nastavení mapování estetiky x. Nicméně ani jednou jsme neupravovali estetiku y a taky to fungovalo. Jak je to možné? Pokud není funkce scale_*_*() explicitně volána uživatelem, ggplot2 odhadne, která funkce je nejvhodnější a zavolá ji interně sám. Volání:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point()

Je tak ve výsledku ekvivalentní k:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    scale_x_continuous() +
    scale_y_continuous()

Pokud se změní povaha podkladových dat, potom se i ggplot2 rozhodne volat jinou funkci scale_*_*(). V případě volání:

xtable %>% 
    ggplot(
        aes(x = factor(x), y = y)
    ) +
    geom_point()

kde proměnná x je konvertována na faktor. Vrátí ggplot2 obrázek ekvivalentní volání:

xtable %>% 
    ggplot(
        aes(x = factor(x), y = y)
    ) +
    geom_point() +
    scale_x_discrete() +
    scale_y_continuous()

ggplot2 v tomto případě volá škálu vytvořenou pro mapování diskrétních proměnných.

Mapování barev (estetiky colour a fill)

Pomocí funkcí scale_*_*() lze měnit mapování všech estetik. Mezi nejčastější upravované škály patří bezesporu barvy, které se mapují v estetikách color/colour a fill.

Poznámka: ggplot2 neumí a podle jeho tvůrců nikdy nebude umět vybarvovat texturou (“šrafováním”).

Pro ilustraci těchto estetik použijeme následující podobu bodového grafu:

xtable %>% 
    ggplot(
        aes(x = x, y = y, color = z, fill = w)
    ) +
    geom_point(
        shape = 21,
        stroke = 3,
        size = 10
        ) +
    scale_x_continuous(
        limits = c(0.5,3.5)
    ) +
    theme(
        legend.direction = "horizontal"
    ) -> p

p

Výsledná podoba obrázku je ovlivněna funkcí theme(). Její fungování je popsáno níže. Celá struktura je přiřazená do proměnné p. S tou budeme nadále pracovat.

Tento graf mapuje proměnné z a w na estetiky color a fill. Ostatní estetiky jsou nastaveny a jsou pro všechny body v grafu shodné.

Proměnná z a w se liší ve své povaze. wje spojitá proměnná a z je kategoriální. Práce s barvami by se obecně měla řídit podle povahy zobrazovaných dat:

  • Spojité proměnné se typicky zobrazují barevným přechodem – jak vidíte na obrázku, základní volbou ggplot2 je přechod z tmavě do světle modré.
  • Kategoriální proměnné, jejichž hodnoty jsou “na stejné úrovni”. Příkladem takové proměnné mohou být například značky automobilů. V takovém případě je zvolit barvy, které jsou pro vnímání člověka “stejně daleko” od sebe. Příkladem takového výběru barev může být proměnná z v ukázkovém obrázku.
  • Posledním typem jsou kategoriální proměnné, které vyjadřují sekvenci (např.: malá, střední, velká). V takovém případě je vhodné volit barevný přechod, který je také rozdělen do kroků, které musí být tak velké, aby byly pro člověka jasně odlišitelné.

Při volbě barev je také užitečné myslet na barvoslepé a na tisk na černobílé tiskárně. Jiné barvy jsou také vhodné pro plochy a jiné pro body. Ve výsledku je výběr barev nesmírně složitý. Najít sekvenci barev pro 10 úrovní je těžké, pro 20 úrovní je to nemožné. Moudrým postupem je proto používat uměřený počet barev (úrovní) a v jejich volbě vycházet z doporučení odborníků.

Spojité proměnné

Typickým postupem zobrazování spojité proměnné v barvě je použití barevného přechodu (gradientu) mezi dvěma nebo více barvami. ggplot2 nabízí hned 4 škály založené na gradientech:

scale_*_gradient() (identické se scale_*_continuous()) je základní volbou, po které u spojitých proměnných ggplot2 sáhne, pokud uživatel neporoučí jinak. Vykresluje přechod mezi dvěma barvami, které jsou nastavené parametry low a high:

p + scale_fill_gradient(low = "hotpink", high = "#56B1F7")

V příkladu je nastaven přechod z tmavě poníkové do modré. Barva je vždy definována jako řetězec (nebo funkcí, která řetězec nebo jejich vektor vytvoří) – a to buď svým jménem ("hotpink") nebo RGB kódem ("#56B1F7").

scale_*_gradient2() je derivátem scale_*_gradient() umožňuje vytvořit přechod přes tři barvy (low, mid, high) a specifikovat pozici středního bodu parametrem midpoint. (Parametr midpoint je defaultně nastaven na hodnotu 0.)

p + scale_fill_gradient2(midpoint = 0.5)

scale_*_gradientn() umožňuje použít přechod přes tolik barev, kolik si jen srdce uživatele přeje. Z tohoto důvodu neobsahuje žádné základní nastavení:

p + scale_fill_gradientn()
Error in scale_fill_gradientn(): argument "colors" is missing, with no default

Uživatel musí prostřednictvím parametru colours zadat vektor barev, které se mají použít. Domnívám se, že pro uživatele – neodborníka – se jedná o nemožný úkol. Je však možné použít předpřipravené palety, které jsou součástí mnoha balíků. Následující příklad využívá paletu inferno z balíku viridis:

p + scale_fill_gradientn(colours = viridis::inferno(3))

Využití scale_*_gradientn() je vhodné spíše v plochách. Nicméně i tam je dobré spolehnout na rady odborníků. Příklad “plošného” obrázku:

Na radách odborníků je zcela postaven scale_*_distiller(). Pracuje s ručně vybranými diskrétními paletami, které jsou dostupné na http://colorbrewer2.org/. scale_*_distiller() tyto diskrétní palety adaptuje i pro spojité proměnné. Rozumný způsob práce je na webu zvolit vhodnou barevnou škálu a její jméno specifikovat v parametru palette. Ten um pracovat s číslem palety i s jejím jménem:

p + scale_fill_distiller(palette = "YlGnBu")

Parametrem direction, který nabývá hodnot 1 nebo -1, lze pořadí barev obrátit:

p + scale_fill_distiller(palette = "YlGnBu", direction = 1)

Používání http://colorbrewer2.org/ lze doporučit v maximální rozumné míře.

Diskrétní proměnné

Pro diskrétní proměnné je výchozí volbou scale_*_hue(). Tato škála vybírá od sebe stejně vzdálené barvy na “HCL wheel” (viz např. Wikipedie). Teoreticky je tak schopna vytvořit barevné schéma pro prakticky neomezený počet kategorií. V praxi od sebe budou barvy při vyšším počtu kategorií nerozpoznatelné.

p + scale_color_hue()

scale_*_hue() umožňuje vybrat výchozí bod na HCL wheel a parametrem direction směr, jakým se na něm výběr pohybuje:

p + scale_color_hue(direction = -1)

Zejména při finalizaci grafických výstupů je vhodné poohlédnout se po předpřipravených paletách. V samotném ggplot2 jde o scale_*_brewer() – bratra scale_*_distiller() pro diskrétní veličiny.

p + scale_color_brewer(palette = "Set1")

Speciální škálou je scale_*_grey(). Ta, jak název napovídá, mapuje do odstínů šedé:

p + scale_color_grey()

Speciálními případy škál jsou scale_*_manual() a scale_*_identity(). scale_*_manual() umožňuje uživateli vytvořit si vlastní škálu. U barev bude tato možnost využívána asi jen zřídka. Za určitých okolností se však může hodit u jiných estetik – například u shape.

scale_*_identity() dokáže vzít proměnnou z datové tabulky a použít ji “tak jak je” pro vykreslení barev, tvarů a podobně.

Co jsme zamlčeli…

Všechny krásné škálovací funkce scale_*_*() v posledku využívají služeb konstruktorových funkcí discrete_scale() a continuous_scale(). S těmito funkcemi komunikujete ze scale_*_*() přes argument .... Pokud tedy budete chtít provádět něco složitějšího, tak se vyplatí projít si nápovědu právě k těmto konstruktorovým funkcím.

14.4 Grupování

U některých funkcí geom_*() je mezi estetikami uvedena jedna s názvem group. Lehko si můžete ověřit, že se nijak nepřekládá do vizuální podoby. Její funkce je totiž jiná. Sděluje ggplot2, že určitá skupina pozorování “patří k sobě” – tvoří jednu skupinu.

Praktické využití lze ilustrovat na proložení křivky v tabulce diamonds:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point() +
    geom_smooth()

V tomto případě se proložila křivka přes všechny pozorování. Můžeme se ale chtít podívat, jestli je závislost stejná pro různé druhy kamenů – například pro skupiny definované kvalitou řezu (cut). V tomto případě použijeme estetiku group:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price, group = cut)
    ) +
    geom_point() +
    geom_smooth()

V tomto případě geom_smooth(), který estetice group rozumí, pracuje s každou skupinou vymezenou hodnotou kategoriální proměnné cut zvlášť. Fakticky každou skupinu proloží její vlastní křivkou.

V praxi je často užitečné pro zpřehlednění využít možnosti mapování jedné proměnné na více estetik:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price, group = cut, color = cut)
    ) +
    geom_point() +
    geom_smooth()

Všiměte si že v tomto případě dostanete stejný výsledek i při namapování kategorické proměnné na estetiku color:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price, color = cut)
    ) +
    geom_point() +
    geom_smooth()

14.5 Statistické transformace

ggplot2 umožňuje nejen vykreslovat samotná data, ale i jejich interně vytvořené statistické transformace. Příkladem může být například proložení křivkou (geom_smooth()). Jejich tvorbu mají na starosti funkce stat_*(). Vztah mezi funkcemi geom_*() a stat_*() není úplně jasný. Každá transformace (stat) má svůj výchozí (přednastavený) geometrický tvar (geom) a každý geom má svůj výchozí stat. V některých případech tak může být jedno, jestli uživatel volá geom nebo ekvivalentní stat, Viz například použití stat_smooth():

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point() +
    stat_smooth()

Tato nejednoznačnost vymezení toho, co vlastně dělá stat_*() a co geom_*() je velmi otravnou nedokonalostí v návrhu ggplot2 (a v ggvis je již vyřešena). Pokud není nevyhnutelné volat přímo stat_*() nutné, je lepší vždy používat funkce geom_*().

Pro přímé volání stat_*() existovaly v podstatě dva důvody:

  1. potřeba specifických nastavení statistických transformací
  2. potřeba použití jiného než standardního geomu pro vykreslení statistické transformace

Pohledem do nápovědy zjistíte, že stat_smooth() umožňuje mnohem více nastavení vyhlazování:

geom_smooth(mapping = NULL, data = NULL, stat = "smooth",
  position = "identity", ..., method = "auto", formula = y ~ x,
  se = TRUE, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE)

stat_smooth(mapping = NULL, data = NULL, geom = "smooth",
  position = "identity", ..., method = "auto", formula = y ~ x,
  se = TRUE, n = 80, span = 0.75, fullrange = FALSE, level = 0.95,
  method.args = list(), na.rm = FALSE, show.legend = NA,
  inherit.aes = TRUE)

V novějších verzích ggplot2 se však již dají zpravidla předávat hodnoty parametrů pro stat_*() prostřednictvím ... v geom_*(). Tato možnost prakticky eliminuje potřebu volat stat_*() funkce přímo kvůli nastavení statistických transformací.

Druhý důvod pro přímé použití stat_*() funkce však stále trvá. V některých případech můžete potřebovat použít pro vyreslení transformace netypický geom. Příkladem může být například zobrazování jádrové hustoty rozdělení dvou proměnných nikoliv pomocí vrstevnic (standardní zobrazení), ale pomocí barevných polygonů. (Výsledkem by byly například barevné plochy s barvou sytější tam, kde je odhadnuta vyšší hustota.) Tyto úlohy jsou však bezpochyby z kapitoly pro pokročilé.

14.6 Pozicování

V ggplotu2 existuje hned několik způsobů, jak je možné ovlivnit pozici vykresleného geomu (tj. geometrického objektu reprezentujícího data) na stránce. První možností bylo použití škál – například jejich logaritmická transformace.

ggplot2 však v tomto ohledu nabízí mnohem více možností. Místem kde začít hledat je parametr position ve funkci layer(). Smyslem jeho použití je předejít překryvu pozorování ve výsledném obrázku. Parametr může nabývat následujících hodnot:

  • identity…je základní volba pro většinu funkcí geom_*(). Prakticky znamená, že z hlediska modifikace pozice geomu předává instrukci “nedělej nic a kresli na skutečnou pozici”.
  • jitter…ke skutečné pozici přidává náhodnou chybu
  • dodge…geomy skládá vedle sebe
  • stack…staví komínky – skládá geomy nad sebe
  • nudge…u překrývajících se geomů přidává ke skutečné pozici definovanou vzdálenost na ose x a y
  • jitterdodge…kombinuje jitter a dodge

V následujícím textu jsou blíže ukázány nejčastěji používané varianty změny pozic: jitter, dodge, stack a navíc poněkud specifická varianta fill. Varianty nudge a jitterdodge jsou specifické pro určité méně geomy a jejich přímé volání je v podstatě výjimečné.

jitter

position = "jitter" je populární volba zejména u bodových grafů, ve kterých je velký problém s overplottingem. Toto použití je tak typické, že existuje dokonce speciální geom_*() funkce: geom_jitter(). Ta je ekvivalentní k geom_point(position = "jitter").

geom_jitter() přidává ke skutečné pozici pozorování náhodnou složku, která je generovaná z uniformního rozdělení:

xtable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_jitter(color = "red") +
    geom_point()

V obrátku vidíte skutečnou pozici pozorování vykreslenou pomocí geom_point() a červené tečky ukazující posunuté hodnoty vykreslené pomocí geom_jitter(). Protože je chyba náhodná, bude obrázek při každém vykreslení vypadat jinak.

Pomocí parametrů width a height je možná nastavit šířku a výšku obdélníku, ve kterém se posunutá pozorování budou nacházet. Protože se náhodná složka generuje z uniformního rozdělení, je každý posun stejně pravděpodobný. Skutečná pozorovaná hodnota je pak právě ve středu obdélníku. To ilustruje následující obrázek:

xtable2 <- tibble(
    x = rep(1:2,1000),
    y = rep(1:2,1000)
)
xtable2 %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_jitter(
        alpha = 0.05,
        color = "red"
        ) +
    geom_point()

Posunutá pozorování (červené tečky) tvoří mračno rovnoměrně vyplňující celý obdélník za pozorovanou hodnotou (černá tečka), která je přesně v jeho středu.

stack

Typické použití této změny této modifikace pozice je u sloupcových grafů. Ty se v ggplot2 tvoří pomocí geom_bar() a jejich základní nestavení je, že mají být použity k vykreslení rozdělení diskrétní proměnné.

Pro použití geom_bar() vytvoříme novou tabulku occupation:

occupation <- tibble(
    sex = rep(c("male","female"),100),
    occupation = sample(c("A","B","C","D"), 200, replace = TRUE, prob = abs(rnorm(4))),
    age = sample(c("old","young"), 200, replace = TRUE)
)

Tabulka obsahuje povolání (A, B, C nebo D), pohlaví a věkovou kohortu 200 lidí. Zajímá nás rozdělení povolání ve vzorku:

occupation %>% 
    ggplot(
        aes(x = occupation)
    ) +
    geom_bar()

geom_bar() v základním nastavení, tedy se stat = "count", provádí statistickou transformaci dat. Zjistí absolutní četnost jednotlivých variant proměnné v estetice x a tyto četnosti následně vykreslí formou sloupcového grafu.

Obrázek momentálně nijak nezohledňuje pohlaví, ale to se dá změnit. Lze ho namapovat například (a typicky) na estetiku fill:

occupation %>% 
    ggplot(
        aes(x = occupation, fill = sex)
    ) +
    geom_bar()

Ve výsledku dodá statistická transformace provedená stat_count() pro každé povolání dvě hodnoty: počet mužů a počet žen. Ty by se v logicky měly kreslit přes sebe – patří v obrázku na jedno místo. Očekávali bychom tedy tento obrázek:

Takový obrázek však není příliš užitečný, protože poskytuje spolehlivou informaci pouze o maximálních hodnotách. Proto již v základním nastavení geom_bar() používá position = "stack, která počet žen a mužů postaví nad sebe – do jednoho komínku.

Poznámka: “Špatný” obrázek můžete vykreslit s geom_bar(position = "identity).

dodge

Dalším typickým řešením u sloupcových grafů je vykreslení hodnot vedle sebe. Tento způsob čtenáři lépe předává informaci o počtu mužů a žen napříč povoláními. Horizontální posunutí sloupců zajišťuje position = "dodge":

occupation %>% 
    ggplot(
        aes(x = occupation, fill = sex)
    ) +
    geom_bar(position = "dodge")

fill

Poněkud specifickou úpravou pozice je fill, která upravuje nejen pozici, ale vlastně provádí i další statistickou transformaci. Nevrací totiž absolutní, ale relativní četnost:

occupation %>% 
    ggplot(
        aes(x = occupation, fill = sex)
    ) +
    geom_bar(position = "fill")

Facetování: na půli cesty mezi grupováním a pozicováním

Výše popsané pozicování mělo pozici geomů vlastně v rámci jednoho obrázku. Facetování k problému přistupuje jinak – rozlamuje původně jeden obrázek na více dílčích obrázků (facet). ggplot2 umožňuje vytvářet buď matice dílčích obrázků pomocí facet_grid() nebo prosté rozlomení do více obrázků pomocí facet_wrap().

Nejjednodušší příklad je použití facet_wrap(). Řekněme, že budeme chtít rozdělit obrázek s rozdělením povolání podle pohlaví:

occupation %>% 
    ggplot(
        aes(x = occupation)
    ) +
    geom_bar() -> p

p + facet_wrap("sex")

facet_wrap() rozdělil celkový obrázek na dva dílčí podle hodnot proměnné sex. Ta je v příkladu určena svým jménem (character). Další možností je její určení pomocí jednostranné rovnice (formula, blíže viz přednáška o ekonometrii v R):

p + facet_wrap(~ sex)

Vytvářet dílčí obrázky lze i podle kombinace více proměnných – můžeme chtít například samostatné dílčí obrázky pro kombinaci pohlaví a věku. V tomto případě se pro specifikaci proměnných použije vektor jejich jmen:

p + facet_wrap(c("sex","age"))

Nebo alternativně:

p + facet_wrap(~ sex + age)

Použít pro tyto aplikace facet_wrap() je možné, ale často je vhodnější sáhnout po facet_grid(), který je přímo navržen pro rozlomení obrázku podle kombinace více proměnných. facet_grid() vyžaduje specifikaci facetování pomocí objektu třídy formula:

p + facet_grid(sex ~ age)

Na levé straně rovnice (na levé straně ~) je proměnná (nebo jejich kombinace spojená +), která má tvořit řádky a na levé straně je proměnná nebo jejich kombinace, která má tvořit sloupce.

Funkce vytvářející facetování mají i další užitečné parametry. Pro facet_wrap() jsou specifické parametry ncol a nrow, které určují formát výstupní tabulky dílčích obrázků – kolik má mít sloupců a kolik řádků. Stačí přitom zadat jen jeden z těchto parametrů:

p + facet_wrap(c("sex","age"), nrow = 1)

Užitečný, ale pro správné čtení dat velmi ošemetný, parametr je scales. Ten umožňuje uvolnit škály v jednotlivých dílčích obrázcích. Základní hodnotou je fixed. V tomto případě jsou měřítka na všech dílčích obrázcích stejná. Varianty free, free_y a free_x umožňují uvolnit měřítka na všech osách nebo jen na ose x popřípadě y:

p + facet_wrap(c("sex","age"), nrow = 1, scales = "free_y")

Příklad ilustruje, že uvolnění os může být pro čtenáře velmi zavádějící. Stejná výška sloupce totiž v různých dílčích obrázcích znamená jinou pozorovanou hodnotu.

14.7 Souřadnicové systémy

ggplot2 podporuje řadu souřadnicových systémů. Základní volbou je lineární coord_cartesian(), ale podporovány jsou i nelineární souřadnicové systémy, které se ovšem používají pouze ve velmi specifických případech jako je například vykreslování map (coord_map()). Dalším příkladem nelineárního systému je coord_polar().

Základní volbou aplikovanou v téměř všech voláních ggplot2 je bezpochyby lineární systém, ve kterém je pozice prvků určena jejich souřadnicemi na ose x a y. Základní funkcí je coord_cartesian(), ale ggplot2 obsahuje i její různé mutace a transformace:

  • coord_fixed() udržuje konstantní zadaný poměr stran.
  • coord_equal() je zkratka pro coord_fixed() s poměrem stran 1.
  • coord_flip() prohazuje osy x a y.
  • coord_trans() umožňuje provést transformaci os.
  • coord_sf() je speciální funkce pro práci s mapami.

Fungování coord_flip() je možné ilustrovat pomocí jednoduchého bodového grafu. V definici mapování je váha (carat) namapována na osu x a cena na osu y:

diamonds %>% 
    ggplot(
        aes(x = carat, y = price)
    ) +
    geom_point() -> p

Použití coord_flip() prohodí osy x a y – výsledek tedy odpovídá použití aes(y = carat, x = price):

p + coord_flip()

coord_flip() je užitečné u některých specifických geomů – například u boxplotů, která zobrazují základní charakteristiky rozdělení spojité proměnné.

coord_trans() umožňuje transformovat osy podobným způsobem jako scale_*_*() funkce. Stejně jako ostatní funkce odvozené od coord_cartesian() umožňuje nastavit interval zobrazený na osách. Podobnou možnost mají opět i scale_*_*() funkce.

Nabízí se tedy otázka, na co jsou vlastně funkce coord_*() užitečné. Pro demonstraci odpovědi nasimulujeme velmi specifickou tabulku:

cltable <- tibble(
    x = c(
        rnorm(300, sd = 2),
        rnorm(40, sd = 0.5) + 10
        ),
    y = c(
        rnorm(300, sd = 1),
        rnorm(40, sd = 0.5) + 5
        )
)

cltable %>% 
    ggplot(
        aes(x = x, y = y)
    ) +
    geom_point() +
    geom_smooth(method = "lm", se = FALSE) -> p
    
p

Simulovaná data obsahují dva shluky pozorování. Pokud je proložíme regresní přímkou (tj. vykreslíme statistickou transformaci dat) získáme (falešné) zdání existence rostoucího vztahu mezi veličinou na ose x a na ose y.

Řekněme, že z nějakých důvodů budeme chtít omezit zobrazené hodnoty na ose x na interval \([-5,5]\). To lez provést buď pomocí funkcí scale_x_*() nebo coord_*():

p + scale_x_continuous(limits = c(-5,5))

p + coord_cartesian(xlim = c(-5,5))

Rozdíl je zjevný. Při použití scale_x_*() je regresní přímka prakticky vodorovná. Naopak při použití coord_*() je stále rostoucí. Rozdíl je v tom, kdy je aplikované statistická transformace (v tomto případě vyhlazení). Při použití scale_*_*() funkce je nejprve omezen rozsah dat a následně je aplikována transformace. Výsledek tak může být zavádějící. Naproti tomu při použití coord_*() je nejprve aplikována transformace a následně je omezen rozsah zobrazených dat. Transformace tak reflektuje i ta pozorování, která nejsou zobrazená.

coord_*() tak nabízí možnost skutečně “zazoomovat” část obrázku, aniž by to ovlivnilo obrázek samotný.

14.8 Vzhled obrázků

Vzhled obrázků, nebo přesněji prvky, které nemají vztah k datům se v ggplot2 ovládají pomocí funkce theme(). Ta umožňuje změnit vzhled obrázků k nepoznání: můžete mít obrázek, který vypadá, jako by byl vytvořen v Excelu, Stata, nebo jako by vyšel v The Economist.

Funkce theme() má obrovské množství parametrů a nebylo by praktické je nastavovat vždy u každého obrázku. Proto v ggplot2 existuje řada předpřipravených kompletních témat (vzhledů). Například theme_bw(), theme_classic() nebo theme_void(). Základní vzhled obrázků potom odpovídá theme_gray(). Nápověda ggplot2 obsahuje hrubá doporučení, kdy je vhodné použít které téma.

Ukázky předpřipravených témat

Pro ukázky použijeme již známý sloupcový graf:

occupation %>% 
    ggplot(
        aes(x = occupation, fill = sex)
    ) +
    geom_bar() -> p

Populární téma je theme_bw(), které ggplot2 doporučuje například pro prezentace:

p + theme_bw()

Klasicky vypadající téma s minimem čar je theme_classic():

p + theme_classic()

Například pro kreslení map oceníte velmi speciální téma theme_void(), které zahodí všechny čáry:

p + theme_void()

Sbírku předpřipravených témat obsahuje například balík ggthemes. Kromě různých theme_*() obsahuje balík i řadu scale_*_*() a několik geom_*() funkcí.

Například minimalistické téma vytvořené podle Tufteho doporučení obsahuje ggthemes::theme_tufte():

p + ggthemes::theme_tufte()

Modifikace témat

ggplot2 umožňuje vytvářet vlastní předpřipravená témata, ale většinou bohatě postačuje použít předpřipravené a upravit ho, to lze zařídit následujícím způsobem:

p + 
    theme_classic() +
    theme(
        panel.grid.major = element_line(linewidth = 0.2)
    )

U theme() funguje vrstvení stejně, jako u všeho ostatního. Volání jednotlivých funkcí modifikuje podkladovou datovou strukturu. Zavolání funkce theme_classic() tedy nastaví všechny parametry tak, jak to odpovídá theme_classic – to znamená, žádné linky na pozadí. Následné volání theme() přepíše to, co před tím nastavilo volání theme_classic() a linky přidá. Žádný další parametr nebyl v theme() zadán a proto se nic jiného nezmění.

Pokud by funkce byly volány v opačném pořadí, potom by byl výsledek následující:

p + 
    theme(
        panel.grid.major = element_line(linewidth = 0.2)
    ) +
    theme_classic()

Výsledek by přesně odpovídal theme_classic. theme_classic() totiž bez milosti přepíše veškeré formátování vytvořené v předchozích voláních theme() nebo theme_*().

Samotné volání theme() ve výše uvedeném příkladu vypadá skutečně velmi krypticky, ale není to tak, protože, podobně jako (skoro) ve všem v ggplot2, za ním stojí promyšlený systém.

Funkce theme() má opravdu mnoho parametrů (viz ?theme), jejich jména odpovídají jménům jednotlivých grafických prvků. Jména jsou naštěstí často velmi výstižná, intuitivní, a navíc je RStudio zvládá (u novějších verzí ggplot2) našeptávat.

Formátování grafických prvků se ve většině případů provádí pomocí volání jedné z následujících funkcí:

element_text(family = NULL, face = NULL, colour = NULL, size = NULL,
  hjust = NULL, vjust = NULL, angle = NULL, lineheight = NULL,
  color = NULL, margin = NULL, debug = NULL)

element_line(colour = NULL, linewidth = NULL, linetype = NULL,
  lineend = NULL, color = NULL)

element_rect(fill = NULL, colour = NULL, size = NULL, linetype = NULL,
  color = NULL)

element_blank()

Tu správnou lze uhodnout. Pokud chcete upravit formátování prvku, který je ve své podstatě text, potom chcete použít element_text(). Příkladem může být modifikace titulku (plot.title) obrázku:

p + 
    labs(
       title = "Just Another Bar Plot" 
    ) +
    theme(
        plot.title = element_text(colour = "red")
    )

Pokud je prvek svým charakterem čára, potom se jeho formátování nastavuje pomocí element_line() (viz modifikace čar na pozadí výše). Prvky, které jsou ve své podstatě čtyřúhelník se potom modifikují pomocí element_rect(). Příkladem může být změna barvy pozadí grafu na poníkovou:

p + 
    theme(
        panel.background = element_rect(fill = "pink")
    )

Speciální funkce je potom element_blank(), která způsobí, že se daný prvek z obrázku kompletně vypustí:

p + 
    theme(
        legend.title = element_blank()
    )

Například toto volání vypustilo jméno legendy a to zcela. Špinavé triky typu scale_fill_discrete(name = "") sice způsobí, že jméno “nebude vidět”. Respektive se vykreslí prázdný řetězec. Místo pro titulek však bude alokováno a bude tak rozhazovat uspořádání obrázku. element_blank() je správná cesta.

Některé parametry v theme() se nastavují pomocí speciálních funkcí – typicky jde o nastavení velikostí pomocí funkce unit(). Další parametry se nastavují prostým zadáním hodnoty. Příkladem může být pozice a orientace legendy:

p + 
    theme(
        legend.position = "bottom",
        legend.direction = "horizontal"
    )

14.9 Vizualizaci prostorových map

V R existuje řada nástrojů, které umožňují analýzu a vizualizaci různých typů prostorových dat. Dva základní balíky jsou sp a hlavně podstatně modernější sf. Balík sf umožňuje načítání prostorových dat z mnoha různých zdrojů včetně OSM a shapefilů. Uvažujme typický příklad, kdy chceme nkreslit tématickou mapu okresů ČR.

Nejdříve potřebujeme pomocí funkce st_read() načíst datový podklad. V tomto případě budeme načítat data ze shapfilu, který může obsahovat více vrstev – načíst se dá však vždy jen jedna. Seznam vrstev získáme pomocí funkce st_layers():

library(sf)
Linking to GEOS 3.12.0, GDAL 3.7.1, PROJ 9.2.1; sf_use_s2() is TRUE
st_layers("data/ggplot2/SPH_SHP_JTSK/JTSK/")
Driver: ESRI Shapefile 
Available layers:
   layer_name geometry_type features fields crs_name
1     SPH_ORP       Polygon      206      4     <NA>
2     SPH_ZSJ       Polygon    22654      5     <NA>
3    SPH_OBEC       Polygon     6258      6     <NA>
4  SPH_OBLAST       Polygon        8      3     <NA>
5     SPH_OPU       Polygon      393      5     <NA>
6    SPH_KRAJ       Polygon       14      3     <NA>
7      SPH_KU       Polygon    13075      7     <NA>
8    SPH_STAT       Polygon        1      3     <NA>
9   SPH_OKRES       Polygon       77      4     <NA>
10     SPH_SO       Polygon       22      5     <NA>
11     SPH_MC       Polygon      142      6     <NA>

Jméno vrstvy použijeme pro načtení správné “části” shapefilu:

# První parametr: shapefile
# Druhý parametr: jméno vrstvy
okresy <- st_read("data/ggplot2/SPH_SHP_JTSK/JTSK/","SPH_OKRES")
Reading layer `SPH_OKRES' from data source 
  `/home/qasar/vyuka/aved/qbook/data/ggplot2/SPH_SHP_JTSK/JTSK' 
  using driver `ESRI Shapefile'
Simple feature collection with 77 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -904585.3 ymin: -1227296 xmax: -431724.3 ymax: -935237
CRS:           NA
okresy
Simple feature collection with 77 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -904585.3 ymin: -1227296 xmax: -431724.3 ymax: -935237
CRS:           NA
First 10 features:
     ID KOD_NUTS3 KOD_LAU1         NAZEV_LAU1                       geometry
1  3100     CZ010   CZ0100 Hlavní město Praha MULTIPOLYGON (((-736538 -10...
2  3201     CZ020   CZ0201            Benešov MULTIPOLYGON (((-746500.6 -...
3  3202     CZ020   CZ0202             Beroun MULTIPOLYGON (((-791181 -10...
4  3203     CZ020   CZ0203             Kladno MULTIPOLYGON (((-776276.7 -...
5  3204     CZ020   CZ0204              Kolín MULTIPOLYGON (((-671965.2 -...
6  3205     CZ020   CZ0205         Kutná Hora MULTIPOLYGON (((-675684 -10...
7  3206     CZ020   CZ0206             Mělník MULTIPOLYGON (((-747304.2 -...
8  3207     CZ020   CZ0207     Mladá Boleslav MULTIPOLYGON (((-697337.2 -...
9  3208     CZ020   CZ0208            Nymburk MULTIPOLYGON (((-688299.8 -...
10 3209     CZ020   CZ0209       Praha-východ MULTIPOLYGON (((-722769.3 -...

Vypsání objektu na obrazovku ukazuje, co vlastně sf objekty jsou:

okresy %>% class()
[1] "sf"         "data.frame"

Jedná se o tabulky, které mají speciání sloupec geometry (občas se jmenuje SHAPE) a atribut, který v podstatě dává prostorovým datům potřebný kontext – například deklaruje použité CRS. V tomto případě je však objekt poškozen a CRS není deklarováno. To se dá opravit:

okresy <- okresy %>% 
  # Deklaruje správné CRS (S-JTSK / Krovak East North - SJTSK) -- mění se pouze atribut, ale ne data
  st_set_crs(5514) %>% 
  # Transformuje data (do WGS84)
  st_transform(4326)

okresy
Simple feature collection with 77 features and 4 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 12.09057 ymin: 48.55181 xmax: 18.85925 ymax: 51.0557
Geodetic CRS:  WGS 84
First 10 features:
     ID KOD_NUTS3 KOD_LAU1         NAZEV_LAU1                       geometry
1  3100     CZ010   CZ0100 Hlavní město Praha MULTIPOLYGON (((14.52826 49...
2  3201     CZ020   CZ0201            Benešov MULTIPOLYGON (((14.42653 49...
3  3202     CZ020   CZ0202             Beroun MULTIPOLYGON (((13.797 49.8...
4  3203     CZ020   CZ0203             Kladno MULTIPOLYGON (((13.92115 50...
5  3204     CZ020   CZ0204              Kolín MULTIPOLYGON (((15.41584 50...
6  3205     CZ020   CZ0205         Kutná Hora MULTIPOLYGON (((15.37964 50...
7  3206     CZ020   CZ0206             Mělník MULTIPOLYGON (((14.33082 50...
8  3207     CZ020   CZ0207     Mladá Boleslav MULTIPOLYGON (((14.95977 50...
9  3208     CZ020   CZ0208            Nymburk MULTIPOLYGON (((15.18524 50...
10 3209     CZ020   CZ0209       Praha-východ MULTIPOLYGON (((14.74597 49...

Objekt sf je ale pořád tabulka a také s ním tak můžeme pracovat. Například k němu přidat data, která chceme vykreslit do tématické mapy.

# CZSO data
library(czso)
codes <- czso_get_codelist("cis109")

migration <- czso_get_table("170242") %>% 
  group_by(uzemiz_kod) %>% 
  summarise(
    hodnota = sum(hodnota),
    .groups = "drop"
  ) %>% 
  left_join(
    .,codes, by = c("uzemiz_kod" = "kodrso")
  )
migration <- migration %>% 
  left_join(okresy,., by = c("KOD_LAU1" = "chodnota"))

migration
Simple feature collection with 77 features and 13 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 12.09057 ymin: 48.55181 xmax: 18.85925 ymax: 51.0557
Geodetic CRS:  WGS 84
First 10 features:
     ID KOD_NUTS3 KOD_LAU1         NAZEV_LAU1 uzemiz_kod hodnota kodjaz
1  3100     CZ010   CZ0100 Hlavní město Praha       <NA>      NA   <NA>
2  3201     CZ020   CZ0201            Benešov      40169    7774     CS
3  3202     CZ020   CZ0202             Beroun      40177    7986     CS
4  3203     CZ020   CZ0203             Kladno      40185   16367     CS
5  3204     CZ020   CZ0204              Kolín      40193    8265     CS
6  3205     CZ020   CZ0205         Kutná Hora      40207    5423     CS
7  3206     CZ020   CZ0206             Mělník      40215   11082     CS
8  3207     CZ020   CZ0207     Mladá Boleslav      40223    4278     CS
9  3208     CZ020   CZ0208            Nymburk      40231   10023     CS
10 3209     CZ020   CZ0209       Praha-východ      40240   26579     CS
      akrcis kodcis        zkrtext           text    admplod    admnepo
1       <NA>   <NA>           <NA>           <NA>       <NA>       <NA>
2  OKRES_LAU    109        Benešov        Benešov 2008-01-01 9999-09-09
3  OKRES_LAU    109         Beroun         Beroun 2008-01-01 9999-09-09
4  OKRES_LAU    109         Kladno         Kladno 2008-01-01 9999-09-09
5  OKRES_LAU    109          Kolín          Kolín 2008-01-01 9999-09-09
6  OKRES_LAU    109     Kutná Hora     Kutná Hora 2008-01-01 9999-09-09
7  OKRES_LAU    109         Mělník         Mělník 2008-01-01 9999-09-09
8  OKRES_LAU    109 Mladá Boleslav Mladá Boleslav 2008-01-01 9999-09-09
9  OKRES_LAU    109        Nymburk        Nymburk 2008-01-01 9999-09-09
10 OKRES_LAU    109   Praha-východ   Praha-východ 2008-01-01 9999-09-09
                         geometry
1  MULTIPOLYGON (((14.52826 49...
2  MULTIPOLYGON (((14.42653 49...
3  MULTIPOLYGON (((13.797 49.8...
4  MULTIPOLYGON (((13.92115 50...
5  MULTIPOLYGON (((15.41584 50...
6  MULTIPOLYGON (((15.37964 50...
7  MULTIPOLYGON (((14.33082 50...
8  MULTIPOLYGON (((14.95977 50...
9  MULTIPOLYGON (((15.18524 50...
10 MULTIPOLYGON (((14.74597 49...

Výslednou tabulku můžeme vykreslit pomocí speciálního geomu: geom_sf()

migration %>% 
  ggplot(
    aes(fill = hodnota)
  ) +
  geom_sf() +
  coord_sf(
    datum = NA
  ) +
  theme_void()

Použití geom_sf() se liší od standardního použití ggplot2 v jednom detailu – geom_sf() si sám ze sf objektu určí, co má kreslit – body, linky nebo polygony. Správně nadefinované CRS také zajišťuje, že mapa je správně vykreslená. Kromě toho je ovládání stejné jako u ostatních geomů.

14.10 Ukládání obrázků

Vytvořené obrázky je možné ukládat s použitím funkce ggsave():

ggsave(filename, plot = last_plot(), device = NULL, path = NULL,
  scale = 1, width = NA, height = NA, units = c("in", "cm", "mm"),
  dpi = 300, limitsize = TRUE, ...)

V základním nastavení plot = last_plot() funkce ggsave() ukládá poslední vykreslený obrázek. Do parametru plot je však možné přiřadit i obrázek (datovou strukturu) uloženou v proměnné. To je velmi užitečné například při tvorbě obrázků, jejichž vykreslování je velmi náročné – komplikovaných map, bodových grafů s opravdu velkým počtem pozorování a podobně.

Jméno souboru, do kterého se má obrázek uložit se definuje v parametru filename jako řetězec. ggsave() řetězec analyzuje a podle přípony zvolí exportní formát obrázku. ggsave() umožňuje formáty do vektorových formátů i bitmap:

  • vektorové formáty: eps, ps, tex (pictex), pdf, svg a wmf (pouze ve Windows)
  • bitmapy: jpeg, tiff, png, bmp

Formát (výstupní zařízení) lze zadat přímo pomocí parametru device.

První rozhodnutí je, zda použít vektorový formát nebo bitmapu. Uložení do bitmapy znamená uložení “fotografie” obrázku. Takto uložený obrázek může být menší (přesněji nikdy nebude velký) a lze očekávat, že půjde vložit do libovolného dokumentu. Na druhou stranu nepůjde zvětšovat bez různých zkreslení a šumů a výstup nikdy nebude kvalitnější než v případě použití vektorové grafiky.

Vektorové formáty ukládají data, která popisují, kde co na obrázku je. Na rozdíl od bitmap je lze libovolně škálovat bez ztráty kvality a občas přijde vhod i možnost je upravovat (např. posunovat popisky u geom_label()/geom_text). Schopnost různých WYSIWYG editorů (např. MS Word atp.) pracovat s vektorovými formáty je navíc omezená a je lepší ji řádně otestovat. Nevýhodou je také jejich velká velikost v případě, že objektů v obrázku je mnoho nebo mají složité tvary.

Příkladem obrázku, u kterého se dramaticky liší velikost podle použitého formátu je následující mapa ČR s katastrálními územími obcí. Zobrazená bitmapa (png) má přibližně 0,1 MB. Stejná mapa ve vektorovém formátu má téměř 11,6 MB.

Mapa ČR s katastrálními územími obcí

Další parametry ggsave() umožňují přesné nastavení výstupu co do rozměrů a rozlišení (pouze pro bitmapy). Při vhodném nastavení těchto parametrů vzhledem k výsledné publikaci lze dosáhnout dobrých výsledků i při používání bitmap. Ve většině případů je však doporučeníhodné používání vektorových formátů.

Poznámka

Pokud opravdu záleří na velikosti výstupních souborů, je možné zvážit dvě cesty:

  • Zejména v případě map není zpravidla nutné pro vizualizace používat podrobnou mapu, která naprostu přesně kopíruje hranice (například) obce. Přesnost znamená spoustu bodů, které musí být ve výstupním formátu uloženy. Zkušenost ukazuje, že pro většinu aplikací postačí ponechat v mapě pouze 1 % bodů – tedy drasticky změnšit rozlišení mapy. Zjednodušení map velmi dobře umí rmapshaper::ms_simplify().
  • Pokud ukládáte výstup jako bitmapu, je možné optimalizovat barevnou paletu. Typicky není nutné mít dokonalé barevné přechody, které jsou opět datově náročné. Například formát png umožňuje indexování barevné palety, které může vést k drastickému nížení velikosti výstupního souboru.

14.11 Co dělat a co nedělat

ggplot2 a jeho rozšíření nabízí ohromnou škálu možností. Před tím než začnete kreslit je dobré se rozmyslet, zda je vůbec rozumné kreslit. Často je užitečnější a pro čtenáře srozumitelnější prezentovat data v podobě tabulky – a to zejména pokud je jich málo. Pokud se rozhodnete kreslit, potom se vždy musíte snažit, aby obrázek čtenáři srozumitelně komunikoval nezkreslenou informaci.

Tufte (2001) formuluje jednoduché doporučení, které byste měli mít vždy v paměti:

Graphical excellence is that which gives to the viewer the greatest number of ideas in the shortest time with the least ink in the smallest space.

14.12 Aplikace

Aplikace I

V tomto příkladu využijeme tabulku VGAMdata::oly12, která obsahuje údaje o účastnících Olympijských her v roce 2012.

oly12 <- VGAMdata::oly12 %>% 
    as_tibble()

print(oly12)
# A tibble: 10,384 × 14
   Name        Country   Age Height Weight Sex   DOB        PlaceOB  Gold Silver
   <fct>       <fct>   <int>  <dbl>  <int> <fct> <date>     <fct>   <int>  <int>
 1 Lamusi A    People…    23   1.7      60 M     1989-02-06 "NEIMO…     0      0
 2 A G Kruger  United…    33   1.93    125 M     NA         "Sheld…     0      0
 3 Jamale Aar… France     30   1.87     76 M     NA         "BEZON…     0      0
 4 Abdelhak A… Morocco    24  NA        NA M     1988-09-02 "AIN S…     0      0
 5 Maria Abak… Russia…    26   1.78     85 F     NA         "STAVR…     0      0
 6 Luc Abalo   France     27   1.82     80 M     1984-06-09 ""          0      0
 7 Maria Laur… Argent…    30   1.82     73 F     NA         ""          0      0
 8 Mohamed Ab… Morocco    23   1.87     75 M     1989-03-05 ""          0      0
 9 Emanuele A… Italy      27   1.9      80 M     1985-08-07 "Genov…     0      0
10 Ilyas Abba… Algeria    19   1.7      NA M     NA         "MEDEA…     0      0
# ℹ 10,374 more rows
# ℹ 4 more variables: Bronze <int>, Total <int>, Sport <fct>, Event <fct>

Z tabulky oly12 vytvoříme tabulku s průměrnou váhou a výškou pro skupiny vymezené sportem a pohlavím:

oly12_stats <- oly12 %>% 
    mutate(
        Sport = str_extract(Sport, "[:alnum:]*")
    ) %>% 
    group_by(Sex,Sport) %>% 
    summarise(
        Height = mean(Height, na.rm = TRUE),
        Weight = mean(Weight, na.rm = TRUE)
    ) %>% 
    ungroup() %>% 
    drop_na() 
`summarise()` has grouped output by 'Sex'. You can override using the `.groups`
argument.

Nyní data z tabulky oly12 vizualizujte. Chceme vidět vztah mezi váhou a výškou. Základním nástrojem pro zobrazení vztahu je bodový graf:

oly12 %>% 
   ggplot(
        aes(x = Height, y = Weight, color = Sex)
    ) +
    geom_point(alpha = 0.3) 
Warning: Removed 1346 rows containing missing values (`geom_point()`).

Tato vizualizace naznačuje, že v datech jsou dvě vzájemně posunutá mračna – ženy a muži. Úplně jasno o vztahu ale nemáme, kvůli obrovskému overplottingu.

V tomto případě může pomoci něco jako vícerozměrný histogram – např. geom_hex():

oly12 %>% 
   ggplot(
        aes(x = Height, y = Weight)
    ) +
    geom_hex()
Warning: Removed 1346 rows containing non-finite values (`stat_binhex()`).

Pro pohled na muže a ženy zvlášť je potom možné a užitečné použít facetování:

oly12 %>% 
    ggplot(
        aes(x = Height, y = Weight)
    ) +
    geom_hex() +
    facet_wrap("Sex", scales = "free")
Warning: Removed 1346 rows containing non-finite values (`stat_binhex()`).

Nyní můžeme rozšířit obrázek o dodatečnou datovou vrstvu – pozici průměrů pro jednotlivé sporty:

oly12 %>% 
    ggplot(
        aes(x = Height, y = Weight)
    ) +
    geom_hex() +
    geom_point(
        data = oly12_stats,
        aes(shape = Sport),
        size = 3,
        color = "red"
    ) +
    scale_shape_manual(values = c(65:90)) +
    facet_wrap("Sex", scales = "free")
Warning: Removed 1346 rows containing non-finite values (`stat_binhex()`).

Alternativně můžeme použít kernel density:

oly12 %>% 
  ggplot(
    aes(x = Height, y = Weight, color = Sex)
  ) +
  # geom_point(
  #   alpha = 0.1
  # ) +
  geom_density2d()
Warning: Removed 1346 rows containing non-finite values (`stat_density2d()`).

Aplikace II

Alternativní technikou zobrazení distribuce je dramatické snížení množství peředávaných informací. Na této myšlence je postaven tzv. boxplot. Porovnejte distribuci mužů a žen pomocí boxplotu.

load("data/ggplot2/village.RData")

village %>% 
  ggplot(
    aes(x = gender, y = age)
  ) +
  geom_boxplot()

Alternativou je využítí tzv. violin plotu.

village %>% 
  ggplot(
    aes(x = gender, y = age)
  ) +
  geom_violin()

Aplikace III

Základním nástrojem pro vizualizaci vztahů dvou spojitých proměnných je bodový graf. Použijte tabulku VGAMdata::oly12 a vykreslete vztah váhy a výšky sportovců z Francie. V obrázku také zobrazte věk a pohlaví sportovců.

oly12 <- VGAMdata::oly12 %>% as_tibble()

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  ggplot(
    aes(x = Height, y = Weight, size = Age, color = Sex)
  ) +
  geom_point(
    shape = 1
  ) +
  theme_classic()

Aplikace IV

Nyní na stejném vzorku rozlište sport, ve ktrém atleti závodí.

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  ggplot(
    aes(x = Height, y = Weight, fill = Sport)
  ) +
  geom_point(
    shape = 21,
    color = "white"
  ) +
  theme_classic()

Toto je klasický příklad zobrazení, ve kterém je zobrazeno příliš mnoho informací a výsledek je potom těžko srozumitelný. Méně je někdy více. Zkuste snížit počet kategorií.

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  mutate(Sport = as.character(Sport)) %>% 
  mutate(
    Sport = case_when(
      str_detect(Sport,"Cycling") ~ "Cycling",
      str_detect(Sport,"Canoe") | Sport == "Rowing" | Sport == "Sailing" ~ "Stuff with a boat",
      Sport %in% c("Basketball","Badminton","Football","Handball") ~ "Collective",
      Sport %in% c("Archery","Fencing","Judo","Shooting","Weightlifting","Wrestling") ~ "Fighting",
      Sport %in% c("Diving","Swimming") ~ "Watersports",
      str_detect(Sport,"Tennis") ~ "Tennis",
      Sport %in% c("Equestrian","Triathlon") ~ "Endurance",
      TRUE ~ Sport
    )
  ) %>% 
  ggplot(
    aes(x = Height, y = Weight, fill = Sport)
  ) +
  geom_point(
    shape = 21,
    color = "white",
    size = 4,
    alpha = 0.75
  ) +
  scale_fill_brewer(
    palette = "Paired"
  ) + 
  theme_classic()

Aplikace V

S pomocí stejných dat zkuste vykreslit vztah výška, váha, věk:

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  ggplot(
    aes(x = Height, y = Weight, size = Age)
  ) +
  geom_point(
    shape = 1
  ) +
  theme_classic()

Vztah s kategorií věk není příliš zřejmý… Zkusíme s tím něco udělat. Jednou možností je opět snížit množství informací – konkrétně rozdělit věk do jednotlivých kategorií a ty použít k obarvení.

Při nastavování kategorií pomůže pohled na histogram:

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  ggplot(
    aes(x = Age)
  ) +
  geom_histogram() +
  theme_classic()
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Jako celkem rozumné se zdá mít kategorie 0-20, 21-25, 26-30, 31-35 a 36+. Hint: cut()

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  mutate(
    age_cat = cut(Age, breaks = c(0,20,25,30,35,Inf))
  ) %>% 
  ggplot(
    aes(x = Height, y = Weight, fill = age_cat)
  ) +
  geom_point(
    shape = 21,
    color = "white",
    size = 4,
    alpha = 0.75
  ) +
  scale_fill_brewer(
    name = "Age",
    palette = "OrRd"
  ) + 
  theme_classic()

Věnujte, prosím, pozornost nastavení barev. Ty by měly odrážet charakter dat. Například *_brewer() funkce pracují s třemi druhy palet_

  • sequential (to je tento případ – kategorie tvoří logickou sekvenci, která jde v jednom směru)
  • diverging (typicky přechod přes 3 barvy – představte si klasickou teplotní mapu)
  • qualitative (mazi kategoriemi není žádný vztah)

Aplikace VI

Zůstaneme u stejné tabulky. Zkuste vykreslit distribuci váhy v jednotlivých věkových kategoriích.

Varianta 1

Můžete použít “zjednodušující” techniky jako boxplot nabo violinplot. Tyto techniky však mohou zkreslovat informaci co do počtu pozorování v každé kategorii. Někdy bývá užitečné kombinovat je s bodovým grafem

Zkuste následující obrázek. Pozor! Obrázek musí přesně komnikovat informaci čtenáři! (A zároveň pozor na overplotting.)

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  mutate(
    age_cat = cut(Age, breaks = c(0,20,25,30,35,Inf))
  ) %>% 
  ggplot(
    aes(x = age_cat, y = Weight)
  ) +
  geom_violin(
    color = NA,
    fill = "grey75"
  ) +
  geom_jitter(
    height = 0,
    width = 0.2
  ) +
  scale_x_discrete(
    name = "Age"
  ) +
  theme_classic()

Varianta 2

Tohle už samotný ggplot jendoduše nezvládne. Můžete se zkusit porozhlédout po rozšířeních: https://exts.ggplot2.tidyverse.org/gallery/

library(ggridges)

oly12 %>% 
  filter(Country == "France") %>% 
  drop_na(Height,Weight,Age,Sex) %>% 
  mutate(
    age_cat = cut(Age, breaks = c(0,20,25,30,35,Inf))
  ) %>% 
  ggplot(
    aes(x = Weight, y = age_cat)
  ) +
  geom_density_ridges() +
  scale_y_discrete(
    name = "Age"
  ) +
  theme_classic()
Picking joint bandwidth of 5.37