Einführung in
Plotly Dash für
die Daten-
visualisierung

In der heutigen Wirtschaft werden datengetriebene Auswertungen immer wichtiger. Für die richtige Kommunikation der Analysen ist Storytelling und damit auch die richtige Visualisierung von Daten ein essenzieller Faktor geworden. Am Markt sind dafür viele verschiedene Softwarelösungen vorhanden, die sich in ihrer Funktion und Preisklasse unterscheiden. Das webbasierte Framework Plotly Dash kann kostenlos für die Visualisierung eingesetzt werden und ist zudem noch interaktiv für den/die Endbenutzer/in im Browser verwendbar. Nachfolgend folgt eine Einführung in das Python Framework von Plotly Dash anhand eines Praxisbeispieles.

Was ist Plotly Dash?

Grundlegend ist Plotly Dash ein kostenloses Open-Source Webframework, welches in den gängigen Datenanalysesprachen Python, R, Julia und F# (experimentell) ermöglicht, Datenvisualisierungen zu erstellen. Plotly Dash basiert auf dem sehr bekannten JavaScript-Framework React, welches in der Webentwicklung große Bedeutung gewonnen hat. Außerdem wird der Webserver Flask von Python eingesetzt.

Ein großer Vorteil dieser webbasierten Technik ist es, dass das Endergebnis praktisch für jede/n Anwender/in verfügbar gemacht werden kann, ohne eine zusätzliche Software installieren zu müssen. Dabei muss aber nun beachtet werden, dass für das Aufrufen der Analysen bei den Endbenutzer/innen im Browser die gesamte Webapp auf einem Webserver deployed werden muss. Dieser Webserver muss Python unterstützen. Eine sehr benutzer/innen/freundliche Methode des Deployments ist es, auf eine containerbasierte Lösung wie Docker zu setzen, da auch Aktualisierungen der Inhalte so einfach möglich sind.

Um das Deployment soll es in diesem Beitrag aber nicht gehen, da es im Zweifelsfall für die Veröffentlichung der Dashboards ohnehin eine kommerzielle kostenpflichtige Option von Plotly gibt.

Inspiration für diverse Dashboards kann man sich in der Dash Gallery holen.

Support von Datenvisualisierungspaketen

Standardmäßig wird für Graph-Objekte in Plotly Dash direkt Plotly verwendet und damit ist es für plotly.py konzeptioniert und sollte demnach auch nach Möglichkeit verwendet werden. Auch externe Pakete wie Seaborn, Matplotlib, Altair/Vega-Lite oder Bokeh können in Dash verwendet werden. Nachteile sind aber die schlechtere Performance und die geringere Interaktivität.

Multi-Page Support

Obwohl Dashboards oft als Single-Side Page erstellt werden, ist es unter Dash auch möglich, mehrere Seiten zu erstellen und eine Navigation zu ermöglichen. Zudem ist die Performance in dieser Hinsicht besser als bei den meisten anderen Dashboard-Frameworks unter Python.

Ein Nachteil ist aber, dass das Framework stateless ist und damit zwischen den einzelnen Seiten Informationen nur mit kleinen Workarounds ausgetauscht werden können.

Dash Enterprise

Neben der Open-Source Version von Plotly Dash gibt es noch Dash Enterprise mit vielen zusätzlichen Features und damit verbundenen Kosten. Trotzdem verwenden bereits 10% der Fortune 500 Companies Dash Enterprise, was auch eine gewisse Ausgereiftheit widerspiegelt.

Praxisbeispiel in Python: Dashboard von Reisedaten in den USA

Bevor mit dem eigentlichen erstellen des Dashboards gestartet werden kann, ist zuvor eine Installation verschiedener Python Pakete erforderlich.

#pip install dash
#pip install dash-renderer
#pip install dash-html-components
#pip install dash-core-components
#pip install dash-bootstrap-components
#pip install dash-table
#pip install plotly --upgrade

Der grundlegende Aufbau einer Dash Webapp besteht aus dem Teil der Layouts, welcher der Teil ist, der anschließend im Browser ersichtlich ist und den Callbacks, welche die Interaktivität der Webapp bereitstellen. In diesem Teil können auch Python Funktionen aufgerufen werden.

