Nauč se Python > Kurzy > Datový kurz PyLadies > Interaktivní vizualizace a aplikace > Interaktivní vizualizace a aplikace II.

Pokročilejší možnosti pro dashboardy

Dashboard jako třída

Použití interact bylo opravdu přímočaré. Pro složitější aplikace už by ale byl samotný interact příliš omezující. Proto jsou v knihovně panel další možnosti, jak naprogramovat interaktivní aplikaci.

My teď použijeme modul param. Další, ještě flexibilnější možnosti, jsou pak popsány v https://panel.holoviz.org/user_guide/Links.html.

Vytvoříme třídu CovidDashboard, která bude funkcionalitou stejná jako dashboard s předchozí části. Čeho si hlavně všimněte:

  • Naše třída dědí od param.Parameterized.
  • Atributy, které se mění interaktivně pomocí widgetů (countries, plotted_variable, use_log_scale, plot_type), jsou na úrovní třídy (class attributes), tj. nevytvářejí se v metodě __init__.
  • V těchto atributech jsou objekty z modulu param, které nepopisují přímo widgety, ale typ dat (Selector, Boolean, ListSelector).
  • Pro získání hodnoty našich atributů neoptřebujeme .value jako u widgetů.
  • Nepotřebujeme vlastní __init__.

Závislost na stavu jednotlivých atributů vyjadřujeme pomocí dekorátoru param.depends, který funguje podobně jako interact.

Aplikaci vytváříme až v metodě view (jméno metody je libovolné). To je také jediná metoda, kde se přímo použije modul panel (pn). V této metodě je pár pozoruhodných věcí:

  • Do pn.Row dáváme funkce aniž bychom je volali (nejsou tam závorky) - panel si je volá vnitřně až když potřebuje vykreslit aplikaci.
  • Objevuje se metoda self.param, kterou jsme nijak nedefinovali. To je metoda třídy Parameterized, která vytvoří widgety ze všech těch atributů typu Selector, Boolean atd., v závislosti na jejich typech a vlastnostech.
In [ ]:
import datetime as dt

import pandas as pd
import numpy as np

import param
import panel as pn
import plotly.express as px

pn.extension("plotly")
In [2]:
# staré známé načítání dat
covid_data_raw = pd.read_csv(
    "https://raw.githubusercontent.com/datasets/covid-19/master/data/time-series-19-covid-combined.csv",
    parse_dates=["Date"],
).rename(columns={"Province/State": "State", "Country/Region": "Country"})

covid_data = (
    covid_data_raw
    .groupby(["Country", "Date"])[["Confirmed", "Recovered", "Deaths"]]
    .agg("sum")
    .reset_index()
)
In [3]:
class CovidDashboard(param.Parameterized):
    """Simple COVID-19 dashboard
    """

    # atributy na úrovni třídy - popisují data použitá pro interaci
    countries = param.ListSelector(
        default=["Czechia"], objects=sorted(covid_data["Country"].unique())
    )
    plotted_variable = param.Selector(
        default="Confirmed", objects=["Confirmed", "Recovered", "Deaths"]
    )
    use_log_scale = param.Boolean(doc="Use log scale", default=False)
    plot_type = param.Selector(
        doc="Plot type", objects={"bar": px.bar, "line": px.line}
    )

    # param.depends zařizuje interaktivní závislot
    @param.depends("countries", "plotted_variable", "use_log_scale", "plot_type")
    def plot(self):
        """Plot data using plotly
        """
        # tady už používáme self.countries atd., kde jsou k dispozici aktuální hodnoty
        return self.plot_type(
            covid_data.loc[covid_data["Country"].isin(self.countries)],
            x="Date",
            y=self.plotted_variable,
            color="Country",
            log_y=self.use_log_scale,
        )

    def view(self):
        """Create panel interactive application
        """
        return pn.Row(self.param, self.plot)

Když vytvoříme instanci této třídy, ještě žádnou aplikaci nevytvoříme.

