Courtesy of pxhere

gganimate

Animierte Grafiken in ggplot

In den bisherigen Abschnitten haben wir unsere Abbildungen und Grafiken auf drei Dimensionen beschränkt: x, y und die Farbe. Der immense Vorteil am fortschreitenden Übergang dahingehend, dass wir Forschungsergebnisse nicht mehr ausschließlich in Printmedien darstellen, ist, dass wir eine vierte Dimension öffnen können (die Zeit), um mehr Informationen in einer Abbildung zu vermitteln. Dabei ist die Grundidee, dass wir eigentlich nicht einen Plot erstellen, sondern stattdessen sehr viele, die als einzelne Bilder zu einem Film zusammengefügt werden. Zwei zentrale Fragen, die man sich stellen sollte, bevor man anfängt Daten zu animieren, sind dabei “Gewinnt der Betrachter bzw. die Betrachterin dabei zusätzliche Information?” und “Ist es den Aufwand wert?”.

Vorbereitende Schritte

Wie schon in den beiden vorherigen Abschnitten nutzen wir auch hier wieder den von uns vorbereiteten Ausschnitt aus den Gapminder Daten, der sich auf Bildungsausgaben und den UNDP Bildungsindex fokussiert. Wie auch bisher können wir dafür die Daten direkt von dieser Website laden:

load(url('https://pandar.netlify.com/post/edu_exp.rda'))

Naheliegenderweise benötigen wir auch in diesem Beitrag wieder ggplot2:

library(ggplot2)

Außerdem können wir die Theme und die Farbpalette aus dem letzten Abschnitt wieder benutzen. Dafür habe ich beide in einem .R-Skript festgehalten, sodass wir die Datei auf dieser Website direkt aus R ansprechen können:

source('https://pandar.netlify.com/post/ggplotting-theme-source.R')

Nach dieser kurzen Vorbereitung können wir uns direkt in die Animationen stürzen!

Informationsgewinn durch Animation

Nehmen wir für die Frage “Gewinnt der Betrachter bzw. die Betrachterin dabei zusätzliche Information?” mal ein Negativbeispiel:

Was wir hier sehen ist die animierte Variante dessen, was wir im ggplot Intro als “normalen” Plot erzeugt hatten. Hier werden die Ausgaben für das primäre Bildungssystem im Vergleich der fünf Länder (Vereinigtes Königreich, Frankreich, Italien, Spanien und Polen) über die Jahre zwischen 1998 und 2018 dargestellt. Die Abbildung hat zwar den Vorteil, die Spannung aufrecht zu erhalten, während wir uns wundern wie es wohl weitergeht, aber vergleichen wir diese Abbildung mal mit der ursprünglichen:

Im direkten Vergleich hat sie sogar einen wesentlichen Nachteil: wir haben wenig Gelegenheit, die gesamte Information zu betrachten, wenn wir im Jahr 2018 angekommen sind. Wir können zwar am Ende der Animation durch das 2-sekündige Standbild einen kurzen Überblick erhaschen, aber der Fokus wird vom Ersteller (in diesem Fall mir) auf die Entwicklung gelenkt und lässt kaum eine detaillierte Inspektion der Daten durch die Betrachtenden zu. In Dingen der Aufmerksamkeitslenkung kann diese Abbildung also durchaus nützlich sein. Wenn wir aber das Ziel haben, unsere Information sachlich und transparent zu vermitteln, gewinnen wir durch die Animation in diesem Fall nichts.

Das Problem liegt hier dabei, dass die Variable, die wir für die Animation benutzen (Year), bereits auf der x-Achse abgetragen ist. Wir gewinnen also keine vierte Dimension in den Daten, sondern nutzen die Erste doppelt. Stattdessen sollten wir uns auf Situationen konzentrieren, in denen wir durch die Animation eine weitere Variable in unsere Abbildung aufnehmen können, die für die Information, die wir zu vermitteln versuchen, einen Mehrwert hat.

Statische Grundlage

Nehmen wir das Beispiel aus dem Beitrag zur Verwendung von Themes und Farben. Dort hatten wir für ein spezifisches Jahr (2013) die Ausgaben für Grundschulbildung und den Education Index gegenübergestellt:

subset(edu_exp, Year == 2013) |>
  ggplot(aes(x = Primary, y = Index, color = Wealth)) +
    geom_point() +
    labs(x = 'Spending on Primary Eduction',
    y = 'UNDP Education Index',
    color = 'Country Wealth\n(GDP per Capita)') +
    ggtitle('Impact of Primary Education Investments', subtitle = 'Data for 2013') +
    theme_pandar() + scale_color_pandar()
