使用Shiny for Python创建仪表板

人工智能与机器学习

概述

人工智能(AI)和机器学习(ML)是当今科技领域最热门的话题之一。AI是指计算机系统能够执行通常需要人类智能的任务,如视觉识别、语音识别和决策制定。ML是AI的一个子集,专注于开发能够从数据中学习的算法。

关键技术

深度学习

深度学习是ML的一个分支,使用多层神经网络来处理复杂的数据模式。它在图像和语音识别方面取得了显著的成果。

强化学习

强化学习是一种通过试错来学习的ML方法。它通常用于游戏和机器人控制等领域。

应用领域

医疗保健

AI和ML在医疗诊断、药物发现和个性化治疗方面有广泛应用。

金融

在金融领域,AI用于风险管理、欺诈检测和算法交易。

挑战

数据隐私

随着AI和ML的发展,数据隐私成为一个重要问题。如何在不侵犯个人隐私的情况下利用数据是一个挑战。

伦理问题

AI系统的决策过程可能不透明,这引发了关于伦理和责任的讨论。

未来展望

AI和ML的未来充满希望,但也伴随着挑战。随着技术的进步,我们期待看到更多创新的应用和解决方案。

介绍

Shiny 包提供了一种使用Python轻松构建Web应用程序的方式。Quarto仪表板可以包含嵌入的Shiny组件(例如,带有滑块的图表,可以控制其输入)。

本节假设您对Shiny没有任何经验,并将教您使用Quarto运行Shiny所需的基本概念。

如果您使用的是R而不是Python,请参阅关于使用Shiny for R的文档。

Shiny 前置要求

为了在 Quarto 文档中使用 Shiny,你需要最新版本的 shiny (>=0.9.0) 和 shinywidgets (>=0.3.1) 包。你可以通过以下命令安装这些包的最新版本:

pip install --upgrade shiny shinywidgets

你好,Shiny

我们将从一个非常简单的仪表板开始,该仪表板由一个图表和一些用于驱动其显示的输入组成:

一张企鹅喙数据仪表盘的截图。左侧边栏包含两个下拉菜单,一个用于选择变量,另一个用于选择分布类型,还有一个复选框用于显示地毯标记。右侧的图表占据了整个页面高度,显示了按物种着色的bill_length_mm直方图。

以下是该仪表盘的源代码(点击最右侧的数字以获取额外解释):

---
title: "企鹅喙数据"
format: dashboard
server: shiny
---

```{python}
import seaborn as sns
penguins = sns.load_dataset("penguins")
```

## {.sidebar}

```{python}
from shiny.express import render, ui
ui.input_select("x", "变量:",
                choices=["bill_length_mm", "bill_depth_mm"])
ui.input_select("dist", "分布类型:", choices=["hist", "kde"])
ui.input_checkbox("rug", "显示地毯标记", value = False)
```

## 列

```{python}
@render.plot
def displot():
    sns.displot(
        data=penguins, hue="species", multiple="stack",
        x=input.x(), rug=input.rug(), kind=input.dist())
```
1
server: shiny 选项指示 Quarto 在文档背后运行一个 Shiny 服务器。
2
通过在二级标题中添加 .sidebar 类来创建侧边栏。侧边栏可以包含代码单元格、图像、叙述和链接。
3
一系列 Shiny 输入元素(与它们交互会更新 input 对象)
4
图表根据当前的 input 值进行渲染和更新。

在这个仪表板中,您可以从左侧的选择框中选择不同的值,图表将更新以反映您的选择。您还可以点击复选框以显示或隐藏地毯标记。让我们一步一步地了解这个启用Shiny的仪表板的构建过程。

元数据

首先要做的是在前面添加 server: shiny。这告诉Quarto将文档渲染为Shiny仪表板(这要求在查看仪表板时需要Python运行时),而不是作为静态HTML页面。当您在server: shiny文档上运行quarto preview <filename>.qmd时,Quarto将为您启动并维护一个Shiny进程,并在浏览器中打开仪表板。

添加输入控件

接下来,我们使用匹配模式ui.input_xxx的函数来创建输入控件。例如,ui.input_select()创建一个选择框,ui.input_slider()创建一个滑块,等等。这些函数的返回值随后由Quarto渲染为HTML和JavaScript。