In [4]:
covid_dashboard = CovidDashboard()
covid_dashboard
Out[4]:
CovidDashboard(countries=['Czechia'], name='CovidDashboard00006', plot_type=<function bar at 0x111fef440>, plotted_variable='Confirmed', use_log_scale=False)

Až teprve metoda view vytváří aplikaci, v našem případě (skoro) stejnou jako jsme vytvořili pomocí interact.

In [ ]:
covid_dashboard.view()

Úkol: Změňte typ atributu countries, aby šla vybrat pouze jedna země.

Vylepšený dashboard

Pojďme náš dashboard trochu vylepšit.

In [6]:
# Odkomentujte pro instalaci iso3166 - potřebujeme pro ISO kódy států
# %pip install iso3166
In [7]:
import iso3166


def country_alpha3(country):
    try:
        return iso3166.countries.get(country).alpha3
    except KeyError:
        return None

Načítání dat si zabalíme do funkce - použijeme poté v třídě pro dashboard.

In [8]:
def get_covid_data():
    covid_data_raw = pd.read_csv(
        "https://raw.githubusercontent.com/datasets/covid-19/master/data/time-series-19-covid-combined.csv",
        parse_dates=["Date"],
    ).rename(columns={"Province/State": "State", "Country/Region": "Country"})

    covid_data = (
        covid_data_raw
        .groupby(["Country", "Date"])
        .agg({"Confirmed": "sum", "Recovered": "sum", "Deaths": "sum", "Lat": "first", "Long": "first"})
        .reset_index()
        .assign(iso_alpha=lambda df: df["Country"].map(country_alpha3))
    )
    
    return covid_data

V aplikacích, které jsou spuštěné někde v oblacích, je velmi užitečné logovat co se děje. Pomůže nám to při odhalováních chyb, ladění rychlosti nebo obecně sledování kdo / jak aplikaci používá.

In [9]:
import logging

logging.basicConfig(level=logging.INFO)

Do našeho vylepšeného dashboardu přidáme následující:

  • Přehledovou tabulku.
  • Zobrazení dat na mapě.
  • Výběr datumu (pro přehledovou tabulku a mapu).
  • Chytřejší načítání dat.