## Warning: Removed 101 rows containing missing values (geom_point).

Zur Erinnerung: hier werden zuerst die Daten für 2013 mittels subset() ausgewählt und dann über die Pipe |> an ggplot() weitergereicht. Dort verwenden wir die Ausgaben (Primary) als x und den Bildungsindex (Index) als y. Zusätzlich färben wir die Punkte nach Wohlstandsgruppe des Landes (Wealth). Diese Daten stellen wir dann als Punkte dar (geom_point()). Mit labs() werden unsere drei Ästhetiken dann noch etwas detaillierter beschriftet und mit ggtitle() wird noch ein passender Titel hinzugefügt. Zu guter Letzt können wir mit theme_pandar() und scale_color_pandar() die optische Aufbereitung aufrufen, die wir im letzten Beitrag erstellt haben.

Wie sofort ersichtlich wird ist das Problem, dass die drei Dimensionen, die uns in einem statischen Plot zur Verfügung stehen (x, y und die Farbe), schon mit Informationen belegt sind. Für die Darstellung einer vierten Variable (der in Year festgehaltenen Zeit) können wir uns einer Animation bedienen. Wenn wir nämlich versuchen, alle Daten gleichzeitig darzustellen, wird das Ganze ein wenig unübersichtlich:

static <- ggplot(edu_exp, aes(x = Primary, y = Index, color = Wealth)) +
  geom_point() +
  labs(x = 'Spending on Primary Eduction',
  y = 'UNDP Education Index',
  color = 'Country Wealth\n(GDP per Capita)') +
  ggtitle('Impact of Primary Education Investments') +
  theme_pandar() + scale_color_pandar()
static
## Warning: Removed 2641 rows containing missing values (geom_point).

Mit faceting könnten wir versuchen das Problem zu lösen, allerdings geht uns dabei die intuitive Darstellung des Zeitverlaufs verloren:

static + facet_wrap(~ Year, scales = 'free')
## Warning: Removed 2641 rows containing missing values (geom_point).

Leider sehen wir schon hier, dass die Datenlage für 1997 sehr dünn ist und für das Jahr 2017 aus irgendeinem Grund vor allem die Informationen aus wohlhabenden Ländern fehlen. Daher schließen wir diese beiden Jahre für die Folgenden Animationen zunächst aus:

trimmed <- subset(edu_exp, Year < 2017 & Year > 1997)
static <- ggplot(trimmed, aes(x = Primary, y = Index, color = Wealth)) +
  geom_point() +
  labs(x = 'Spending on Primary Eduction',
  y = 'UNDP Education Index',
  color = 'Country Wealth\n(GDP per Capita)') +
  ggtitle('Impact of Primary Education Investments') +
  theme_pandar() + scale_color_pandar()

Grundidee von gganimate

Um uns Animationen in der relativ intuitiven Schreibweise von ggplot2 zu ermöglichen, hat Thomas Lin Pedersen das Paket gganimate erstellt. Dabei besteht die Grundidee des Pakets darin, aus einem ggplot anhand einer differenzierenden Variable mehrere ggplots zu machen. Letztendlich ist das Vorgehen also das Gleiche wie beim Faceting, nur dass wir hier die entstandenen Grafiken nicht in einem Raster anordnen, sondern einfach nacheinander anzeigen (und so die Illusion einer Animation erwecken). Dabei ist das Vorgehen recht einfach: wir erstellen mit ggplot() eine statische Abbildung und fügen dieser mit + einfach Befehle zur Erstellung von Animationen hinzu. Dabei werden in gganimate im Wesentlichen drei große Animationen unterschieden:

  • enter_-Befehle steuern das Verhalten, wenn Daten auftauchen. enter_fade z.B. bewirkt, dass eine Geometrie anhand des Übergangs von komplett durchsichtig zu komplett undurchsichtig im klassischen “fade-in” erscheint.
  • exit_-Befehle verhalten sich genauso, steuern aber das Verhalten, wenn Daten verschwinden.
  • transition_-Befehle steuern die Übergänge zwischen Datenzuständen. In unserem Fall z.B. sind die Daten von Spanien im Jahr 2008 und 2009 unterschiedlich:
subset(trimmed, Year %in% c(2008, 2009) & geo == 'esp')
##      geo Country Wealth Region Year Population Expectancy Income  Primary
## 1125 esp   Spain   High europe 2008   46068811      81.25  34164 20.00129
## 1126 esp   Spain   High europe 2009   46583566      81.66  32653 21.27594
##      Secondary Tertiary    Index
## 1125  25.37782 26.80364 61.33333
## 1126  27.61234 28.60976 62.00000

transition_-Befehle steuern, wie zwischen diesen Daten der Pfad interpoliert werden soll. enter_ und exit_ funktionieren im Grunde identisch zueinander, weswegen sie auch in einer gemeinsamen Hilfe dokumentiert sind, die mit ?enter_exit angesprochen werden kann. Das hat den Vorteil, dass wir dort alle Möglichkeiten auf einen Blick sehen. In den allermeisten Fällen werden mit diesen beiden Animationsarten aber Feinheiten geregelt, während der Kern der Animation über die Darstellung von Übergängen passiert.

Wenn wir uns für eine Animation des Übergangs zwischen Datenzuständen (in unserem Fall die Jahre) ausgesucht haben, muss das in mehrere Plots übersetzt und diese Plots am Ende zu einer Animation zusammengefügt werden. Dabei ist eins der gängigsten Formate, um animierte Bilder zu präsentieren, das gif (Graphics Interchange Format, gesprochen gif). Dieses Format ist nichts weiter als eine Aneinanderreihung von Bildern, die dann (meistens in Schleife) abgespielt werden. gganimate bietet aber eine Reihe verschiedener Renderer, mit denen unterschiedliche Datenformate erzeugt werden können (siehe ?renderers für eine Übersicht). Diese beiden Schritte übernimmt in gganimate die animate()-Funktion. Diese kann entweder direkt angewandt werden, um eine animierte Grafik in RStudio anzuzeigen oder - besser - um die Animation in einem Objekt abzulegen, die man dann mit anim_save() abspeichern kann. Hierbei macht besonders das Speichern Sinn, weil das Animieren von Grafiken mitunter mehrere Minuten in Anspruch nehmen kann. So aufwändig erstellte Plots durch einen Misclick zu verwerfen kann durchaus einiges an Frustrationen erzeugen (besonders wenn man einen Text wie diesen verfasst).

Zusammengefasst ist das Vorgehen also:

  1. statische Grafik mit ggplot() erstellen,
  2. mit transition_, enter_ und/oder exit_ die gewünschten Animationen hinzufügen,
  3. mit animate() die Animation vornehmen und
  4. zu guter Letzt mit anim_save() die Animation speichern.

Animierter Scatterplot: Erster Versuch

Um mit gganimate arbeiten zu können, muss das Paket natürlich installiert sein. Wenn wir unsere Animationen als gifs speichern wollen, kann es außerdem notwendig sein, gifski zu installieren, das den enstprechenden Renderer liefert:

install.packages('gganimate')
install.packages('gifski')

Für das Laden reicht es aber dann aus, nur gganimate aufzurufen, weil der Renderer nur intern angesprochen wird:

library('gganimate')

Weil die Darstellung von Zeitverläufen der häufigste Anwendungsfall für die Animation ist, hat sie in gganimate sogar eine eigene Übergangsfunktion: transistion_time(). Diese können wir benutzen, um die Übergänge zwischen verschiedenen Zeitpunkten in unseren Daten zu animieren. Den statischen Plot hatten wir oben bereits als static abglegt. Die Funktion nimmt natürlich die Zeitvariable als Argument entgegen, anhand derer die Animation betrieben wird:

fluid <- static + transition_time(Year)

Diese Animation können wir dann an animate() weitergeben, bevor wir schlussendlich mit anim_save() die Abbildung abspeichern:

animated <- animate(fluid)
anim_save('step1.gif', animated)

Das Ergebnis dieses Versuchs sollte jetzt so aussehen:

Animierter Scatterplot: Mehr Versuche

Noch ist das Ergebnis nicht wirklich zufriedenstellend. Der leichte Stroboskop-Effekt ist nicht so hübsch und einige der Verbindungen zwischen Punkten stammen zumindest augenscheinlich nicht von den gleichen Ländern. Der Grund für das zweite Problem liegt darin, dass der Datensatz für einige Länder recht “löchrig” ist, also bei weitem nicht in allen Jahren die Ausgaben und/oder der UNDP Bildungsindex ermittelt wurden. Wenn wir uns z.B. die Daten aus Indien ansehen, sehen wir direkt, dass die Daten nur in ausgewählten Jahren vorliegen:

subset(trimmed, geo == 'ind', select = c('Country', 'Wealth', 'Year', 'Primary', 'Index'))
##      Country     Wealth Year  Primary    Index
## 1661   India Lower Mid. 1998       NA 26.66667
## 1662   India Lower Mid. 1999 12.18634 28.00000
## 1663   India Lower Mid. 2000 14.84075 29.33333
## 1664   India Lower Mid. 2001 13.73107 30.00000
## 1665   India Lower Mid. 2002       NA 30.66667
## 1666   India Lower Mid. 2003 11.35044 31.33333
## 1667   India Lower Mid. 2004       NA 31.33333
## 1668   India Lower Mid. 2005       NA 32.00000
## 1669   India Lower Mid. 2006       NA 32.66667
## 1670   India Lower Mid. 2007       NA 33.33333
## 1671   India Lower Mid. 2008       NA 34.66667
## 1672   India Lower Mid. 2009  7.58993 35.33333
## 1673   India Lower Mid. 2010  7.49038 36.00000
## 1674   India Lower Mid. 2011  9.03030 36.00000
## 1675   India Lower Mid. 2012  9.40785 37.33333
## 1676   India Lower Mid. 2013  9.79172 38.66667
## 1677   India Lower Mid. 2014       NA 40.66667
## 1678   India Lower Mid. 2015       NA 42.00000
## 1679   India Lower Mid. 2016       NA 42.66667

Wie wir schon für ggplot2 gesehen haben, ist die Grundüberzeugung auch in der Umsetzung von gganimate, dass es leichter ist hier direkt Daten anzupassen, statt Veränderungen an der Darstellung vorzunehmen. Für unseren Fall heißt das, dass wir die Zeilen mit fehlenden Werten entfernen müssen. Dadurch werden die Übergänge zwischen den verbleibenden Jahren animiert, statt die Daten auszublenden. So würde für Indien dann der Übergang von 2003 zu 2009 als langsamer Verlauf zwischen (11.35, 31.33) und (7.59, 35.33) angezeigt werden.

nomiss <- subset(trimmed, !(is.na(Primary) | is.na(Index)))

subset(nomiss, geo == 'ind', select = c('Country', 'Wealth', 'Year', 'Primary', 'Index'))
##      Country     Wealth Year  Primary    Index
## 1662   India Lower Mid. 1999 12.18634 28.00000
## 1663   India Lower Mid. 2000 14.84075 29.33333
## 1664   India Lower Mid. 2001 13.73107 30.00000
## 1666   India Lower Mid. 2003 11.35044 31.33333
## 1672   India Lower Mid. 2009  7.58993 35.33333
## 1673   India Lower Mid. 2010  7.49038 36.00000
## 1674   India Lower Mid. 2011  9.03030 36.00000
## 1675   India Lower Mid. 2012  9.40785 37.33333
## 1676   India Lower Mid. 2013  9.79172 38.66667

Z.B. die Daten für Indien sind jetzt auf die wenigen Zeilen reduziert, in denen beide Variablen vorliegen, die wir auf x- und y-Achse abtragen.

Das zweite Problem - dass einige Übergänge eventuell zwischen Ländern stattfinden, die nicht die selben sind - müssen wir schon in der Erstellung des statischen Plots beheben. Bisher enthält unser ggplot keinerlei Angaben dazu, wie Daten des gleichen Landes einander zugeordnet werden können. Um hier sicher zu gehen, dass immer alle Punkte zueinander gehören, zwischen denen ein Übergang animiert wird, können wir mit dem group-Argument eine Gruppierungsvariable hinzufügen:

static <- ggplot(nomiss, aes(x = Primary, y = Index, color = Wealth, group = Country)) +
  geom_point() +
  labs(x = 'Spending on Primary Eduction',
  y = 'UNDP Education Index',
  color = 'Country Wealth\n(GDP per Capita)') +
  ggtitle('Impact of Primary Education Investments') +
  theme_pandar() + scale_color_pandar()

An der statischen Abbildung ändert sich dadurch nichts, es wird lediglich Information darüber hinterlegt, dass Country eine Gruppierung darstellt, die kennzeichnet, dass Zeilen das Datensatzes zueinander gehören. Mit diesen Anpassungen können wir einen zweiten Versuch starten:

fluid <- static + transition_time(Year)
animated <- animate(fluid)
anim_save('step2.gif', animated)

Auch das wirkt durch das abrupte Erscheinen und Verschwinden von einzelnen Datenpunkten noch sehr unruhig. Um das ein wenig abzuschwächen, können wir die vorhin erwähnten enter_ und exit_-Funktionen nutzen, um die Punkte allmälich ein- und auszublenden. Dazu nutze ich hier die _fade Variante der beiden:

fluid <- static + transition_time(Year) +
  enter_fade(alpha = .1) + exit_fade(alpha = .1)

Um nicht für jede Vorschau immer die gesamte Animation zu durchlaufen, können wir mit plot() auch einfach spezifische Frames ansehen:

plot(fluid, 2)

Hier sehen wir uns den 2. Frame der Animation an. Per Voreinstellung werden in gganimate 100 Frames bei 10 FPS (frames per second) erstellt, sodass die Standard-Animation stets 10 Sekunden lang ist. Wie wir sehen können, erscheinen hier gerade einige Punkte, während manche schon wieder verschwinden. In voller Umsetzung sieht das Ganze dann so aus:

Feinschliff-Funktionalität

Im letzten Abschnitt haben wir uns angesehen, wie Daten ein- und ausgeblendet werden und wie der Übergang zwischen verschiedenen Zuständen erzeugt werden kann. Um den Übergang zwischen den Zuständen noch zu verfeinern, können wir mit den shadow_-Funktionen arbeiten. Mit shadow_trail ergibt sich z.B. Folgendes:

fluid <- static + transition_time(Year) +
  enter_fade(alpha = .1) + exit_fade(alpha = .1) +
  shadow_wake(.5)

Um von der Preview-Funktionaliät Gebrauch zu machen:

plot(fluid, 44)

Wie man sieht, ist es sehr leicht möglich mit solchen Anpassungen die Abbildung “ein wenig” chaotisch werden zu lassen. Aber natürlich variiert das von Situation zu Situation - hier, wie bei allen Dingen in R, lohnt es sich einfach verschiedene Dinge auszuprobieren! Dazu bietet gganimate im Wesentlich drei “Schatten” shadow_mark(), shadow_trail() und das schon gesehene shadow_wake().

Über dieses explizite Hinzufügen von Übergangseffekten hinaus können wir auch bestimmen, wie zwischen zwei Situationen gewechselt wird. Im Moment gehen die Punkte linear ineinander über, aber über ease_aes() können wir eine vielzahl von Übergangsfunktionen definieren. Dabei geht es vor allem um die Beschleunigung, mit der Punktetransition stattfindet (linear, quadratic, cubic und so weiter). Jede der Funktionen kann entweder als Beschleunigung (-in) oder als Bremse (-out) oder als beides (in-out) benutzt werden. So können wir z.B. elastic-in benutzen, um diese Transition zu erzeugen:

fluid <- static + transition_time(Year) +
  enter_fade(alpha = .1) + exit_fade(alpha = .1) +
  ease_aes('elastic-in')

Die dritte Anpassung bezieht sich auf die Achsen. Generell ist es empfehlenswert, global fixierte Achsen zu nutzen, um die Darstellung der Daten vergleichbar zu halten. Nichtsdestotrotz sind sich verändernde Achsen bei manchen Abbildungen sehr beliebt und manchmal auch außerordentlich sinnvoll. Wenn wir z.B. die Entwicklung der Einwohnerzahlen der Länder betrachten würden, wäre es nicht sonderlich hilfreich, für das Jahr 1800 die gleich Skalierung wie für 2020 zu nutzen. Für unseren Plot können wir z.B. Folgendes tun:

fluid <- static + transition_time(Year) +
  enter_fade(alpha = .1) + exit_fade(alpha = .1) +
  view_follow()

Dadurch verändern sich während unserer Animation die Ober- und Untergrenzen der dargestellten x- und y-Skalen. Das wirkt besonders in unserem Fall etwas ungünstig, weil sehr extreme Werte nur kurz und vereinzelt auftauchen. Das liegt höchstwahrscheinlich an der Definition der Primary Variable, die am GDP relativiert wird und demzufolge bei Wirtschaftskrisen sehr schnell in die Höhe springt.

