Best Practice: Entwicklung robuster Shiny Dashboards als R-Pakete
Dieser Blogartikel widmet sich dem Erstellen von Dashboards. Dashboards eignen sich hervorragend als interaktives Tool zur Veranschaulichung von Rohdaten, aggregierten Informationen und analytischen Ergebnissen. Bei der Entwicklung von Softwarelösungen mit R nutzen wir bei INWT das Paket shiny von RStudio.
Mit shiny kann man Apps erstellen, die als eigenständige Webseite fungieren, oder interaktive Elemente, die in Reports eingebunden werden können. Zusätzlich zu den Basisfunktionen von R und shiny stehen zur Entwicklung von Dashboards tausende R-Pakete zur Verfügung - mit Nutzern aus unterschiedlichsten Bereichen.
Shinys Beliebtheit ist in dessen Flexibilität begründet. Sowohl einfache und schnell zu entwickelnde Anwendungen als auch komplexe interaktive Apps mit maßgeschneiderten CSS- und Javascript-Elementen sind möglich. So ist die Verwendung von shiny-Dashboards attraktiv für Unternehmen jeder Größe.
Eine große und aktive R-Community bietet viele Tutorials zum Erlernen von shiny. Zum Einstieg eignen sich folgende Seiten:
- http://shiny.rstudio.com/tutorial/ stellt umfassendes Material zur Verfügung, das sich als Einstieg in die Shiny-App-Entwicklung eignet
- http://shiny.rstudio.com/articles/ stellt Artikel vor, die weiterführende Funktionen und Entwicklungsmöglichkeiten beschreiben
- https://github.com/rstudio/shiny-examples bietet eine Übersicht an Beispiel-Apps zur Inspiration
Dieser Blogartikel soll kein weiteres Einsteiger-Tutorial werden. Stattdessen präsentieren wir hier INWTs Best Practices für das Entwickeln von robusten und automatisiert testbaren shiny Dashboards. Insbesondere wenn eine shiny App anwächst, steigt die Unübersichtlichkeit des Codes und es entstehen vermeidbare Fehlerquellen.
Wir haben ein Beispiel erstellt, anhand dessen wir gerne unsere Best Practices der shiny-Entwicklung veranschaulichen möchten. Dabei handelt es sich um einfache deskriptive Analysen von E-Commerce-Daten, die Fragen beantworten wie etwa: Welche Produkte verkaufen sich am besten? Um welche Uhrzeit wird am häufigsten gekauft? Wie entwickelt sich der Umsatz seit Beginn des Jahres?
Wäre dies ein realer Kundenauftrag, ließe sich dieses Dashboard noch um Prognose-Funktionalitäten, wie z.B. eine Kundensegmentierung oder Absatzprognosen erweitern. Der folgende Screenshot zeigt das fertige Dashboard.
Shiny Dashboard des ecomAnalytics Paketes
Das Dashboard haben wir über die kostenfreien und öffentlich zugänglichen Server von RStudio gehostet. Es kann unter folgendem Link gefunden werden: https://inwtlab.shinyapps.io/ecomanalytics/
Den Code zu diesem Dashboard stellen wir auf GitHub zur Verfügung: https://github.com/INWTlab/ecom-analytics
Die darin analysierten Beispieldaten stammen von einem britischem Händler für Geschenke: https://archive.ics.uci.edu/ml/datasets/online+retail
Best Practices: shiny Dashboard-Entwicklung als eigenständiges R-Paket
Das einfache Programmieren eigener R-Pakete bietet Entwicklern wie Anwendern viele Vorteile und stellt einen Hauptgrund für den hohen Stellenwert der Statistikumgebung R innerhalb der Data-Science-Community dar. Vorteile, die sich durch eine individuelle Paketentwicklung ergeben, lassen sich gut auf die Anwendung von shiny Dashboards übertragen. Die folgende Liste nennt einige Argumente für die Entwicklung eines shiny Dashboards innerhalb eines R-Paketes:
Anwendung:
- Installation und Updates erfolgen automatisiert
- Pakete können Beispieldaten enthalten, die die Funktionalität des Dashboards präsentieren
Entwicklung:
- Einhaltung formeller wie inhaltlicher Qualitätsstandards durch eine testgetriebene Entwicklung
- Einfache Distribution von R-Paketen: Innerhalb des Paketes werden Abhängigkeiten definiert, welche mitinstalliert werden
- Umfangreiche Dokumentationsmöglichkeiten von Funktionen und Modulen
- Versionskontrolle
- Wiederverwendung von Funktionen und Modulen für weitere Pakete
Zunächst vergleichen wir die Datenstruktur einer herkömmlichen shiny App mit unserer Best Practice Struktur. Hier fällt bereits der unterschiedliche Umfang der Dateistrukturen auf - ein Indiz der unterschiedlichen Möglichkeiten, die beide Varianten bieten.
Dateistrukturen: einfache shiny App (links) vs. shiny App im Paket(rechts)
Die einzelnen Paketbestandteile übernehmen dabei folgende Aufgaben:
ecomAnalytics
ist der Name unseres Beispielpakets.DESCRIPTION
enthält generelle Informationen des Pakets, wie zum Beispiel den Autor und Maintainer, eine Paketbeschreibung oder die Distributionslizenz. Außerdem werden in dieser Datei Pakete und deren Versionsnummern definiert, die innerhalb des Projekts importiert und geladen werden sollen.Namespace
ist der Ort, an dem Paket-Abhängigkeiten abgestimmt werden. Die Datei wird durch roxygen2 automatisiert erstellt und listet die zu importierenden und exportierenden Pakete und Funktionen.inst
bietet einen Ort für Dateien, die dem installierten Paket unverändert hinzugefügt werden sollen. Hier befinden sich die Basis-Dateien einer shiny App.R
bietet ein Verzeichnis für zusätzliche R-Dateien. Funktionen, die innerhalb von Server.R oder UI.R aufgerufen werden sind hier definiert.tests
enthält die Bestandteile, die das R-Paket testthat zum automatisierten Testen benötigt. Dazu gehören test-kpiCalculations.R innerhalb des Ordners testthat. Hier sind die eigentlichen Tests definiert. testData beinhaltet Daten, auf die innerhalb des Tests zugegriffen wird. testthat.R wird automatisch erstellt und legt fest, in welchem Verzeichnis die Tests die ausgeführt werden.data
enthält Daten, die zusammen mit der shiny App distribuiert werden. Auf diese Daten kann innerhalb des shiny Dashboards zugegriffen werden.man
beinhaltet Dokumentationsdateien, die den Nutzenden über Funktionalitäten der shiny App aufklärt
Best Practices: shiny-Development-Workflow
Besonders beim Strukturieren von shiny Apps in Paketform ist es oft nicht ersichtlich, welche Reihenfolge der Entwicklung die richtige ist. Es gibt nicht den einen richtigen shiny-Development-Workflow, doch für uns bei INWT hat sich folgender Best-Practice-Workflow bewährt.
Einmalig:
1. R-Paket erstellen
Als erster Schritt wird ein R-Paket erstellt. Dies lässt sich innerhalb von R einfach mit den folgenden Befehlen durchführen.
usethis::create_package("ecomAnalytics")
Anschießend wird die DESCRIPTION
-Datei ausgefüllt und zusätzlich benötigte Verzeichnisse, wie inst oder data manuell hinzugefügt. Mit der folgenden Zeile wird ein Test-Verzeichnis plus Test-Datei erstellt, die wir für ein robustes Dashboard benötigen.
usethis::use_test("kpiCalculations")
Ziel ist die Erstellung einer Struktur, wie die rechte Seite des obigen Schemas. Noch fehlende Dateien mögen manuell hinzugefügt werden.
2. Struktur und Wechselwirkung der Dateien verstehen
Bevor es an das Programmieren eigentlicher Inhalte geht, soll verstanden werden, welchen Zweck die einzelnen Dateien haben und in welcher Wechselwirkung sie miteinander stehen. Bleiben wir dabei beim Beispiel des Ecommerce Analytics Dashboards. Die obige Übersicht setzt die Dateien des ecomAnalytics-Pakets in einen inhaltlichen Kontext.
Die beiden wichtigsten Elemente einer App sind UI.R
und Server.R
. Aus diesen beiden Dateien wird letztlich das Dashboard erstellt. In UI.R
wird, wie der Name schon sagt, das User Interface
definiert. In Server.R
erfolgt jegliche Berechnung des shiny Dashboards.
Ist der Umfang zu erstellenden Projekts klein kann es genügen, den gesamten Code in diesen beiden Dateien unterzubringen. Steigt der Funktionsumfang jedoch wird diese Variante sehr schnell unübersichtlich, schlecht zu warten und fehleranfällig. Daher machen wir von einer erweiterten Struktur Gebrauch. Dabei folgen wir dem Prinzip, jegliche Logik und komplexe Code-Abschnitte aus UI.R und Server.R herauszuhalten. Dort sollen nur Funktionen aufgerufen werden, welche aus Hilfsdateien geliefert werden. Das steigert die Übersichtlichkeit innerhalb von Projekten enorm.
uiElements.R
beinhaltet einzelne Bausteine des User Interfaces, die in der UI.R
anschließend aufgerufen werden. styleDefinitions.css
ist ein Ort, an dem alle Farb-, Schrift-, und Design-Optionen definiert werden, die von den Default-Werten abweichen sollen.
Hinter Server.R
angeordnet findet sich z.B. kpiCalculations.R
. Diese exemplarische Datei ist nach einer Gruppe von Funktion benannt, die sie enthält. Aufgerufen werden diese Funktionen innerhalb von Server.R. Für jede Gruppe von Funktionen bietet es sich an eine Test-Datei mit analogem Namen zu erstellen, z.B. test-kpiCalculations.R
. Hier werden Tests zu den jeweiligen Funktionen geschrieben, die ein einwandfreies Funktionieren garantieren.
Erweitert sich nun der Funktionsumfang eines Projekts nimmt zwar die Menge an Hilfsdateien zu, die zentralen Dateien bleiben jedoch, und das ist das entscheidende, übersichtlich.
Mehrmalig:
Der folgende Zyklus wird für eine Funktion oder eine Gruppe zusammengehöriger Funktionen durchlaufen. Hat man den Workflow ein Mal durchlaufen beginnt er erneut ab diesem Schritt mit einer neuen Funktionalität. Der Prozess endet sobald alle gewünschten Funktionen implementiert sind und kann leicht wieder aufgenommen werden, sobald die App erweitert werden soll.
3. Visuelles Grundgerüst bauen
In inst/app/UI.R
wird begonnen das visuelle Grundgerüst aufzusetzen. Für das Erstellen von Dashboards wird das Paket shinydashboard verwendet. Wir unterteilen die Datei dafür in drei Teile: einen Header, eine Sidebar und einen Body. Die drei Tabs, die das ecomAnalytics-Beispiel zeigt finden sich im Body.
Der dazugehörige Code sieht wie folgt aus:
#UI.R
dashboardPage(
header = panelTitle(...), #*
sidebar = dashboardSidebar(panelSelectInput, ...), #*
body = dashboardBody(...,
tabPanel("Shop Level Analytics", value = "tab1", #*
shopLevelKpis(), #*
shopLevelProductRanking(), #*
shopLevelTimeAnalysis(), #*
...),
tabPanel("Individual Level Analytics", value = "tab2", #*
...),
tabPanel("Raw Data", value = "tab3", #*
...)
)
)
Die meisten Funktionen und Tabs, die hier aufgerufen werden, wurden bisher noch nicht definiert (mit '#*' markierte Zeilen). Unser Workflow sieht vor, sich zunächst gedanklich zu überlegen, welche Bestandteile das Dashboard haben soll, diese hier zu benennen und anschließend zu definieren.
Damit UI.R
, insbesondere bei großen Projekten, übersichtlich bleibt lagern wir die Definitionen dieser ui-Elemente aus.
4. UI Elemente erstellen
In R/uiElements.R
werden nun die Elemente definiert und exportiert, welche in UI.R aufgerufen werden. Hierbei handelt es sich um das Äußerliche, wie der Name vermuten lässt. Von den Platzhaltern, die in Schritt 3 für Funktionen benutzt wurden, soll nun die Funktion shopLevelKpis()
exemplarisch definiert werden.
#uiElements.R
#' @export
#' @rdname uiElements
shopLevelKpis <- function() {
fluidRow(
h4("Key Performance Indicators"),
box(width = 12,
infoBoxOutput('revenueKpi', width = 4),
infoBoxOutput('customersKpi', width = 4),
infoBoxOutput('numProductsKpi', width = 4)
))
}
Hier wurde also definiert, shiny_ das Element shopLevelKpis
eine Reihe, bestehend aus drei Info-Boxen, sein soll. Die eigentliche Berechnung fehlt noch und soll im nächsten Schritt erfolgen.
5. Serverstruktur definieren
inst/app/Server.R
ist der Ort für jegliche Berechnungen einer shiny App. Zunächst sollen zu jedem ui-Element Funktionen geschrieben werden, um ein technisches Funktionieren der App zu ermöglichen. Dabei geht es noch nicht darum inhaltlich richtige Funktionen zu schreiben.
Passend zu dem zuvor definierten ui-Element shopLevelKpis()
sollen nun die benötigten Gegenstücke definiert werden.
#Server.R
getRevenueKpi <- reactive({
revenue <- 1000000
revenue
})
output$revenueKpi <- renderPrint({
revenue <- getRevenueKpi()
infoBox(title = "Total Revenue", revenue, icon = icon("dollar"),
color = "black", width = 12)
})
Zunächst wird eine reaktive Funktion getRevenueKpi()
, mit einem fiktiven Wert von 1 Mio. definiert. Innerhalb der Render-Funktion wird die reaktive Funktion aufgerufen. In infoBox()
werden letzte Details definiert.
Über output$revenueKpi
lässt sich der fiktive Revenue-Wert nun abrufen. Im Anschluss kann die inhaltlich richtige Berechnung programmiert werden.
Anmerkung: Ebenfalls möglich ist, an diesem Punkt zurück zu Schritt 3 zu springen und zunächst alle Funktionen mit fiktiven Werten zu entwickeln und erst im Anschluss mit den Schritten 5-7 fortzufahren.
6. Inhaltliche Funktionalität erstellen
Für inhaltlich korrekte Ergebnisse werden zunächst Daten eingeladen. In unserem Beispiel übernimmt das die reaktive Funktion getRawData()
:
#inst/app/Server.R
getRawData <- reactive({
req(input$file1)
ecomData <- read.csv(input$file1$datapath,
header = input$header,
sep = input$sep)
ecomData
})
Anschließend wird die bereits erstellte Funktion getRevenueKpi()
verbessert. Daten werden aufgerufen und als ecomData-Objekt gespeichert. Das Daten-Objekt wird wiederum als Input für calcRevenueShop()
verwendet, eine ausgelagerte Funktion, die den Umsatz des gesamten Shops berechnet und revenue zurückgibt.
#inst/app/Server.R
getRevenueKpi <- reactive({
if(!is.null(input$file1)){
ecomData <-getRawData()
revenue <- calcRevenueShop(ecomData)
} else {
revenue <- '0'
}
})
calcRevenueShop()
selbst ist nicht sonderlich kompliziert und befindet sich in der Hilfsdatei R/kpiCalculations.R
.
#R/kpiCalculations.R
#' @export
#' @rdname kpiFunctions
calcRevenueShop <- function(ecomData) {
format(round(ecomData %>% select('Sales') %>% sum()), big.mark = ' ')
}
Die Render-Funktion, die output$revenueKpi
erstellt kann unverändert bleiben.
#inst/app/Server.R
output$revenueKpi <- renderPrint({
revenue <- getRevenueKpi()
infoBox(title = "Total Revenue", revenue, icon = icon("dollar"),
color = "black", width = 12)
})
In diesem Sinne wird jede bereits technisch funktionierende Funktion nun auch inhaltlich sinnvoll gestaltet. Nach Abschluss dieser Phase, hat man bereits eine shiny App die funktioniert und gültige Ergebnisse produziert.
Lediglich in den Aspekten Robustheit und Style gibt es noch Potenzial. Diesen widmen sich die nächsten beiden Schritte des Best-Practice-Workflows.
7. Unit Tests schreiben
Das Verwenden von automatisierten Tests in R-Paketen ist ein wichtiger Teil des Entwicklungsprozesses. Das Schreiben automatisierter Tests bedeutet kurzfristig zwar Mehraufwand, minimiert langfristig jedoch Debugging-Arbeiten und garantiert robuste shiny Dashboards - vor allem bei Dashboards mit hoher Komplexität ist dies entscheidend. Best Practices automatisierter Tests im shiny-Kontext decken im Wesentlichen zwei Bereiche ab: formelle Tests und inhaltliche Tests.
Erstere überprüfen die technische Funktionalität, wohingegen letztere garantieren, dass Outputs inhaltlich plausibel sind. Test 1 garantiert, dass kein leeres Objekt ausgegeben wird, Test 2 kontrolliert, dass das gewünschte Format übermittelt wird (ohne Dezimalstellen) und Test 3 checkt, dass revenue
eine positive Zahl ist, da ein negativer Umsatz nicht plausibel ist.
#tests/testthat/test-kpiCalculations.R
test_that("Correct Revenue KPI Output for Shop Level Analytics", {
...
revenue <- calcRevenueShop(ecomData)
testthat::expect_true(!is.null(revenue) #test 1
testthat::expect_true(!grepl('\\.', revenue)) #test 2
testthat::expect_true(revenue > 0) #test 3
})
Mit dem folgenden Befehl lassen sich die Tests ausführen:
devtools::test()
8. Style definieren
8.1
Es gibt mehrere Möglichkeiten Style-Informationen in Shiny einzubauen. Entscheidend ist für uns, dass jegliche Style-Definitionen in ausgelagerten CSS-Datein gespeichert werden. Das kann in einer übergreifenden Datei geschehen, wie in diesem Beispiel mit styleDefinitions.css
oder in mehreren, nach UI-Elementen aufgeteilten, Dateien. Diese Definitionen fließen anschließend in die UI.R, wo sie umgesetzt werden.
8.2
So sieht das Dashboard ohne die Verwendung eines benutzerdefinierten Styles aus. Obwohl das Standard-Layout eines Shiny-Dashboards keineswegs schlecht aussieht, gibt es oftmals Sonderwünsche oder den Wunsch nach einer Optik, die z.B. mit der Corporate Identity eines Unternehmens im Einklang ist.
8.3
Hätten wir das Dashboard gerne in einem Style, der zu der CI von INWT passt, würden wir manche Stellen gerne anpassen. Wir haben einige exemplarische Stellen markiert, an denen wir mit dem Standard-Layout von Shiny nicht ganz einverstanden sind. Z.B. hätten wir gerne
- den Header und die Icons der Key Performance Indikatoren in hellblau wie die Balken des - Histogramms,
- alle Eingabefelder ebenfalls in hellblau und
- die Unterüberschriften auf gleicher Höhe wie die oberste Überschrift.
8.4
Am Beispiel der Infoboxen zeigen wir ein Anpassen der Style-Informationen. Verändert werden soll die Form, die Schrift sowie die Farbe.
Um die passenden CSS-Befehle zu finden, geht man zunächst zum Quellcode des Dashboards (Rechtsklick, dann auf Untersuchen
klicken). Im Untersuchungsmodus fährt man nun über die Stelle, deren Style angepasst werden soll. Dadurch wird der dazugehörige HTML-Code markiert und man erhält ebenfalls die verwendeten CSS-Informationen dieser Stelle. Nun kann man die CSS-Informationen testweise abändern und die Reaktion direkt im Browser beobachten.
Der Code zeigt die eben beschriebenen Anpassungen. Die neuen CSS-Informationen werden nun einfach in die Datei styleDefinitions.CSS
gespeichert. Beim nächsten Laden der App wird das neue User Interface unter Berücksichtigung der neuen Einstellungen erstellt.
/* inst/app/www/styleDefinitions.css */
.info-box {
min-height: 60px; border-radius: 30px;
}
.info-box-icon {
height: 60px; line-height: 60px; width: 90px;
}
.info-box-content {
padding-top: 10px; padding-bottom: 0px;
}
.info-box-text {
text-transform: capitalize;
}
.bg-black {
background-color: #b3d0ec!important;
}
8.5
Nach dem Hinzufügen der neuen Style-Informationen sieht das Dashboard nun so aus. Die Farbgebung ist der CI von INWT angepasst und ist stimmig. Zudem sind alle Felder für Benutzereingaben in hellblau unterlegt und fördern eine intuitive Bedienung des Dashboards.
9. Wiederholung
Im letzten Schritt des Workflows geht es darum diese Abfolge so lange zu wiederholen bis alle gewünschten Funktionalitäten entwickelt sind.
Die Liste der Hilfsdateien wächst, doch UI.R
und Server.R
bleiben, abgesehen von einer größeren Anzahl an Funktionsaufrufen, schlank.
Zusammenfassung unserer Prinzipien
- Legen Sie shiny Dashboards innerhalb eines R-Paketes an. Was zunächst nach einem größeren Aufwand aussieht, zahlt sich aus sobald der Umfang eines Projektes steigt. Denn dann lässt sich die Paket-Struktur flexibel erweitern, ohne Abstriche bezüglich der Übersichtlichkeit machen zu müssen.
- Lagern Sie jegliche Komplexität aus den zentralen Datein (
UI.R
undServer.R
) aus. Schreiben sie separate Funktionen und Module, speichern Sie diese in Hilfsdateien und erhalten Sie schlanke UI.R- und Server.R-Dateien, worin lediglich Funktionsaufrufe stattfinden müssen. - Schreiben Sie Tests für Ihre ausgelagerten Funktionen. Durch automatisiertes formales sowie inhaltliches Überprüfen ihrer Funktionen ist es leicht ein konstant hohes Qualitätsniveau zu gewährleisten.
Folgen Sie diesen Prinzipien, erhalten Sie ein schlankes und übersichtliches shiny Dashboard, welches sich flexibel erweitern lässt und gleichzeitig robust gegenüber Fehlern ist.
Feedback
Haben Sie Anmerkungen oder Verbesserungsvorschläge zu der Gestaltung des ecomAnalytics-Paketes und den darin angewandten Best Practices? Dann erstellen Sie einfach ein Issue auf Github oder senden Sie einen Pull-Request.