In [16]:
class GreatCovidDashboard(param.Parameterized):
    """Almost the best COVID dashboard, (C) PyDataLadies 2020
    """

    # výběr zemí - možnosti doplníme podle dat
    countries = param.ListSelector(default=["Czechia"], objects=["Czechia"])
    # výběr zobrazené veličiny
    plotted_variable = param.Selector(
        default="Confirmed", objects=["Confirmed", "Recovered", "Deaths"]
    )
    # přepínání logaritmického měřítka
    use_log_scale = param.Boolean(doc="Use log scale", default=False)
    # výběr typu grafu
    plot_type = param.Selector(
        doc="Plot type", objects={"bar": px.bar, "line": px.line}
    )
    # výběr datumu pro zobrazení dat na mapě
    map_plot_date = param.Date(
        # vybrané datum i meze změníme v update_covid_data na základě dat
        dt.date(2020, 4, 1),
        bounds=(dt.date(2020, 1, 1), dt.date(2020, 5, 1)),
    )

    def __init__(self, **params):
        # vytvoříme si logger pro tuto třídu
        self._logger = logging.getLogger("GreatCovidDashboard")
        self._logger.info("Initializing")
        # musíme zavolat i __init__ mateřské třídy
        super().__init__(**params)
        # "skrytý" atributu _covid_data aktualizujeme v metodě update_covid_data
        self._covid_data = None
        self.update_covid_data()
        # na začátek vybereme poslední datum k zobrazení
        self.map_plot_date = self._covid_data["Date"].max().date()
        self._logger.info("Initialized")

    def update_covid_data(self):
        # načteme aktuální data do "skrytého" atributu _covid_data
        self._logger.info("Loading COVID data")
        self._covid_data = get_covid_data()
        # a změníme parametry vybírátka data
        self.param.map_plot_date.bounds = (
            self._covid_data["Date"].min().date(),
            self._covid_data["Date"].max().date(),
        )
        self.param.countries.objects = sorted(self._covid_data["Country"].unique())

    # propery nám pomůže udělat "chytrý" atribut covid_data
    # který používá "skrytý" _covid_data a aktualizuje ho pokud je třeba
    @property
    def covid_data(self):
        # načítat data chci jen pokud jsou zastaralá
        if self._covid_data["Date"].max() < (dt.date.today() - dt.timedelta(days=1)):
            self.update_covid_data()
        return self._covid_data

    @param.depends("countries", "plotted_variable", "use_log_scale", "plot_type")
    def plot(self):
        """Plot time evolution for one or more countries
        """
        return self.plot_type(
            self.covid_data.loc[self.covid_data["Country"].isin(self.countries)],
            x="Date",
            y=self.plotted_variable,
            color="Country",
            log_y=self.use_log_scale,
        )

    @param.depends("plotted_variable", "use_log_scale", "map_plot_date")
    def map_plot(self):
        """Map-plot of COVID data on a specific date
        """
        # vytvoříme si pomocný dataframe jen pro vybrané datum
        data = self.covid_data.loc[self.covid_data["Date"].dt.date == self.map_plot_date]
        if self.use_log_scale:
            # přidáme sloupec log(plotted_variable)
            color_column = f"log({self.plotted_variable})"
            data = data.assign(**{color_column: np.log10(data[self.plotted_variable])})
        else:
            color_column = self.plotted_variable
            
        return px.choropleth(
            data,
            locations="iso_alpha",
            color=color_column,
            hover_name="Country",
            hover_data=["Confirmed", "Recovered", "Deaths"],
            color_continuous_scale=px.colors.sequential.Plasma,
        )

    @param.depends("map_plot_date", "countries")
    def selected_day_numbers(self):
        """Overview table for the selected date
        """
        data = self.covid_data.loc[
            (self.covid_data["Date"].dt.date == self.map_plot_date)
            & (self.covid_data["Country"].isin(self.countries))
        ]
        return pn.Column(
            pn.pane.HTML(f"Situation on {self.map_plot_date}"),
            pn.pane.DataFrame(
                data.set_index("Country")[["Confirmed", "Recovered", "Deaths"]]
            ),
        )

    def view(self):
        self._logger.info("Creating dashboard view")
        return pn.Column(
            pn.pane.HTML("<h1>Great COVID-19 Dashboard</h1>"),
            self.selected_day_numbers,
            pn.Row(
                pn.Param(self.param, widgets={"map_plot_date": pn.widgets.DatePicker,}),
                self.plot,
            ),
            self.map_plot,
        )

A teď už můžeme dashboard vytvořit.

In [17]:
great_covid_dashboard = GreatCovidDashboard()
INFO:GreatCovidDashboard:Initializing
INFO:GreatCovidDashboard:Loading COVID data
INFO:GreatCovidDashboard:Initialized

A nakonec zobrazit.

In [ ]:
great_covid_dashboard.view().servable(title="Great COVID-19 Dashboard")

Úkoly

Zkuste teď náš nový krásný dashboard vylepšit a zprovoznit na internetu.

  1. Přidejte widget pro výběr projekce mapy. Projekce se mení pomocí parametru location funkce choropleth.
  2. Spusťte aplikaci na Heroku. Nejjednodušší bude nahradit předchozí dashboard.

A pokud bude zbyde čas, zkuste třeba

  1. Změnit widget na výběr zemí na MultiChoice.
  2. Přidat vybírátko začátku časové osy.
  3. Přidat graf vývoje smrtnosti, tj. podílu zemřelých k nakaženým.

Anebo vymyslete a naprogramujte vlastní vylepšení :)

In [ ]:


Toto je stránka lekce z kurzu, který probíhá nebo proběhl naživo s instruktorem.