Eine letzte Option, mit der wir den Plot noch verfeinern können, ist mit der Kennzeichnung der aktuellen Situation. Im Verlauf unserer Animation vergehen Jahre, allerdings erfahren wir nirgendwo, welches Jahr wir aktuell sehen. Dazu hat gganimate eine eigene Funktionalität: die transition_-Funktionen geben alle spezifische Platzhalter aus, die wir in der Animation verwenden können (diese sind in der jeweiligen Hilfe immer unter “Label Variables” gelistet). Für das von uns genutzte transition_time() ist dieser Platzhalter frame_time, in dem die aktuelle Ausprägung der Variable hinterlegt wird, die wir als Zeitvariable genutzt haben. Diese können wir benutzen, um z.B. im Verlauf der Animation den Titel oder andere Beschriftungen anzupassen. In unserem Fall bietet es sich an, den Untertitel mit der Jahreszahl zu versehen. Dafür können wir wieder ggplot-Elemente hinzufügen. In diesem Fall überschreiben wir mit dem neuen ggtitle() den Titel, den wir bereits in static definiert hatten:

fluid <- static + transition_time(Year) +
  enter_fade(alpha = .1) + exit_fade(alpha = .1) +
  ggtitle('Impact of Primary Education Investments', 'Year: {frame_time}')

Nach der Animation durch animate() sieht diese Abbildung dann so aus:

Animations-Optionen

Alle Optionen, die wir bisher besprochen haben, beziehen sich auf neue Elemente, die dem Plot hinzugefügt werden. Über diese Dinge hinaus können wir in gganimate aber auch direkt an den Einstellungen der Animation als Ganzes herumbasteln. Wie schon erwähnt sind Animationen per Voreinstellung 100 Frames lang und laufen bei 10 FPS. In einigen Fällen können 10 Sekunden zu wenig sein, um die Gesamtheit der Daten angemessen darzustellen. Dafür haben wir drei Argumente, die wir in der animate-Funktion nutzen können, um die FPS und die Dauer der Animation nach unseren Vorlieben anzupassen:

  • nframes: Die Gesamtzahl der Frames.
  • fps: Frames pro Sekunde.
  • duration: Länge der Animation in Sekunden.

Wenn wir alle drei Argumente nutzen kann es zu widersprüchlichen Informationen kommen, sodass im Normalfall nframes und fps gesetzt werden. Wenn wir z.B. die Animationslänge bei gleichbeinden FPS verdoppeln möchten, können wir dies mit

animated <- animate(fluid,
  nframes = 200, fps = 10)

erreichen. Darüber hinaus können wir über das Argument renderer auch bestimmen, auf welche Art die Animation erzeugt und in welchem Format sie am Ende vorliegen soll. ?renderers gibt dabei eine Übersicht.

Eine Anpassung, über die wir bereits gesprochen haben, aber deren Umsetzung in gganimate bisher verheimlicht blieb, ist die Nutzung von Anfangs- und Endpausen, um einen besseren Eindruck von Ausgangslage und aktueller Situation zu vermitteln. Dafür können wir mit start_pause und end_pause die Anzahl der Frames festlegen, die wir in dieser Pause verbringen möchten:

animated <- animate(fluid,
  nframes = 200, fps = 10,
  start_pause = 20,
  end_pause = 20)

So ermöglicht die Animation jetzt, sich erst einen Eindruck von dem zu machen, was man im Jahr 1998 vorfindet und abschließend die Lage in 2017 zu beurteilen:

Bar Chart Races

Eine außerordentlich beliebte, sehr einfache Animation sind Bar Chart Races. Vor einiger Zeit erfreuten sich diese Animationen auch auf YouTube einer außerordentlichen Beliebtheit, weil man mit ihnen letztlich jede Konkurrenzsituation über die Zeit sehr übersichtlich darstellen kann (dieses 5-minütige Video mit einem Bar Chart Race der 15 wertvollsten Marken hat z.B. 9 Millionen Views). Wir können versuchen mit unseren neu gefundenen Kenntnissen über Plot-Animationen ein solches Rennen nachzubauen.

Zunächst benötigen wir zusätzlich zu unserer eigentlichen abhängigen Variablen (in diesem Beispiel nutzen wir den Education Index) eine Rangreihe, anhand derer wir dir Länder sortieren können. Allerdings muss diese Rangreihe innerhalb der einzelnen Jahre ermittelt werden, sodass wir etwas umständlich mit ave() arbeiten müssen. Eine beliebte Alternative zur Erstellung von Werten in gruppierten Daten stellt die group_by-Funktionalität im Datenmanagement-Paket dplyr dar.