Zunächst folgen aber erstmal alle notwendigen Imports. Hier kann man den Kommentaren eine Zuordnung zu den Layout-Teil und dem Callback-Teil entnehmen.

# Basics
import pandas as pd
import numpy as np
# Data Visualization
import plotly.express as px
# Dash
import dash
# Layout
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
# DataTable
import dash_table
# Callbacks
from dash.dependencies import Input, Output

Datenaufbereitung

In dieser beispielhaften Dash Applikation sollen verschiedene Tourismusdaten der USA zum Ausdruck gebracht werden.

Dabei handelt es sich um sehr grundlegende Darstellungen, um den Fokus möglichst nur auf Dash legen zu können. Nachfolgend ein Bild der Applikation:

Screenshot Webapp

Nun ist also klar, was als Ziel erreicht werden soll.

Zuerst müssen dafür die benötigten Daten geladen werden. In diesem Fall werden die Daten wegen der Einfachheit als globale Variablen abgespeichert. Für alle Darstellungen werden insgesamt zwei verschiedene CSVs als Quellen verwendet.

# Global Data 
df_ny = pd.read_csv("data\AB_NYC_2019.csv")
df_hotel_bookings = pd.read_csv("data\hotel_bookings.csv")

Nun kann mit der tatsächlichen Datenvorverarbeitung begonnen werden. Für jedes Diagramm wird eine Funktion erstellt, worin zuerst wenn notwendig Anpassungen an den Daten gemacht werden und schlussendlich mit Plotly ein Plot erstellt wird.

Im Falle des ersten Plots wird die globale Variable df_ny verwendet. Hier werden Infos zu Unterkünften in New York City behandelt. Bevor man den Plot erstellt, werden noch grobe Ausreißer entfernt, dass sind in diesem Fall Unterkünfte mit einem Preis ab 500.

Anschließend wird eine Mapbox verwendet, worin die Latitude und Logitude für die Position auf der Karte herangezogen werden. Bei einer Berührung des Datenpunktes kann man Infos zur Nachbarschaft, zum Preis und zum Raumtyp entnehmen. Für die Farbskala der Punkte ist ebenfalls der Preis ausschlaggebend. Dieser Plot wird dann von dieser Funktion zurückgegeben.

def plot_1():
    # data preparation 
    global df_ny
    df_ny = df_ny[df_ny["price"] < 500] # get only hotels with a price less than 500
    # plot
    fig = px.scatter_mapbox(df_ny, 
                            lat="latitude", lon="longitude", 
                            hover_name="name", 
                            hover_data=["neighbourhood_group","price", "room_type"],
                            color="price", 
                            color_discrete_sequence=px.colors.sequential.RdBu
            )
    fig.update_layout(mapbox_style="open-street-map")
    fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
    return fig

Als nächstes wird ein Liniendiagramm erstellt, welches über die Ankünfte in einer Kalenderwoche Auskunft gibt. Dabei muss in der Dropdown-Box oberhalb ein Jahr ausgewählt werden.

Dafür muss zuerst der globale DataFrame df_hotel_bookings nach dem Jahr und der Kalenderwoche der Ankunft gruppiert werden. Hierbei wird eine Zählung der Ankünfte insgesamt in dieser Woche durchgeführt. Danach wird noch eine neue Spalte erstellt, welche das Jahr und die Kalenderwoche vereinigt und anschließend wird noch nach dem Jahr und der Kalenderwoche sortiert.

Auf der x-Achse des Liniendiagramms wird dann die Kalenderwoche cw aufgetragen und auf der y-Achse die Zählung der Ankünfte. Als Datenbasis dient lediglich das selektierte Jahr in der Dropdown-Box. Für eine korrekte Darstellung der x-Achse ist es wichtig, den Typen auf 'category' zu setzen. Zuletzt wird dieses Diagramm auch wieder zurückgegeben.