此示例仅使用了两种输入类型,但Shiny还有很多其他类型。使用Shiny组件浏览器查看所有这些类型,以及您可以复制并粘贴到仪表板中的代码片段。

上面的示例使用以下代码定义了一个输入:

```{python}
ui.input_select("x", label="变量:",
                choices=["bill_length_mm", "bill_depth_mm"])
```

每个输入函数都将输入ID作为其第一个参数。 输入ID是一个唯一的字符串,用于标识此输入;它必须是一个简单的、语法有效的Python变量名。我们将使用此ID从仪表板的其他部分访问输入的值。

Warning

确保您的Shiny仪表板中的每个输入ID都是唯一的。如果您为两个不同的输入使用相同的ID,Shiny将无法区分它们,您的仪表板将无法正常工作。

每个输入函数的第二个参数通常是一个人类可读的字符串,将显示在输入控件旁边。例如,ui.input_select()函数传递了第二个参数"变量:",这就是为什么选择框旁边有一个标签”变量:“。

侧边栏和工具栏

在许多仪表板中,将所有输入控件视觉上集中在一个侧边栏中是可取的。您可以通过将.sidebar类添加到二级标题来实现这一点,就像我们在示例中所做的那样:

## {.sidebar}

```{python}
ui.input_select("x", "变量:",
                choices=["bill_length_mm", "bill_depth_mm"])
ui.input_select("dist", "分布:", choices=["hist", "kde"])
ui.input_checkbox("rug", "显示地毯标记", value = False)
```

作为侧边栏的替代方案,您还可以水平布局输入,甚至直接将它们附加到卡片上。有关更多详细信息,请参阅关于输入的文章。

显示动态输出

在Shiny中,仪表板可以包含输出——图表、表格、文本等——这些输出会根据用户输入动态更新。

上面的示例使用以下代码定义了一个动态图表:

```{python}
@render.plot
def displot():
    sns.displot(
        data=penguins, hue="species", multiple="stack",
        x=input.x(), rug=input.rug(), kind=input.dist())
```

这里的函数被命名为displot。函数的主体使用典型的Seaborn代码来创建图表。并且向函数添加了@render.plot装饰器,以指示Shiny应使用此函数来创建图表。(如果您之前没有见过装饰器,它们是Python的一个特性,允许您向函数添加额外的行为。) input.x()input.rug()input.dist() 方法调用正在获取之前在仪表盘中创建的 xrugdist 输入的值。

请注意,我们的代码从未调用 displot() 函数!仅仅是定义该函数并使用 @render.plot 装饰它的行为,就足以告诉 Shiny 和 Quarto:

  • 在此位置插入一个图表到仪表盘中。
  • 使用函数体来创建图表。
  • 每当由于用户交互导致 input.x()input.rug()input.dist() 的值发生变化时,自动重新运行函数体,并使用结果更新现有图表。

这个示例只包含一个 @render.plot 输出,但 Shiny 应用程序可以包含多个输出和不同类型的输出,正如你在下面的示例中将看到的。查看 Shiny 组件浏览器 以了解有哪些类型的输出可用。

响应式编程

在前一节中,我们说过每当 displot 函数引用的任何输入发生变化时,该函数都会自动重新运行。Shiny 是一个响应式编程框架,这意味着它会负责跟踪应用程序中输入和输出之间的关系。当一个输入发生变化时,只有受该输入影响的输出会被重新渲染。这是一个强大的特性,使得创建能够高效响应用户输入的仪表板变得容易。

Note

input 对象被设计为由 Shiny 的响应式框架进行跟踪,

Shiny 专门跟踪对_响应式感知_对象(如 input 对象)的更改,而不是对任何任意的 Python 变量的更改。你不能简单地写 x = 100,然后在 displot 函数中使用 x,并期望每当 x 发生变化时 displot 自动重新运行。

同样,Shiny 只会自动重新运行响应式感知的函数,比如用 @render.plot 装饰的函数。它不会帮助重新执行文档顶层或常规 Python 函数中的代码。

附加功能

接下来,我们将探索一个更深入的示例,涵盖更多功能,包括提取设置代码、响应式计算以及更高级的布局构造,如页面。以下是我们将要构建的交互式文档:

Palmer Penguins 仪表板的截图。导航栏显示两个页面:Plots 和 Data。左侧是一个侧边栏,包含一张企鹅图片和四个输入:一组用于 Species 的复选框;一组用于 Islands 的复选框;一个用于 Distribution 的下拉菜单;以及一个显示地毯标记的复选框。右侧页面分为两行,每行显示一个密度图:顶部行为 bill_depth_mm;底部行为 bill_length_mm

以下是此仪表板的源代码。你可以点击最右侧的数字获取语法和机制的额外解释,我们也会在下面详细解释。

---
title: "Palmer Penguins"
author: "Cobblepot Analytics"
format: dashboard
server: shiny
---

```{python}
#| context: setup
import seaborn as sns
from shiny import reactive
from shiny.express import render, ui
penguins = sns.load_dataset("penguins")
```

# {.sidebar}

![](images/penguins.png){width="80%"}

```{python}
species = list(penguins["species"].value_counts().index)
ui.input_checkbox_group(
    "species", "Species:",
    species, selected = species
)

islands = list(penguins["island"].value_counts().index)
ui.input_checkbox_group(
    "islands", "Islands:",
    islands, selected = islands
)

@reactive.calc
def filtered_penguins():
    data = penguins[penguins["species"].isin(input.species())]
    data = data[data["island"].isin(input.islands())]
    return data
```