edu_exp$Index_Rank <- ave(-edu_exp$Index, edu_exp$Year, FUN = function(x) rank(x, ties.method = 'first'))

Wir bilden hier also für den negativen Index (damit die Rangreihe den Ländern mit dem höchsten Index den niedrigsten Rangplatz, also die 1, zuweist) innerhalb der einzelnen Jahre (Year) mit der Funktion rank() eine Rangreihe, bei der Gleichstände alphabetisch aufgelöst werden. Für das Jahr 1997 bekommen wir also z.B. folgende Top 10:

subset(edu_exp, Index_Rank < 11 & Year == 1997)
##      geo        Country Wealth   Region Year Population Expectancy Income
## 169  aus      Australia   High     asia 1997   18387205      78.63  32055
## 1135 est        Estonia   High   europe 1997    1409678      70.17  13733
## 1282 gbr United Kingdom   High   europe 1997   58307320      77.24  30340
## 1765 isr         Israel   High     asia 1997    5566551      78.11  25054
## 2752 nor         Norway   High   europe 1997    4420266      78.27  54769
## 2815 nzl    New Zealand   High     asia 1997    3752355      77.19  26212
## 2941 plw          Palau   High     asia 1997      18016         NA  13722
## 3466 svn       Slovenia   High   europe 1997    1987457      75.15  20094
## 3487 swe         Sweden   High   europe 1997    8868857      79.37  32441
## 3907 usa  United States   High americas 1997  271713634      76.58  41463
##       Primary Secondary Tertiary    Index Index_Rank
## 169        NA        NA       NA 78.66667          3
## 1135 18.12812        NA 38.25692 73.33333         10
## 1282       NA        NA       NA 76.66667          6
## 1765       NA        NA       NA 78.66667          4
## 2752 37.94443  18.13375 46.01137 79.33333          2
## 2815 17.44218  21.57709       NA 77.33333          5
## 2941       NA        NA       NA 74.00000          8
## 3466       NA        NA       NA 76.00000          7
## 3487       NA        NA       NA 74.00000          9
## 3907       NA        NA       NA 84.66667          1

Damit wir unserer Abbildung auch die Information aus den Farben nicht ungenutzt lassen, können wir in dieser die Regionen kodieren. Dafür müssen wir diese, wie schon mit Wealth passiert, in einen schön gelabelten factor umwandeln:

edu_exp$Region <- factor(edu_exp$Region,
  levels = c('europe', 'asia', 'americas', 'africa'),
  labels = c('Europe', 'Asia', 'Americas', 'Africa'))

Weil wir in unserem Plot nicht alle Länder der Erde gleichzeitig darstellen wollen, sondern ein “Rennen” darstellen, beschränken wir unsere Daten auf die Top 10 pro Jahr:

top10 <- subset(edu_exp, Index_Rank < 11)

Unseren statischen Plot können wir wieder stückweise erstellen. Zunächst die Grundkomponenten:

static <- ggplot(top10,
  aes(y = Index_Rank, x = Index, fill = Region, group = Country))

Auf der y-Achse werden also die Ränge abgetragen, auf der x-Achse der tatsächliche Wert. Die Färbung soll den vier Regionen entsprechen und damit wir die Zuweisung der Datenpunkte über die Zeit zueinander korrekt machen, nutzen wir wieder das Land als Gruppierungsvariable.

static <- static + 
  geom_rect(aes(ymin = Index_Rank - .4, ymax = Index_Rank + .4,
  xmin = 0, xmax = Index))

Weil es sich um Bar Chart Race handelt, nutzen wir geom_rect() um Rechtecke zu erstellen. Dieser werden durch die in ggplot() schon von uns festgelegte y-Ästhetik auf der y-Achse and ihrem Rang platziert - hier wird zudem festgesetzt, dass sie +/- 0,4 Ränge um ihren Rang breit sein sollen. Darüber hinaus sollen sie auf der x-Achse von 0 bis zu ihrer Index Ausprägung gehen.

static <- static +
  geom_text(hjust = 'right', aes(label = Country), color = 'white')

Damit wir in der Animation erkennen, welches Land welches ist, beschriften wir die Datenpunkte an den Stellen x (Index) und y (Index_Rank) mit einem Label, in diesem Fall der Ausprägung der Variable Country. Damit der Text vollständig im Balken liegt machen wir ihn mit hjust = 'right' rechtsbündig.

static <- static +
  scale_x_continuous(expand = c(0, 0)) +
  scale_y_reverse()