def plot_2(year):
    # data preparation 
    global df_hotel_bookings
    df_arrivals_cw = df_hotel_bookings[["arrival_date_year", "arrival_date_week_number", "hotel"]].groupby(["arrival_date_year", "arrival_date_week_number"]).count() # group by year and week number and count arrivals
    df_arrivals_cw = df_arrivals_cw.reset_index()
    df_arrivals_cw["cw"] = df_arrivals_cw["arrival_date_year"].astype(str) + "-" + df_arrivals_cw["arrival_date_week_number"].astype(str) # new column 'cw' out of year and week number
    df_arrivals_cw = df_arrivals_cw.sort_values(["arrival_date_year", "arrival_date_week_number"]) # sort values based on year and week number
    # plot
    plot = px.line(df_arrivals_cw[df_arrivals_cw["arrival_date_year"] == int(year)], # display only data of given year
                    x="cw", y="hotel", 
                    title='Arrivals per calendar week', 
                    labels={"hotel": "arrivals"}, 
                    color_discrete_sequence=px.colors.sequential.RdBu
            )
    plot.update_xaxes(type="category")
    return plot

Das letzte und dritte Diagramm ist ein Ringdiagramm, welches in Prozentwerten beschreibt, wie viele der Hotelbuchungen storniert wurden.

Hier wird bei der Datenvorverarbeitung nach der Spalte 'is_canceled' beim DataFrame df_hotel_bookings gruppiert. Damit erhält man die absolute Zahl an Stornierungen bzw. durchgeführten Reisen. Für die bessere Lesbarkeit werden die Flags 1/0 für eine Stornierung bzw. nicht Stornierung auf 'canceled' und 'not canceled' umbenannt.

Danach kann das Diagramm dargestellt und zurückgegeben werden.

def plot_3():
    # data preparation 
    global df_hotel_bookings
    
    df_canceled = df_hotel_bookings[["is_canceled", "hotel"]].groupby("is_canceled").count() # group by 'is_cnaceled' and count
    df_canceled = df_canceled.reset_index()
    df_canceled["is_canceled"] = np.where(df_canceled["is_canceled"] == 1, "canceled", "not canceled") # rename is_canceled flag 1 to 'canceled' and 0 to 'not canceled'
    
    # plot
    fig = px.pie(df_canceled, values="hotel", names="is_canceled", color_discrete_sequence=px.colors.sequential.RdBu, hole=0.6,)
    return fig

Nun liegen alle Funktionen vor, welche anschließend nur mehr in den Callbacks aufgerufen werden müssen.

Bevor man sich mit den Callbacks beschäftigt, legt man aber zuerst das Layout der Webapp fest.

Dashboard Layout

Für die Frontend-Elemente werden die Pakete dash-html-components und dash-core-components verwendet, die direkt aus dem Python Code anschließend HTML Elemente liefern. Falls diese Auswahl nicht der Erwartung entspricht, können zusätzlich mittels JavaScript und React neue Elemente erstellt werden.

Da eine Responsive-Webapp geschaffen werden soll, wird das Bootstrap Theme verwendet. Mit dieser Zeile wird die Grundlage für die Webapp geschaffen.

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

Da sich das App-Layout aus einzelnen Cards zusammensetzt, welche anschließend in einem Grid angeordnet werden, wird zum einfacheren Verständnis pro Card eine eigene Variable erstellt, welche anschließend nur noch eingetragen werden muss im direkten App-Konstrukt. Eine direkte Einpflegung des Codes in diese Teile wäre deshalb auch möglich.

Die erste Card enthält die Unterkünfte in New York City. Dazu wird einfach für diese Card im Body eine Überschrift der Kategorie 4 erstellt und anschließend noch der Graph eingebettet. Für jedes Element wird noch eine ID festgelegt, diese ist wichtig für das spätere Zugreifen aus den Callbacks. Die IDs müssen immer eindeutig sein.

plot_1_card = dbc.Card(
    dbc.CardBody(
        [
            html.H4('Map Chart - Accomodations in New York City', id='card1-title'),
            dcc.Graph(id='example-graph-1')
        ]
    )
)

Im Falle des zweiten Diagramms erfolgt Ähnliches. Zusätzlich wird noch die Dropdown-Box für die Auswahl des Jahres eingeführt. Standardmäßig ist hier das Jahr 2016 ausgewählt. Alles andere erfolgt gleich wie in Card 1.