```{python}
ui.input_select("dist", "Distribution:", choices=["kde", "hist"])
ui.input_checkbox("rug", "Show rug marks", value = False)

Learn more about the Palmer Penguins dataset.

Plots # <7>

```{python}
@render.plot
def depth():
    return sns.displot(
        filtered_penguins(), x = "bill_depth_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```
```{python}
@render.plot
def length():
    return sns.displot(
        filtered_penguins(), x = "bill_length_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```

Data

```{python}
@render.data_frame
def dataview():
    return render.DataGrid(filtered_penguins())
```

1.  `server: shiny` 选项指示 Quarto 在文档背后运行一个 Shiny 服务器。

2.  `context: setup` 单元格选项表示此代码单元格应在应用程序启动时运行(而不是在每个新客户端会话启动时运行)。昂贵的初始化代码(例如加载数据)应放置在 `context: setup` 中。

3.  通过将 `.sidebar` 类添加到一级标题来创建全局侧边栏。侧边栏可以包括代码单元格以及图像、叙述和链接。

4.  这些复选框输入组的选项内容由数据集中 `species``islands` 字段的可用类别动态驱动。

5.  当用户与复选框组交互时,这将导致数据集的不同过滤视图。`@reactive.calc` 函数重新计算过滤后的数据集,并将其作为 `filtered_penguins()` 提供。

6.  这些输入影响图表的显示,但不影响过滤后数据集的内容。

7.  一级标题(此处为 `# Plots``# Data`)在仪表板中创建页面。

8.  图表通过引用过滤后的数据集(`filtered_penguins()`)以及面向图表显示的输入(`input.dist()``input.rug()`)来渲染。当数据集或这些输入发生变化时,图表会自动重新渲染。

9.  “数据”选项卡也引用 `filtered_penguins()`,并在过滤数据发生变化时更新。



### 设置单元格

在静态的 Quarto 文档中,`{python}` 代码单元格仅在文档渲染时运行,而不是在查看时运行。在 `server: shiny` 文档中,`{python}` 代码单元格在渲染时和每次在浏览器中加载仪表板时都会运行。这一点很重要,因为每个访问仪表板的用户都需要自己在内存中独立的一份输入/输出副本,以避免同时使用的用户相互干扰。

然而,有时我们有一些代码对于每个用户来说运行过于繁重,我们只想在文档的 Shiny 运行时进程启动时运行一次。例如,在上面的示例中,我们使用 `sns.load_dataset("penguins")` 导入包并加载数据:

````{.python .pymd}
```{python}
#| context: setup
import seaborn as sns
from shiny import reactive
from shiny.express import render, ui
penguins = sns.load_dataset("penguins")

我们这样做是因为对于每个用户来说,加载数据既耗时又耗内存,不如只为进程加载一次。

通过简单地在代码单元格中添加 `#| context: setup`,我们可以告诉 Quarto 只在 Shiny 进程启动时运行一次代码。设置单元格是提取你希望只运行一次而不是每次页面加载时运行的代码的好方法。你在设置单元格中定义的变量可以被文档中的所有其他代码单元格读取。

### 仪表板页面

在此仪表板的顶部,你可以看到“Plots”和“Data”标题。这些被称为**仪表板页面**。仪表板页面是将你的仪表板组织成多个页面的一种方式,每个页面都有自己的一组输出。你可以通过在 Markdown 中添加一级标题来插入仪表板页面。在本例中,`# Plots` 和 `# Data`:

```` {.python .pymd}
# Plots

# Data

数据框输出

在 Data 页面中,有一个动态的数据框输出。这是通过以下代码创建的:

```python```
```markdown
@render.data_frame
def dataview():
    return render.DataGrid(filtered_penguins())

`@render.data_frame` 函数中,你可以直接返回一个 Pandas 数据框,Shiny 会自动将其渲染为一个交互式的数据网格。(`filtered_penguins()` 函数是一个返回数据框的响应式计算——我们将在接下来探讨这一点。)

你还可以选择将数据框对象包装在一个 [`render.DataGrid`](https://shiny.posit.co/py/api/render.DataGrid.html)[`render.DataTable`](https://shiny.posit.co/py/api/render.DataTable.html#shiny.render.DataTable) 构造函数中;在本例中,我们使用了前者。这并不是严格必要的,但它允许你对数据网格或表格设置额外的选项,例如过滤和选择。

`render.DataGrid``render.DataTable` 之间的唯一区别是渲染表格的外观:`render.DataGrid` 使用更紧凑、类似电子表格的外观,而 `render.DataTable` 使用更传统的表格外观。

### 响应式计算

在这个示例中,用户使用选择框来过滤数据集,过滤后的数据集显示在三个不同的动态输出中:两个图表和一个数据框。记住,随着输入的变化,Shiny 会自动重新执行那些被 `@render.plot``@render.data_frame` 装饰的、受这些输入影响的函数。但我们把过滤数据集的代码放在哪里呢?

最明显的地方是将其代码复制到三个渲染函数中的每一个中。但这不是一个好主意,既因为重复代码维护起来很烦人,又因为为了得到完全相同的结果而重复运行相同的过滤代码三次是低效的。我们可以将重复的代码提取到一个函数中,这将消除维护问题,但并不会更高效。

Shiny 有一个解决方案:**响应式计算**。响应式计算是一个响应式感知的函数,每当其输入变化时都会重新执行,但其返回值不会被渲染到仪表板中。相反,返回值会被缓存,并且可以被渲染函数(甚至其他响应式计算)访问。这使我们能够将过滤逻辑放在一个单一的响应式计算中,然后让三个渲染函数从该响应式计算中访问过滤后的数据集。

要创建一个响应式计算,我们使用 `@reactive.calc` 装饰器。以下代码创建了一个名为 `filtered_penguins` 的响应式计算:

```` {.python .pymd}
```{python}
@reactive.calc
def filtered_penguins():
    data = penguins[penguins["species"].isin(input.species())]
    data = data[data["island"].isin(input.islands())]
    return data
```

要读取响应式计算的值,像调用函数一样调用它。例如,depth 图表看起来像这样:

```{python}
@render.plot
def depth():
    return sns.displot(
        filtered_penguins(), x = "bill_depth_mm",
        hue = "species", kind = input.dist(),
        fill = True, rug=input.rug()
    )
```

注意 filtered_penguins() 调用。重申一下,这个调用并不一定会导致 filtered_penguins 函数运行。相反,它通常会返回该函数的缓存值,该值会自动更新,无论何时其引用的输入发生变化。由于 depth 图表引用了 filtered_penguins 计算,因此每当这些输入变化时,它都会重新渲染。

学习更多

要了解更多关于 Python 交互式文档的 Shiny 信息,请参阅以下文章:

组件浏览器 列举了可用的 Shiny 输入和输出,并提供了你可以复制粘贴到仪表板中的代码片段。

输入布局 描述了多种布局 Shiny 输入的方式(侧边栏、输入面板、直接将输入附加到卡片等)

运行仪表板 更深入地介绍了如何在 VS Code 中和命令行上运行 Shiny 仪表板,以及如何将其部署给最终用户。

执行上下文 深入探讨了不同代码单元何时运行(例如渲染与服务)。

Shiny for Python 提供了所有可用 UI 和输出小部件的深入文档,以及关于事物如何工作的概念性讨论。