Damit die Balken bis an die eingezeichnete x-Achse gehen, setzen wir die voreingestellte “Erweiterung” des Plots nach links und rechts außer Kraft. Außerdem invertieren wir die y-Achse, damit die Länder auf dem 1. Platz oben und die auf dem 10. Platz unten stehen.

static <- static +
  ggtitle('Top Spots in the Education Race') +
  labs(y = element_blank(), x = 'UNDP Education Index') +
  theme_pandar() + scale_fill_pandar(drop = FALSE) +
  theme(axis.text.y = element_blank())

Dann, zu guter Letzt, unsere Beschriftungen der Achsen und Titel. Die y-Achse lassen wir unbeschriftet (wir weisen das leere Element zu). Außerdem verwenden wir noch die von uns definierte Theme und die Farbskala. Bei Letzterer stellen wir noch ein, dass wir Regionen nicht aus der Farbzuweisung entfernen möchten, wenn sie nicht auftauchen. Zum Abschluss unterdrücken wir die voreingestellte Kennzeichnung der Werte an der y-Achse (auch dafür überschreiben wir was auch immer passieren würde einfach mit dem leeren Element in element_blank()).

Das Balkendiagramm mit sich überlagenden Daten für alle Jahre sieht vorhersehbar chaotisch aus, sodass wir besser wieder das Faceting nutzen, um uns einen kurzen Überblick zu verschaffen:

static + facet_wrap(~ Year, scales = 'free')

Anders als im letzten Beispiel nutzen wir nicht transition_time(), um unsere Animation zu betreiben, sondern stattdessen transition_states(). Das liegt daran, dass unsere Abbildung nicht Verläufe von Datenpunkten abbildet, sondern zwischen distinkten Zuständen wechselt. Diese Funktion erlaubt uns außerdem festzulegen, wie lange in einem Zustand (bei uns das Jahr) verweilt werden soll (state_length), im Verhältnis zu der Animation der Übergänge (transition_length). Damit die Übergänge flüssig aussehen, verwende ich hier ein Verhältnis von 1 zu 30. Außerdem können wir über wrap = FALSE festlegen, dass die Animation nicht am Ende versuchen soll in den Ausgangszustand zurückzukehren.

fluid <- static + 
  transition_states(Year, transition_length = 30, state_length = 1,
    wrap = FALSE)

Wie schon beim letzten Beispiel bietet es sich an, die aktuelle Jahreszahl im Verlauf der Animation einzublenden. Bei transition_states() wird der dafür benötigte Platzhalter closest_state genannt:

fluid <- fluid + 
  ggtitle('Top Spots in the Education Race', subtitle = 'Year: {closest_state}')

Abschließend müssen wir noch entscheiden, was mit Balken von Ländern passieren soll, die entweder neu in die Top 10 einsteigen oder aus dieser herausfallen. In beiden Fällen macht es Sinn, dass die Balken von unten kommen, bzw. nach unten verschwinden:

fluid <- fluid + 
  enter_drift(y_mod = -1) + exit_drift(y_mod = -1)

Wir können auch gleichzeitig mehrere enter_ und exit_ Animationen miteinander verbinden. Z.B. können die nach unten verschwindenden Balken auch verblassen und die neu entstehenden langsam an Farbe gewinnen:

fluid <- fluid + 
  enter_fade(alpha = .1) + exit_fade(alpha = .1)

Wenn wir uns jetzt z.B. den 12. Frame der Animation ansehen, ergibt sich folgendes Bild:

plot(fluid, 12)

Soweit, so gut. Diese Grafik können wir wieder an animate() übergeben. Weil die Animation der ein- und austretenden Balken und der Positionswechsel möglichst flüssig aussehen sollen, erhöhe in diesem Fall die FPS auf 20. Außerdem füge ich am Ende 1 Sekunde Pause ein und verlängere die Gesamtanimationsdauer auf 15 Sekunden, damit die Animation nicht zu hektisch wird:

animated <- animate(fluid,
  fps = 20, duration = 15,
  end_pause = 20)

Abschließend sieht die Animation so aus:


Die hier entstandenen Grafiken sind zwar animiert, aber erlauben dennoch keine Datenexploration, die unabhängig von der lenkenden Hand des Erstellers oder der Erstellerin ist. Wie solche Grafiken erstellt werden können sehen wir im kommenden Abschnitt zu plotly.

Martin Schultze
Koordination