plot_2_card = dbc.Card(
    dbc.CardBody(
        [
            html.H4('Line Chart - Arrivals per Calendar Week', id='card2-title'),
            dcc.Dropdown(
                id='year-dropdown',
                options=[
                    {'label': '2015', 'value': '2015'},
                    {'label': '2016', 'value': '2016'},
                    {'label': '2017', 'value': '2017'}
                ],
                value='2016'
            ),
            dcc.Graph(id='example-graph-2')
        ]
    )
)

In der dritten Card erfolgt genau das gleiche Konzept wie in Card 1.

plot_3_card = dbc.Card(
    dbc.CardBody(
        [
            html.H4('Ring Chart - Canceled vs. not canceled', id='card3-title'),
            dcc.Graph(id='example-graph-3')
        ]
    )
)

Für den DataTable wird das dafür importierte Paket verwendet und ebenfalls gleich wie andere Elemente zuvor eingebettet.

datatable_card = dbc.Card(
    dbc.CardBody(
        [
            html.H4('DataTable', id='card4-title'),
            dash_table.DataTable(id='table')
        ]
    )
)

Nun sind die grundlegenden Komponenten der Webapp festgelegt und es kann der Fokus auf das gesamte Layout der App fallen.

Dazu muss auf die zuvor festgelegte Variable app mit .layout zugegriffen werden. Danach erfolgt alles ähnlich zu den Cards. In einem Div über die ganze Anwendung werden verschiedene HTML-Elemente eingefügt. Zuerst wird eine übergeordnete Überschrift eingefügt. Hierbei wird mit style eine Anpassung der visuellen Darstellung gemacht. Zusätzlich zu dieser Überschrift wird ein Untertitel eingefügt, welcher ebenfalls einer Style-Anpassung unterzogen wird.

Danach wird noch ein Div eingefügt, worin das Grid mit den Cards enthalten ist. Hierbei sieht man wie in jeder einzelnen Zelle direkt auf die zuvor erstellten Variablen zugegriffen wird. Wie zuvor erwähnt, könnte hier stattdessen auch der Code direkt eingefügt werden.

app.layout = html.Div(
    children=[
        # headings
        html.H1(
            children='Introduction to Dash',
            style={
                'textAlign': 'center',
                'color': 'black'
            }
        ),
        html.Div(id='main', children='Dash: A web application framework for Data Analytics', 
            style={
                'textAlign': 'center',
                'color': '#AF0038'
            }
        ),
        # grid with cards
        html.Div([
            dbc.Row([
                dbc.Col([plot_1_card]), dbc.Col([plot_2_card])
            ]),
            dbc.Row([
                dbc.Col([plot_3_card]), dbc.Col([datatable_card])
            ])
        ])
    ]   
)

Dashboard Callbacks

Um alle Graph-Elemente und den DataTable mit Daten zu versorgen, werden die Callbacks herangezogen. Im Falle vom ersten und dritten Diagramm könnte auch auf Callbacks verzichtet werden, da hier keine Interaktivität festgelegt wird. Lediglich beim zweiten Diagramm ist das Verwenden von Callbacks essenziell, da nur so die Interaktivität bezüglich der Selektion des Jahres ermöglicht werden kann. Dazu aber mehr im zweiten Callback. Wozu Callbacks also benötigt werden, ist jetzt klar - für die Interaktivität.

Ein Callback hat immer einen Input, welcher den Callback auslöst. Das kann beispielsweise die Dropdown-Value sein, die sich verändert. Weiters ist auch ein Output bei den Callbacks vorhanden. Dieser beschreiben, wo im Layoutkonstrukt diese Änderungen zum Einsatz kommen sollen. Wenn für die Änderungen irgendwelche Zustände relevant sind, gibt es außerdem noch States. Mit States kann der aktuelle Wert eines bestimmten Elements ausgelesen werden, ohne dass dessen Wert sich verändern muss. In diesem Beispiel werden aber keine States verwendet. Input, Output und State müssen von den dash.dependencies importiert werden. Alle Zugriffe via Input, Output und State finden mittels der im Layout festgelegten IDs statt, das konkrete Property von diesem Element muss ebenfalls angegeben werden.

