Shiny statt Javascript

Mit Javascript wird das Internet interaktiv. Das kann auch bei Datenanalysen wichtig sein. Mit Shiny klappt sowas auch in R – ganz ohne Javascriptkenntnisse. Zunächst ein Geständnis: Ich habe Probleme mit Javascript. Leider kann ich nicht genau begründen, warum – denn im Grunde finde ich die Sprache hervorragend in ihrer Flexibilität. Aber irgendwie komme ich mit der Syntax nicht ganz klar. Mein Glück: Ich darf oft mit super Entwicklern zusammenarbeiten, die JS im Schlaf schreiben können. Ich kann mich dann darauf konzentrieren, die Daten im Vorfeld bereitzustellen mit Python oder R. Immer wieder kommt es aber vor, dass wir Datenjournalisten den Fachredakteuren aus verschiedenen Ressorts großen Datenmengen zugänglich machen müssen. Während wir am schnellsten wären, wenn wir die Analysen einfach in R-Code schreiben können, brauchen die Kollegen was einfacheres. Und trotzdem sollten sie zu eigenen Erkenntnissen kommen können. Exceltabellen sind da oft nicht die ideale Lösung. In einem meiner letzten Großprojekte zu den Wanderungsbewegungen in München habe ich sehr viel mit R Markdown-Dokumenten gearbeitet, in denen die Redakteure Tabellen zu verschiedenen Themen durchsuchen konnten. Das war gut, aber ich will für die Zukunft andere Möglichkeiten ausprobieren. Da kommt mir ein Einfall von neulich ganz recht, um mal Shiny auszuprobieren. denn ich will meine Daten selbst interaktiv bekommen. Was ist Shiny? Shiny ist zunächst Mal ein Paket für R. Es ermöglicht, interaktive Web-Apps direkt in R zu programmieren, ohne dass die Daten von R rausgespeichert werden müssen und dann mit Javascipt (oder anderen Sprachen) weiterverarbeitet werden. Shiny verspricht viel Funktionalität: Ausgabe als HTML-Dateien, flexible Layouts und Themes, eigenes CSS oder interaktive htmlwidgets (auch eine R-Bibliothek). Meine Idee: Ein WM-Planer Ich stand neulich vor einem Problem: Ich wollte einen Termin Anfang Juli planen. Zwar hatte ich auf dem Schirm, dass da Fußball-WM ist. Aber wann genau Deutschland spielt, musste ich erst mühsam ergooglen. Meine Idee soll das vereinfachen: Eine kleine App mit zwei Auswahlfeldern für alle Mannschaften und ihr Abschneiden in der Vorrunde. Daraus lässt sich der komplette Weg ins Finale ableiten. Und es ist ein einfaches Beispiel für den Einsatz von Shiny mit ein wenig Interaktivität und einer überschaubaren Datenmenge. Die Vorarbeit An die Daten zu kommen ist kein Problem. Es gibt zahlreiche Möglichkeiten, den WM-Spielplan maschinenlesbar zu bekommen, etwa als JSON oder als CSV. Ich habe mir die Daten als CSV heruntergeladen und in R vorbereitet. Dazu gehört zum Beispiel, das Datum in das richtige Format für die interne Datenverarbeitung umzuwandeln (als sogenanntes POSIXct). Das geht gut mit der Bibliothek lubridate. Außerdem musste ich aus den Daten die jeweiligen Gruppen extrahieren (nicht Group XYZ, sondern nur XYZ). Das klappt gut mit dem Package stringR und dessen Funktion str_extract(). Der spektakulärste Teil ist das Umwandeln vom sogenannten langen Format der Daten in das weiten Format. Beim langen Format steht jede Beobachtung mit nur einer Variable in einer Zeile, die Daten werden quasi übereinander gestapelt. Beim weiten Format steht jede Spalte für eine Variable und jede Zeile für eine Beobachtung. Das funktioniert hier besser. Diese Umwandlung erledigt das Paket tidyR für uns. Mit der Funktion spread().library(tidyverse) library(lubridate) data <- read.csv("match_schedule.csv") data$Group <- as.character(data$Group) data$Home.Team <- as.character(data$Home.Team) data$Away.Team <- as.character(data$Away.Team) data$Date <- dmy_hm(data$Date, tz="Europe/Berlin") data %>% mutate(Group = str_extract(Group, "[A-Z]$")) %>% select(-Result) -> data teams <- unique(filter(data, Round.Number %in% c(1,2,3))$Home.Team) data %>% gather(key = "Place", value = "Team", Home.Team:Away.Team) -> data_long data_vorrunde <- list() for (i in seq_along(teams)) { data_long %>% filter(Team == teams[i]) %>% group_by(Team) %>% select(-c(Location, Place)) %>% spread(Round.Number, Date) -> d_current data_vorrunde[[i]] <- d_current } data_vorrunde <- bind_rows(data_vorrunde) colnames(data_vorrunde) <- c("Group", "Team", "Erste Runde", "Zweite Runde", "Dritte Runde")Die Vorarbeit ist beendet. An dieser Stelle speichere ich die Daten in R und mache kurz mit Excel weiter, um dort die Daten für die möglichen Wege durch die Finals einzufügen? Warum? Weil es schneller geht. Ich könnte theoretisch Filter bauen, um immer genau die Spiele zu treffen, bei denen ich bestimmte Daten für Achtel-, Viertel- und Halbfinale eintragen will. Denn meine Daten bisher enthalten logischerweise nur die Gruppenspiele, noch keine Finalspiele. Doch die kann ich auch schon vorhersehen, je nachdem, ob die Teams Gruppenerster oder Gruppenzweiter werden. Aber das dauert in R etwa genauso lange, wie die Arbeit in Excel. Da ich das nur einmal machen muss, bin ich mit einer optischen Tabellenverarbeitung auch zügig unterwegs. Ich suche mir also alle Spielpaarungen (unter anderem der Kicker hat da eine gute Übersicht), die die gleichen Termine haben (Gruppenerste von der einen Gruppe, Gruppenzweite von einer anderen) und füge die in die noch leeren Spalten ein, außerdem kopiere ich die Spiele und füge die Trennung Gruppenerster/Gruppenzweiter als Variable ein. Gespeichert wird das Ergebnis in Excel als CSV und dann wieder in R geladen. In R muss ich leider die Spalten wieder ins richtige Datumsformat bringen. CSVs speichern das nicht ab. (Ich könnte mir das also ganz am Anfang sparen) Damit ich das nicht für alle Spalten einzeln machen muss, nutze ich mutate_at(), das hier in den Spalten 3 bis 10 arbeitet (also diese Spalten modifiziert, wie ich das uns .funs eingebe):data_edited %>% mutate_at(.vars = c(3:10), .funs = dmy_hm, tz="Europe/Berlin") -> data_editedDanach bringe ich die bisher noch englischen Teamnamen mit einem Wörterbuch für die deutschen Begriffe zusammen. So haben wir hinterher die Teams in der deutschen Schreibweise:countries %>% read_csv2("countries.csv") data_edited %>% left_join(countries, by = c("Team" = "Englisch")) -> data_editedDas Ergebnis geht jetzt an die App. Ich speichere es als RDS-Datei. Ein R-Format. Die Shiny-App Eine Shiny-App besteht aus zwei Komponenten. Man kann die in zwei verschiedene Dateien schreiben, man kann sie aber auch kombinieren, was ich in diesem Beispiel mache. Das ist hier übersichtlicher. Ein guter Startpunkt für die Entwicklung mit Shiny ist das Tutorial bei RStudio, oder der Kurs bei Datacamp ($). Es ist anfangs ein bisschen kompliziert, geht aber nach einer kurzen Einarbeitungszeit ganz gut von der Hand. Zunächst werden – wie bei R üblich – die benötigten Pakete geladen. Außerdem lade ich hier unsere Daten von eben als Variable d. Die beiden Shiny-Komponenten sind die sogenannte UI und der Server. Beide muss ich definieren, und am Ende zur App kombinieren. Die UI In der Variable ui definiere ich das Aussehen der App. Vor allem, welche Eingabe und Ausgabefelder es geben wird. Außerdem kann ich hier einstellen, wie das Seitenlayout funktioniert und das Theme der App (mit der Einstellung theme = ). Die vorgefertigten Layouts basieren auf Bootstrap. Einem Layout, das von Twitter entwickelt wurde, und sich an die Displaygröße anpasst. Grundsätzlich könnten wir aber auch eigene Layouts verwenden. Das UI besteht aus verschiedenen Inputs, mit denen die Nutzer unsere Daten verändern können. Eine Auswahl: checkboxInput Einfachauswahl über Checkbox checkboxGroupInput Mehrfachauswahl mit Checkboxen dateInput Datum (geht auch mit dateRangeInput) fileInput Lädt eine Datei numericInput ein Nummerneingabefeld textInput ein Textfeld radioButtons Checkboxen mit runden Radiobuttons selectInput Auswahl über eine Dropdown-Liste. Benutzen wir hier. sliderInput Auswahl über einen Slider submitButton / actionButton können eine Funktion zugewiesen bekommen Wir definieren unsere UI mit zwei SelectInputs – Dropdown-Listen. Auch im HTML werden die mit dem Tag Select definiert, daher kommt wohl der Begriff in Shiny. Das Theme der App ist United, der Titel WM 2018-Planer. In der Sidebar lege ich die Dropdown-Menüs fest: SelectInput Nummer eins heißt teamname, womit ich ihn später im Server ansprechen kann. Hier kann man auch dem Menü einen eigenen Titel geben, dazu muss man die Auswahlmöglichkeiten definieren und man kann einen Wert davon als Startwert festlegen. Das gleiche mache ich mit den Vorrundenergebnissen. Im sogenannten MainPanel definiere ich den Output (mehr dazu unten) und schreibe einen helpText rein, der erklärt, was die App macht.ui <- shinyUI(fluidPage(theme = shinytheme("united"), # Application title titlePanel("WM 2018-Planer"), # Sidebar mit zwei Selectboxen sidebarLayout( # Hier beginnt das Layout sidebarPanel( selectInput("teamname", "Team", choices = unique(d$Deutsch), selected = "Deutschland"), # Hier wird der erste Input definiert. teamname ist die Variable, auf die wir im Server zugreifen selectInput("vorrunde", "Vorrundenergebnis", choices = unique(d$Status), selected = 1) # Hier wird der zweite Input definiert ), mainPanel( helpText("Welche Zeiten sollte man sich fürs WM-Schauen freihalten?"), htmlOutput(outputId = "dates") # hier wurde das Output definiert, das wir in der Serverfunktion berechnen ) ) )) Der Server Im Server gebe ich die Berechnungen ein, die die App ausführen soll. Außerdem lege ich fest, welche Ausgabe zu meinen in der UI definierten Bereichen ausgegeben wird. Wie bei den Inputs, gibt es auch hier Unterschiede bei den Outputs: klassische Tabellen Data Table interaktive Tabellen Fotos Graphen Konsolenausgaben Text HTML Weitere Infos gibt es in dem sehr übersichtlichen Shiny-Cheatsheet [PDF]. Ich nutze in meinem Beispiel einen HTML-Output, weil ich da flexibel HTML ausgeben kann, wie ich es möchte. Ich mache es mir hier sehr einfach, indem ich alles einzeln als HTML-Code einfüge. Theoretisch könnte ich mit Shiny auch auf die einzelnen HTML-Tags zugreifen. So sieht der Server-Bereich dann aus:server <- function(input, output) { output$dates <- renderUI({ # Hier wird festgelegt welches Element aus der UI wir verändern d %>% filter(Deutsch == input$teamname & Status == input$vorrunde) -> d_current # Hier ist die Berechnung mit den Daten, jedesmal wenn etwas neues ausgewählt wird. HTML( # Ab hier erfolgt die Ausgabe als reines HTML paste0("<h2>", input$teamname, "</h2>"), paste0("Die <b>Vorrundenspiele</b> in Gruppe ", d_current$Group, " finden statt am: ", "<br><br>"), paste(format(d_current$`Erste Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), format(d_current$`Zweite Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), format(d_current$`Dritte Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste0("Wird ", input$teamname, " ", input$vorrunde, " – dann finden die folgenden Spiele so statt:<br>"), paste0("<br>"), paste("<b>Achtelfinale</b>:", format(d_current$`Achtel`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Viertelfinale</b>:", format(d_current$`Viertel`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Halbfinale</b>:", format(d_current$`Halb`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Spiel um Platz Drei</b>:", format(d_current$`Platz Drei`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Finale</b>:", format(d_current$`Finale`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste0("<i>&copy; 2018 Benedict Witzenberger</i>") ) }) } Beides zusammen Ist dann nur noch eine Zeile:shinyApp(ui = ui, server = server)Fertig ist die App. Die nächste Frage ist dann: Wo spiele ich sie aus? Grundsätzlich gibt es zwei Möglichkeiten. Ich kann die vorgefertigte Hostinglösung von RStudio benutzen – in der Gratisvariante reicht das zum Testen aus. Will ich mehr Traffic oder mehrere Apps parallel, muss ich einen besseren Tarif buchen. Variante zwei ist das Selberhosten. Dafür muss ich auf meinem Webserver Shiny installieren. RStudio hat dafür eine Open Source-Variante des Shiny Servers. Für unseren Fall reicht aber das kostenlose Hosting von RStudio. Direkt im Programm RStudio kann ich die App mit meinem Account dort verknüpfen und sie deployen (hochladen). Die komplette Shiny-App sieht dann so aus:library(shiny) library(shinythemes) library(dplyr) d <- readRDS("Daten_final.rds") # Define UI for application that draws a histogram ui <- shinyUI(fluidPage(theme = shinytheme("united"), # Application title titlePanel("WM 2018-Planer"), # Sidebar with a slider input for number of bins sidebarLayout( sidebarPanel( selectInput("teamname", "Team", choices = unique(d$Deutsch), selected = "Deutschland"), selectInput("vorrunde", "Vorrundenergebnis", choices = unique(d$Status), selected = 1) ), mainPanel( helpText("Welche Zeiten sollte man sich fürs WM-Schauen freihalten?"), htmlOutput(outputId = "dates") ) ) )) # Define server logic required to draw a histogram server <- function(input, output) { output$dates <- renderUI({ d %>% filter(Deutsch == input$teamname & Status == input$vorrunde) -> d_current HTML( paste0("<h2>", input$teamname, "</h2>"), paste0("Die <b>Vorrundenspiele</b> in Gruppe ", d_current$Group, " finden statt am: ", "<br><br>"), paste(format(d_current$`Erste Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), format(d_current$`Zweite Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), format(d_current$`Dritte Runde`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste0("Wird ", input$teamname, " ", input$vorrunde, " – dann finden die folgenden Spiele so statt:<br>"), paste0("<br>"), paste("<b>Achtelfinale</b>:", format(d_current$`Achtel`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Viertelfinale</b>:", format(d_current$`Viertel`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Halbfinale</b>:", format(d_current$`Halb`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Spiel um Platz Drei</b>:", format(d_current$`Platz Drei`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste("<b>Finale</b>:", format(d_current$`Finale`, format = "%a., %d.%m.%y %H:%M Uhr"), sep = "<br>"), paste0("<br>"), paste0("<br>"), paste0("<i>&copy; 2018 Benedict Witzenberger</i>") ) }) } # Run the application shinyApp(ui = ui, server = server) Und so sieht das Ergebnis aus: Der WM-Planer (Das Laden dauert einen Moment) Der Beitrag Shiny statt Javascript erschien zuerst auf Benedict Witzenberger.

zum Artikel gehen

Eventbericht: Pokémon Go Fest 2023 Viele Pokémon, nur wenig Shiny

Die Redaktion hat es sich nicht nehmen lassen, hat sich ihre Pokébälle geschnappt und selbst ein paar der possierlichen Tierchen gefangen. Der Beitrag Eventbericht: Pokémon Go Fest 2023</br> <h4 class=secondary_title>Viele Pokémon, nur wenig S

zum Artikel gehen

Climb the (bed)rock with Python, Javascript and GO

Bedrock is now available in eu-central-1. It’s time to get real and use it in applications. Reading all blog posts about Bedrock, you might get the impression that Python and LangChain is the only way to do it. Quite the opposite! As Bedrock makes c

zum Artikel gehen

The Same for everyone: Lambda Function *and* Code with the same language via AWS CDK

The AWS CDK has been presented on reInvent 2018. Its mission is to simplify develop infrastructure as code. There are several other frameworks, as discussed here on the blog in tRick. So there are some competitors… Can you use shiny new CDK also fo

zum Artikel gehen

R can not be pushed in Production - deprecated!

Running Shiny on Fargate Some guys still thinking R cannot be used at scale or only in a limited way. I still do not understand the reason why people are like this. Since my last post about AWS Batch, which is a Docker-based service within AWS, which enab

zum Artikel gehen

Problemstoffsammlung am 3. Juli in Gaggenau und Loffenau - Hygieneanforderungen beachten

Am Samstag, den 3. Juli findet die Problemstoffsammlung in Gaggenau und Loffenau statt. Das Schadstoffmobil steht von 8:00 bis 12:00 Uhr in Gaggenau auf dem Parkplatz bei der Jahnhalle. Anschließend findet die Sammlung von 13:30 bis 14:30 Uhr in Loffenau

zum Artikel gehen