用scrapbook构建Jupyter笔记本工作流

357 阅读8分钟

好的软件设计的一个原则是限制软件组件的功能和范围。Jupyter笔记本在开发过程中,其规模和复杂程度往往在不断增加。把一个复杂的工作流程的所有逻辑放在一个笔记本中是很诱人的。将一个工作流程分解成多个笔记本,需要一种在笔记本之间沟通数据的方式。 笔记本作者需要能够将数据或结果从一个笔记本中持久化,并在另一个笔记本中读取,以便建立一个工作流程。这方面有许多常见的选择。

  • 将数据保存到CSV/Pickle/Parquet,等等。
  • 保存到数据库(关系型或对象型存储)。
  • 进程间通信

所有这些选项都有一个共同的问题:笔记本和数据是分开的。让数据和笔记本共存于一个地方会很有用。这就是nteractscrapbook库的作用。Scrapbook允许笔记本作者将笔记本会话中的一些数据持久化到笔记本文件本身。然后其他笔记本(或Python应用程序)可以读取笔记本文件并使用这些数据。

构建工作流

可以创建较小的笔记本,对其进行单元测试,然后用papermill进行参数化和执行,而不是用一个笔记本来执行整个工作流程。然后,每个笔记本的输出可以被工作流中的后续笔记本所读取。每个笔记本都会执行并保存任何结果,供流程中的下一步使用。Scrapbook将这些值保存在笔记本文件本身中。在工作流程的后期,笔记本文件被读取,并重新获得这些值。任何Python对象或显示值都可以被持久化,只要它们能被序列化。该库包括一些基本的编码器,新的编码器也可以很容易地被创建。

安装

首先,为了使用scrapbook,你必须安装它。

pip install scrapbook

或者如果你希望能够安装所有的可选依赖项(对于像Amazon S3或Azure这样的远程服务器:)

pip install scrapbook[all]

它是如何工作的?

Scrapbook利用了笔记本只是JSON文档的优势,能够为单元格存储不同类型的输出。了解这一点的最好方法是看一个简单的例子。

首先,创建一个源笔记本并导入scrapbook库。

import scrapbook as sb

现在,在一个单元格中,定义一个值。

x = 1

当我们保存笔记本时,上面的单元格(在JSON .ipynb文件中)将看起来像这样(你可能会看到不同的ID和执行数)。

{
   "cell_type": "code",
   "execution_count": 1,
   "id": "6b5d2b33",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = 1"
   ]
}

glue 现在,在随后的单元格中,我们可以使用scrapbook ,将x 的值添加到当前笔记本中。

sb.glue("x", x)

保存笔记本后,上面的单元格(在JSON .ipynb文件中)将看起来像这样。

{
   "cell_type": "code",
   "execution_count": 1,
   "id": "228fc7d4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/scrapbook.scrap.json+json": {
       "data": 1,
       "encoder": "json",
       "name": "x",
       "version": 1
      }
     },
     "metadata": {
      "scrapbook": {
       "data": true,
       "display": false,
       "name": "x"
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "sb.glue(\"x\", x)"
   ]
  }

虽然你在笔记本中看不到这个单元格的任何输出,但仍有数据隐藏在单元格输出中,作为一个编码的数字值。元数据也被保存下来,以便剪贴簿以后可以正确地读取这个值。

同样,如果到此为止的笔记本已经被保存,我们现在可以使用scrapbook 读取笔记本,并使用它从笔记本文件中获取值。通常,我们在一个不同的笔记本或 Python 应用程序中做这个,但在同一个笔记本中也可以工作 (只要它被保存在磁盘上)。

nb = sb.read_notebook("scrapbook_and_jupyter.ipynb")

笔记本对象 (nb) 有许多属性,直接对应于笔记本文件的 JSON 模式,正如nbformat docs 中记录的那样。但它也有一些额外的方法来处理scraps ,这些值已经被粘在笔记本上了。你可以直接看到这些废品。

nb.scraps
Scraps([('x', Scrap(name='x', data=1, encoder='json', display=None))])

或者在一个DataFrame 中看到它们。

nb.scrap_dataframe
  name  data encoder display                     filename
0    x     1    json    None  scrapbook_and_jupyter.ipynb

而且你可以很容易地取到值。

x = nb.scraps['x'].data
x
1

现在我们已经涵盖了基础知识,让我们把工作放在一起,做一个更复杂的例子。

一个工作流程样本

对于这个工作流程,让我们以我的文章中关于造纸厂的例子为基础假设我们想为一些股票代码运行一个笔记本,寻找任何在其历史最高价(ATH)的阈值内的符号。然后,我们将运行第二个笔记本,读取第一步的所有笔记本,并只显示那些在阈值内的股票的数据。

在这个例子中,我们将使用更多的scrapbook 功能。

工作流程的第一步

源笔记本将为每个证券商执行一次。为了保持简单(和快速),本例中笔记本将生成假数据,但可以很容易地与真实数据连接。笔记本生成一个价格序列,一个所有时间高点(ATH)的价格,然后确定最后的价格是否在ATH的阈值范围内,同时还生成一个图表。笔记本保存了绘图、源数据和一些数值。

length = 1000
symbol = "XYZ"
d = {
    "a": 1,
    "b": 2,
}
threshold = 0.1 # 10%

import pandas as pd
import numpy as np
import scrapbook as sb

import matplotlib.pyplot as plt

# generate a DataFrame that has synthetic price information
idx = pd.date_range(start='20100101', periods=length, freq='B')
prices = pd.DataFrame({'price' : np.cumsum(np.random.random(length) - .5)}, index=idx)
# normalize to always be above 0
prices['price'] += abs(prices['price'].min())
prices['ATH'] = prices['price'].expanding().max()