Zu jedem Callback wird dann eine Funktion geschrieben, worin die eigentliche Handlung geschieht. Als Parameter werden die Inputs (und ggf. States) übergeben und die Rückgabewerte stellen die Outputs dar.

Wie erwähnt, könnte auf den ersten Callback verzichtet werden. Der Aufruf findet lediglich beim Laden der Seite statt, das ist erkennbar an der Input-Komponente und deren Property. Die Input-Komponente ist nämlich lediglich der Untertitel und dieser wird nur beim Laden verändert.

Für den DataTable wird der globale DataFrame df_hotel_bookings mit ausgewählten Spalten verwendet. Danach werden lediglich die zuvor erstellten Funktionen für den ersten und dritten Plot aufgerufen. Zurückgegeben wird dann nach der Reihenfolge der Outputs. Deshalb wird mit dem ersten und dritten Plot begonnen. Danach werden die Daten für den DataTable im richtigen Format übergeben und zuletzt die Spaltennamen.

Damit ist der Callback vollständig.

@app.callback(
    Output(component_id='example-graph-1', component_property='figure'),
    Output(component_id='example-graph-3', component_property='figure'),
    Output(component_id='table', component_property='data'),
    Output(component_id='table', component_property='columns'),
    Input(component_id='main', component_property='children')
)
def initial_call(input_value):
    global df_hotel_bookings

    data_table = df_hotel_bookings.iloc[:, [0,1,3,4,5,6]]
    
    plot1 = plot_1()
    plot3 = plot_3()

    return plot1, plot3, data_table.to_dict('records'), [{"name": i, "id": i} for i in data_table.columns]

Der zweite Callback ist nur für den zweiten Plot zuständig. Er wird aufgerufen, wenn sich der Wert von der Dropdown-Box verändert. Dann wird die Funktion für den zweiten Plot einfach mit dem ausgewählten Jahr aufgerufen und der Plot zurückgegeben.

@app.callback(
    Output(component_id='example-graph-2', component_property='figure'),
    Input(component_id='year-dropdown', component_property='value')
)
def update_output_year_line_chart(input_value):
    plot = plot_2(input_value)
    
    return plot

Ausführen der Webapp

Schlussendlich muss nach der Erstellung von Layout und Callbacks noch die Webapp gestartet werden. Das erfolgt mit dem nachfolgenden Codeblock. Wenn das debug-Flag auf True gesetzt wird, wird "Hot-reloading" ermöglich. Das bedeutet, dass Codeänderungen gleich im Browser zu sehen sind. Das wird in einem Jupyter-Notebook aber nicht unterstützt!

Falls ein anderer Port als der standardmäßige 8050 Port verwendet werden soll, kann das ebenfalls hier angegeben werden.

if __name__ == '__main__':
    app.run_server(debug=False, port=8051)

Potenzial

Dash hat ein überaus großes Potenzial, da es frei am Markt verfürgbar ist. In der Wirtschaft ist dieses Tool in der Datenvisualisierung noch nicht wirklich verbreitet, jedoch hat es aus dem besagten Grund großes Potenzial. Auch aus persönlicher Erfahrung heraus kann ich sagen, dass es immer wichtiger wird, Informationen an die Menschen zu bringen. Durch andere Softwarelösung, womit Dashboards erstellt werden können, ist das aber oftmals mit einem hohen Kostenfaktor und viel Lizenzmanagement verbunden, da auch alle Endbenutzer/innen oft eine Lizenz benötigen. Aus diesem Grund ist die Option, auf eine browserbasierte Darstellungsmöglichkeit für Daten zurückzugreifen, eine äußerst gute, da damit praktisch jede/r zugreifen kann.

Im Vergleich zu kompletten Webframeworks ist Plotly Dash ein eigener Ansatz in Richtung Data Science und dabei hat man einige Vorteile in der einfacheren Nutzung. Wenn man bereits mit Python oder einer anderen gängigen Sprache in diesem Gebiet vertraut ist, muss nicht noch zusätzlich eine weitere Sprache erlernt werden.

Damit noch viel Spaß bei der Verwendung von Plotly Dash in vielen verschiedenen Varianten.

Quellen

Beatrice Steiner

Studentin Data Science and Business Analytics an der FH St. Pölten