distance = 1 - prices.iloc[-1]['price']/prices.iloc[-1]['ATH']
if distance <= threshold:
    close_to_ath = True
else:
    close_to_ath = False

fig, ax = plt.subplots(figsize=(12,8))
ax.plot(prices['price'])
ax.plot(prices['ATH'])
ax.text(prices.index[-1], prices['price'].iloc[-1], f"{distance * 100: .1f}%");

粘合不同的类型

我们已经介绍了一个基本类型的glue 方法。如果传入的类型可以使用内置的编码器之一进行序列化,它就会被序列化。为了保留数字类型,它们将被编码为JSON。

sb.glue("length", length)                           # numeric - int (stored as json)
sb.glue("symbol", symbol)                           # text
sb.glue("distance", distance)                       # numeric - float
sb.glue("close_to_ath", close_to_ath)               # bool

你也可以为更复杂的类型指定编码器。目前(从scrapbook的0.5版本开始),有json、pandas、text和display的编码器。

glue 函数还有一个display 的_参数_。这决定了当笔记本被黏贴时,该值是否在笔记本中可见。默认情况下,当数值被存储时,你不会在笔记本中看到它。

显示编码器将只保存显示的值,而不是支持它的基础数据。这对于那些可能有很多数据需要创建结果的视觉类型来说可能是有意义的,而且你只想要视觉结果而不是数据。例如,如果我们只想从上面得到我们的绘图,我们可以只坚持显示。我们没有一个编码器会对matplotlib.figure.Figure (所以会引发一个异常),但既然可以显示,就可以这样存储。

# with display set, this will display the value, see it in the output below?
sb.glue("dj", d, encoder="json", display=True)  
sb.glue("prices", prices, encoder="pandas")
sb.glue("message", "This is a message", encoder="text")

try:
    sb.glue("chart", fig)
except NotImplementedError as nie:
    print(nie)
# but we can store the display result (will also display the value)
sb.glue("chart", fig, encoder="display")
{'a': 1, 'b': 2}
Scrap of type <class 'matplotlib.figure.Figure'> has no supported encoder registered

现在,一个参数化的笔记本已经退出,并且可以用不同的值来执行,我们用一个简单的脚本(或从命令行)来运行它,以获得一些股票。例如,我们可以在笔记本文件存在的目录中做这样的事情(有一些假的股票)。

mkdir tickers
for s in AAA ABC BCD DEF GHI JKL MNO MMN OOP PQD XYZ PDQ
do
    papermill -p symbol $s scrapbook_example_source.ipynb tickers/${s}.ipynb
done

在这一点上,假设笔记本没有出现故障,应该有一个笔记本文件的目录,其中有每个股票的数据。

第二个工作流程步骤

我们在工作流程中的第二个笔记本加载上面生成的每个工作簿,创建一个在阈值范围内的报告。

这里使用了一个额外的API。read_notebooks 方法,它允许我们一次性地获取所有的笔记本。我们将对它们进行迭代,显示每个笔记本的代码和距离,并为每个在阈值内的笔记本显示图表。

source_dir = "tickers"
sbook = sb.read_notebooks(source_dir)

现在我们有一个笔记本的剪贴簿(sbook),我们可以迭代浏览。

for nb in sbook.notebooks:
    print(f"{nb.scraps['symbol'].data: <5} {nb.scraps['distance'].data * 100: .2f}%")
    if nb.scraps['close_to_ath'].data:
        display(nb.scraps['chart'].display['data'], raw=True)   
AAA    49.81%
ABC    60.51%
BCD    0.13%

DEF    94.09%
FB     80.13%
GHI    19.65%
JKL    44.80%
MMN    100.00%
MNO    2.42%

OOP    24.18%
PDQ    93.33%
PQD    18.19%
XYZ    44.14%

规则

最后要提到的一个API是reglue 。你可以在一个现有的笔记本上使用这个方法,把一个废料 "重新 "粘到当前的笔记本上。你也可以重命名该废料。

如果你想把一些数据传播到另一个将读取当前笔记本的笔记本上,这可能是最有用的。

reglue 的另一个用途是显示视觉元素。

nb.reglue("length", "length2") # new name
nb.reglue("chart")             # will display chart, just like earlier

剪贴簿的一些可能的缺点

使用笔记本来存储你的数据并不是一种优化的存储数据的方式。选择这样的工具有很多潜在的问题。

  • 它显然不能像关系型数据库或对象型数据库那样扩展。
  • 对于那些阅读笔记本代码的人来说,并不清楚有多少数据被持久化了,也不清楚这些数据在哪里。
  • 它也没有很好的工具来支持手动编辑数据,特别是对于更复杂的类型,将是大块的Base64编码的文本。

你不会想用这样的工具来支持由笔记本产生的大量数据。但对于少量的数据,特别是笔记本的简明摘要或输出,它提供了非常理想的功能,即把数据与产生它的笔记本放在一起。

扩展scrapbook

你可以通过编写你自己的编码器来扩展这个框架。这些文件展示了一个简单的例子,所以如果你最终遇到的数据不能用默认的编码器来编码,你可以创建自己的编码器。

总结

Scrapbook是一个有用的小库,用于将笔记本和它们产生的数据保存在一个文件中。它与papermill整合得很好,后者允许你向你的笔记本传递参数。Scrapbook对于运行相互提供数据的多个笔记本的工作流程特别有用。

The postBuilding Jupyter notebook workflows with scrapbookappeared first onwrighters.io.