数据科学思想-全-

数据科学思想(全)

原文:Thoughtful Data Science

协议:CC BY-NC-SA 4.0

零、前言

“开发人员是当今商业中最重要,最有价值的领域,而与行业无关。”

————《The New Kingmakers》作者 Stephen O'Grady

首先,让我感谢您,并祝贺您,读者,决定决定花您宝贵的时间阅读本书。 在接下来的各章中,我将带您从开发人员的角度探索或什至重新发现数据科学的旅程,并将发展本书的主题,即数据科学是一项团队运动,并且,如果获得成功,开发人员将不得不在不久的将来扮演更大的角色,并与数据科学家更好地合作。 但是,为了使数据科学对所有背景和行业的人都具有更大的包容性,我们首先需要通过使数据简单易用使其民主化-这实际上就是本书的目的。

我为什么要写这本书?

正如我将在第 1 章“来自开发人员的数据科学观点”中更详细地解释的那样,我首先是拥有 20 多年开发经验的开发人员,他在构建具有多样化的性质的软件方面有丰富的经验; 前端,后端,中间件等。 回顾这段时间,我意识到,正确地执行算法有多少是我首先想到的事情。 数据始终是别人的问题。 我很少需要分析它或从中提取见解。 充其量,我正在设计正确的数据结构以某种方式加载它,这将使我的算法更有效地运行并且代码更优雅且可重复使用。

但是,随着人工智能和数据科学革命的进行,对我来说很明显像我这样的开发人员需要参与其中,因此在 7 年前的 2011 年,我抓住了机会成为 IBM Watson 核心平台用户界面和工具的首席架构师。 当然,我不假装自己不是机器学习或 NLP 方面的专家。 通过实践学习不能代替获得正式的学术背景。

但是,我想在本书中展示的很大一部分是,使用正确的工具和方法,具备正确的数学基础的人(我只是在谈论高中水平的微积分概念)可以迅速成为该领域的优秀从业者。 成功的关键因素是尽可能简化构建数据管道的不同步骤; 从获取,加载和清理数据到可视化和探索数据,一直到构建和部署机器学习模型。

为了进一步推动使数据更容易被数据科学家以外的社区访问的想法,三年前,我在 IBM Watson Data Platform 团队中担任领导职务,其使命是扩大开发人员社区,它们以一种特殊的教育和行动主义视角处理这些数据。 在此期间,作为首席开发人员倡导者,我开始公开谈论开发人员和数据科学家在更好地协作解决复杂数据问题方面的需求。

注意

注意:在会议和聚会上的讨论中,有时我会遇到麻烦的数据科学家的麻烦,因为他们将我的叙述解释为我说数据科学家不是优秀的软件开发人员。 我想弄清楚这一点,包括数据科学家读者在内,与您相提并论。

大多数数据科学家都是优秀的软件开发人员,具有全面的计算机科学概念知识。 但是,它们的主要目的是解决复杂的数据问题,这些问题需要快速的迭代实验来尝试新事物,而不是编写精美的可重用组件。

但是我不想只讲这个话题。 我也想散散步,并启动了 PixieDust 开源项目,这是我为解决这一重要问题所做的不起眼的贡献。 随着 PixieDust 工作的顺利进行,通过具体的示例应用,叙述变得更加清晰易懂,开发人员和数据科学家都可能对此感到兴奋。

当我有机会写一本关于这个故事的书时,我犹豫了很长时间,然后才开始这次冒险,主要有两个原因:

  • 我曾在博客,文章和教程中广泛撰写过有关我作为 Jupyter 笔记本的数据科学从业者的经验的文章。 在各种会议上,我作为演讲者和研讨会主持人也有丰富的经验。 一个很好的例子是我在 2017 年在 ODSC 伦敦发表的主题演讲,题为《数据科学的未来:更少的权力游戏,更多的联盟》。 但是,我以前从未写过书,也不知道会有多大的承诺,尽管以前曾写过书的朋友多次警告过我。
  • 我希望本书具有包容性,并平等地面向开发人员,数据科学家和业务用户,但我一直在努力寻找实现该目标的正确内容和基调。

最后,开始这项冒险的决定很容易。 在 PixieDust 项目上工作了 2 年后,我感到我们已经通过非常有趣的创新取得了令人瞩目的进展,这些创新引起了开源社区的极大兴趣,并且写书将很好地补充我们在帮助开发人员参与数据科学方面的倡导工作。

附带说明一下,对于正在考虑写书且有类似问题的读者,我只能建议第一个带有大写“是的,继续努力”的读者。 可以肯定的是,这是一项重大承诺,需要付出大量的牺牲,但前提是您要有一个扎实的故事来讲故事,这确实值得您付出努力。

这本书适合谁

本书将服务于对正在发展的数据科学家和开发人员感兴趣的技能开发或任何希望成为专业数据科学家的人员。 通过其创建者介绍的 PixieDust,这本书对于已经完成的数据科学家来说也将是一个很好的桌面伴侣。

不管个人的兴趣水平如何,清晰,易读的文本和真实场景都将适合该领域的人们,因为他们可以在 Jupyter 笔记本中运行 Python 代码。

要生成正常运行的 PixieDust 仪表板,只需要少量的 HTML 和 CSS。 流利的数据解释和可视化也是必要的,因为本书针对的是数据专业人员,例如业务和一般数据分析师。 后面的章节也有很多内容。

这本书涵盖的内容

这本书包含两个大致相等长度的逻辑部分。 在上半年中,我列出了本书的主题,即弥合数据科学与工程学之间的鸿沟的必要性,其中包括有关我提议的 Jupyter + PixieDust 解决方案的深入详细信息。 下半年致力于将我们在上半年中学到的知识应用于四个行业案例。

第 1 章,“开发人员对数据科学的观点”,我尝试通过我自己的经验来定义数据科学,并建立一个数据管道来在 Twitter 上执行帖子情感分析。 我认为这是一项团队运动,并且在大多数情况下,数据科学团队和工程团队之间存在孤岛,这会导致不必要的摩擦,效率低下,最终导致无法充分发挥其潜力。 我还认为,数据科学将继续存在,并最终将成为当今所谓的计算机科学不可或缺的一部分(我想有一天会有新术语出现,例如“计算机数据科学”更好地捕捉了这种双重性)。

第 2 章,“借助 Jupyter 笔记本和 PixieDust”,我开始深入研究流行的数据科学工具,例如 Python 及其专用于数据科学的开源库生态系统,以及 Jupyter 笔记本。 我解释了为什么我认为 Jupyter 笔记本电脑将在未来几年成为大赢家。 我还从简单的display()方法开始介绍 PixieDust 开源库功能,该方法使用户可以通过构建引人注目的图表直观地浏览交互式用户界面中的数据。 使用此 API,用户可以从多个渲染引擎(例如 Matplotlib,Bokeh,Seaborn 和 Mapbox)中进行选择。 display()函数是 PixieDust MVP(最低可行产品)中的唯一功能,但是随着时间的流逝,当我与许多数据科学从业人员进行互动时,我为快速成为 PixieDust 工具箱添加了新功能:

  • sampleData():一个简单的 API,可轻松将数据加载到 Pandas 和 Apache SparkDataFrame
  • wrangle_data():用于清理和按摩数据集的简单 API。 此函数包括使用正则表达式从非结构化文本中提取内容的函数,可将列分解为新的列。 wrangle_data() API 也可以基于预定义的模式提出建议。
  • 包管理器:允许用户在 Python 笔记本中安装第三方 Apache Spark 包。
  • Scala 桥接:使用户能够在 Python 笔记本中运行 Scala 代码。 在 Python 端定义的变量可以在 Scala 中访问,反之亦然。
  • Spark 作业进度监视器:使用实时进度条跟踪您的 Spark 作业状态,该进度条直接显示在正在执行的代码的输出单元格中。
  • PixieApp:提供一个以 HTML/CSS 为中心的编程模型,使开发人员可以构建复杂的仪表板,以对笔记本中内置的分析进行操作。 PixieApps 可以直接在 Jupyter 笔记本中运行,也可以使用 PixieGateway 微服务作为分析 Web 应用进行部署。 PixieGateway 是 PixieDust 的开源配套项目。

下图总结了 PixieDust 的开发过程,包括最近添加的内容,例如 PixieGateway 和 PixieDebugger,这是 Jupyter 笔记本的第一个可视化 Python 调试器:

What this book covers

PixieDust 旅程

从本章中删除的一个关键信息是,PixieDust 首先是一个开源项目,它通过开发人员社区的贡献而生存和呼吸。 就像无数开源项目一样,随着时间的推移,我们可以期望将更多突破性功能添加到 PixieDust 中。

第 3 章,“深入了解 PixieApp”,我将带给读者关于 PixieApp 编程模型的深入了解,并通过分析 GitHub 数据的示例应用说明了每个概念。 我从对 PixieApp 的解剖结构的高级描述开始,包括其生命周期以及带有路由概念的执行流程。 然后,我详细介绍了开发人员如何使用常规 HTML 和 CSS 代码段构建仪表板的 UI,与分析进行无缝交互以及利用 PixieDust display() API 添加复杂的图表的细节。

PixieApp 编程模型是弥合数据科学与工程学之间差距的工具策略的基石,因为它简化了分析操作的流程,从而增加了数据科学家与开发人员之间的协作,并缩短了应用的上市时间 。

第 4 章“使用 PixieGateway 服务器”将 PixieApps 部署到 Web 上,我将讨论 PixieGateway 微服务,使开发人员可以将 PixieApps 发布为分析 Web 应用。 首先,我将展示如何作为 Kubernetes 容器在本地和云上快速部署 PixieGateway 微服务实例。 然后,我将介绍 PixieGateway 管理控制台功能,包括各种配置配置文件以及如何实时监视已部署的 PixieApps 实例和相关的后端 Python 内核。 我还具有 PixieGateway 的图表共享功能,该功能使用户可以将使用 PixieDust display() API 创建的图表转换为团队中任何人都可以访问的网页。

PixieGateway 是一项突破性的创新,具有可以显着加快分析操作速度的潜力,而这正是当今迫切需要的,可以充分利用数据科学的前景。 它代表了市场上已经存在的类似产品的开源替代品,例如 R-Studio 的 Shiny Server 和 Plotly 的 Dash

第 5 章,“最佳做法和高级 PixieDust 概念”,通过研究 PixieApp 编程模型的高级概念,我完成了 PixieDust 工具箱的深入研究:

  • @captureOutput装饰器:默认情况下,PixieApp 路由要求开发人员提供 HTML 片段,该片段将被注入到应用 UI 中。 当我们要调用不了解 PixieApp 架构的第三方 Python 库并直接将输出生成到笔记本时,这是一个问题。 @captureOutput通过自动重定向第三方 Python 库生成的内容并将其封装到适当的 HTML 片段中来解决此问题。
  • 利用 Python 类继承获得更大的模块化和代码重用:将 PixieApp 代码分解为可以使用 Python 类继承功能组合在一起的逻辑类。 我还将展示如何使用pd_app自定义属性调用外部 PixieApp。
  • PixieDust 对流数据的支持:展示 PixieDust display()和 PixieApp 也可以如何处理流数据。
  • 使用 PixieApp 事件实现仪表板下钻:提供一种机制,让 PixieApp 组件发布和订阅用户与 UI 交互时生成的事件(例如,图表和按钮)。
  • 为 PixieDust display() API 构建自定义显示渲染器:遍历扩展 PixieDust 菜单的简单渲染器的代码。 该渲染器显示一个自定义 HTML 表,其中显示了所选数据。
  • 调试技术:介绍 PixieDust 提供的各种调试技术,包括称为 PixieDebugger 的可视化 Python 调试器和用于显示 Python 日志记录消息的%%PixiedustLog魔术。
  • 运行 Node.js 代码的能力:我们讨论pixiedust_node扩展,该扩展管理负责直接从 Python 笔记本中执行任意 Node.js 脚本的 Node.js 进程的生命周期。

由于开源代码模型具有透明的开发过程,并且不断增长的用户社区提供了一些有价值的反馈,因此随着时间的推移,我们能够优先考虑和实现许多这些高级功能。 我要说明的重点是遵循具有适当许可证的开源模型(PixieDust 使用此处提供的 Apache 2.0 许可证运作良好。 它帮助我们发展了用户社区,从而为我们提供了必要的反馈,以优先考虑我们认为具有很高价值的新功能,并在某些情况下以 GitHub 拉取请求的形式提供了代码。

第 6 章“使用 TensorFlow 进行图像识别”,我将深入探讨四个行业案例中的第一个。 我从机器学习的高级介绍开始,然后是机器学习的子领域深度学习和 TensorFlow 框架的介绍,该框架使构建神经网络模型更加容易。 然后我继续构建图像识别示例应用,包括四个部分的相关 PixieApp:

  • 第 1 部分:使用预训练 ImageNet 模型构建图像识别 TensorFlow 模型。 我使用写给诗人的 TensorFlow 教程,展示了如何构建分析以加载和评分神经网络模型。
  • 第 2 部分:创建一个 PixieApp,用于对第 1 部分中创建的分析进行操作。 该 PixieApp 从用户提供的网页 URL 中抓取图像,根据 TensorFlow 模型对图像评分,然后以图形方式显示结果。
  • 第 3 部分:我展示了如何直接在笔记本中集成 TensorBoard 图形可视化组件,并提供了调试神经网络模型的功能。
  • 第 4 部分:我展示了如何使用自定义训练数据重新训练模型,以及如何更新 PixieApp 来显示两个模型的结果。

我决定使用 TensorFlow 进行深度学习图像识别,以开始一系列示例应用,因为这是一个日益流行的重要用例,并演示了如何构建模型并将其部署在同一笔记本中的应用中,这是缩小数据科学与工程之间差距的主题。

第 7 章,“大数据 Twitter 情感分析”,我谈到在 Twitter 规模上进行自然语言处理。 在本章中,我将展示如何使用 IBM Watson Natural Language 了解基于云的服务对推文进行情感分析。 这非常重要,因为它提醒读者,重用托管托管服务而不是内部构建功能有时可能是一个有吸引力的选择。

我首先介绍 Apache Spark 并行计算框架,然后继续分为四个部分构建应用:

  • 第 1 部分:使用 Spark 结构化流获取 Twitter 数据
  • 第 2 部分:从文本中提取情感和最相关的实体来丰富数据
  • 第 3 部分:通过创建实时仪表板 PixieApp 来对分析进行操作。
  • 第 4 部分:一个可选部分,使用 Apache Kafka 和 IBM Streaming Designer 托管服务重新实现应用,以演示如何添加更大的可伸缩性。

我认为读者(尤其是不熟悉 Apache Spark 的读者)将喜欢本章,因为它比上一章更容易理解。 关键要点是如何构建可通过连接到 Spark 集群的 Jupyter 笔记本进行扩展的分析。

第 8 章,“金融时间序列分析和预测”,我谈论时间序列分析,它是数据科学中非常重要的领域,在行业中有许多实际应用。 本章首先深入探讨 NumPy 库,它是许多其他库(例如 Pandas 和 SciPy)的基础。 然后,我继续构建示例应用,该应用分析了由历史库存数据组成的时间序列,分为两个部分:

  • 第 1 部分:提供时间序列的统计信息,包括各种图表,例如自相关函数(ACF)和部分自相关函数(PACF)
  • 第 2 部分:使用statsmodels Python 库,基于 ARIMA 算法构建预测模型

时间序列分析是数据科学的一个重要领域,我认为它被低估了。 在撰写本章时,我个人学到了很多东西。 我当然希望读者也能喜欢它,并且阅读它会激发人们对这个伟大话题的更多了解。 如果是这样,我还希望您能说服您在接下来的时间序列分析学习中尝试 Jupyter 和 PixieDust。

第 9 章,“使用图的美国国内航班数据分析”,我通过图的研究完成了这一系列行业用例。 我选择了一个用于分析航班延误的示例应用,因为该数据很容易获得,并且非常适合使用图形算法(嗯,为了全面披露,我可能还选择了它,因为我已经编写了一个类似的应用来预测航班延误,基于我使用 Apache Spark MLlib 的天气数据)。

我首先介绍图和相关图算法,包括几种最流行的图算法,例如广度优先搜索和深度优先搜索。 然后,我继续介绍用于构建示例应用的networkx Python 库。

该应用由四个部分组成:

  • 第 1 部分:显示如何将美国国内航班数据加载到图形中。
  • 第 2 部分:创建USFlightsAnalysis PixieApp,该应用使用户可以选择始发地和目的地机场,然后根据选定的中心性显示两个机场之间最短路径的 Mapbox 地图
  • 第 3 部分:将数据浏览添加到 PixieApp,其中包括飞往选定起点机场的每家航空公司的各种统计信息
  • 第 4 部分:使用在第 8 章,“金融时间序列分析和预测”中学习的技术,建立用于预测航班延误的 ARIMA 模型

图论也是数据科学的另一个重要且不断发展的领域,本章很好地介绍了该系列文章,我希望该系列文章提供一组多样化且具有代表性的行业用例。 对于对使用大数据图形算法特别感兴趣的读者,我建议您查看 Apache Spark GraphX,它使用非常简单灵活的 API 来实现许多图形算法。

第 10 章,“最终见解”结束时,我给出了简要摘要并解释了我对 Drew's Conway 维恩图的理解。 然后,我将讨论 AI 和数据科学的未来,以及公司如何为 AI 和数据科学革命做好准备。 另外,我还列出了一些很好的参考资料供进一步学习。

附录,“PixieApp 快速参考”是开发人员快速参考指南,提供了所有 PixieApp 属性的摘要。 这将在适当的示例的帮助下解释各种注释,自定义 HTML 属性和方法。

但是关于介绍的内容已经足够了:让我们从第一个章节“开发人员的数据科学观点”开始我们的旅程。

要充分利用这本书

  • 遵循该示例所需的大多数软件都是开源的,因此可以免费下载。 全书提供了说明,从安装包括 Jupyter 笔记本服务器的 anaconda 开始。
  • 在第 7 章,“大数据 Twitter 情感分析”中,示例应用需要使用 IBM Watson 云服务,包括 NLU 和 Streams Designer。 这些服务带有免费套餐,足以按照示例进行操作。

下载示例代码文件

您可以从这个页面从您的帐户下载本书的示例代码文件。 如果您在其他地方购买了这本书,则可以访问这个页面并注册以将文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册这个页面
  2. 选择支持标签。
  3. 单击代码下载&勘误表
  4. 搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。

下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:

  • Windows 的 WinRAR/7-Zip
  • 适用于 Mac 的 Zipeg/iZip/UnRarX
  • 适用于 Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 的这个页面。 我们还从这个页面提供了丰富的书籍和视频目录中的其他代码包。 去看一下!

下载彩色图像

我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载

使用的约定

本书中使用了许多文本约定。

CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 例如:“您可以使用{%if ...%}...{%elif ...%}...{%else%}…{%endif%}表示法有条件地输出文本。”

代码块设置如下:

import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df

当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:

import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df

任何命令行输入或输出的编写方式如下:

jupyter notebook --generate-config

粗体:表示新的术语,重要的单词或您在屏幕上看到的单词,例如在菜单或对话框中,也显示在这样的文本中。 例如:“下一步是创建一个使用用户值并返回结果的新路由。该路由将由 Submit Query 按钮调用。”

注意

警告或重要提示如下所示。

提示

提示和技巧如下所示。

一、开发人员对数据科学的看法

“数据是一件宝贵的事情,并且会比系统本身持续更长时间。”

——万维网的发明者 Tim Berners-Lee

在本介绍性章节中,我将通过尝试回答一些基本问题来开始对话,这些基本问题有望为本书的其余部分提供上下文和清晰性:

  • 什么是数据科学,为什么它呈上升趋势
  • 为什么数据科学将继续存在
  • 为什么开发人员需要参与数据科学

作为开发人员和最近的数据科学从业者,我将使用自己的经验,讨论我所从事的具体数据管道项目以及从这项工作中得出的数据科学策略,该策略包括三个支柱:数据,服务和工具 。 在本章的结尾,我将介绍 Jupyter 笔记本,这是我在本书中提出的解决方案的核心。

什么是数据科学

如果您在网上搜索数据科学的定义,肯定会发现很多。 这反映了一个事实,即数据科学对不同的人意味着不同的事物。 关于数据科学家究竟要做什么以及他们必须接受什么培训尚无真正的共识; 这全都取决于他们要完成的任务,例如数据收集和清理,数据可视化等等。

现在,我将尝试使用一个通用的,希望是一致同意的定义:数据科学是指分析大量数据以提取知识和见解以导致可采取行动的决策的活动。 但是它仍然很模糊。 有人会问我们在谈论什么样的知识,洞察力和可行的决策?

为了确定对话的方向,我们将范围缩小到数据科学的三个领域:

  • 描述性分析:数据科学与相关联,它与信息检索和数据收集技术相关,目的是重构过去的事件以识别模式并找到有助于理解发生了什么以及导致它发生的原因的见解。 这样的一个示例是按区域查看销售数据和人口统计数据,以对客户偏好进行分类。 这部分需要熟悉统计和数据可视化技术。
  • 预测性分析:数据科学是预测某些事件当前正在发生或将来会发生的可能性的方法。 在这种情况下,数据科学家会查看过去的数据以查找解释变量并建立统计模型,该模型可应用于我们试图预测其结果的其他数据点,例如,预测信用卡交易发生实时欺诈的可能性。 这部分通常与机器学习领域相关。
  • 规范性分析:在这种情况下,数据科学被视为做出更好决策的一种方式,或者我应该说是数据驱动的决策。 想法是考虑多种选择并使用模拟技术来量化和最大化结果,例如,通过着眼于最小化运营成本来优化供应链。

从本质上讲,描述性数据科学回答了问题(数据告诉我),预测性数据科学回答了为什么(数据以某种方式发生)的问题,并且规范性数据科学回答了如何(我们朝着特定目标优化数据)的问题。

数据科学将继续存在吗?

让我们从一开始就直截了当地:我强烈认为答案是肯定的。

但是,并非总是如此。 几年前,当我第一次听说数据科学作为概念时,我最初认为这是描述该行业中已经存在的一项活动的又一个营销流行语:商业智能BI)。 作为主要从事解决复杂系统集成问题的开发人员和架构师,可以很容易地使自己确信我不需要直接参与数据科学项目,即使很明显他们的数量正在增加 ,原因是开发人员传统上将数据管道视为黑箱,可通过定义明确的 API 对其进行访问。 但是,在过去的十年中,我们已经看到学术界和行业对数据科学的兴趣呈指数增长,这一点很明显,这种模式是不可持续的。

随着数据分析在公司的运营过程中发挥越来越大的作用,开发人员的作用也得到了扩展,以更接近算法并构建在生产中运行它们的基础结构。 数据科学已成为新的淘金热的另一项证据是数据科学家职位的飞速增长,该职位已连续两年在 Glassdoor 上排名第一,雇主确实在 Indeed 上发布的最多。 猎头公司也在 LinkedIn 和其他社交媒体平台上四处寻觅,向有资料显示任何数据科学技能的人发送大量招聘信息。

对这些新技术进行所有投资的主要原因之一是希望它将带来重大改进并提高业务效率。 但是,尽管这是一个不断发展的领域,但当今企业中的数据科学仍然仅限于实验,而不是像人们期望的那样大肆宣传。 如果数据科学正在逐渐消亡并最终消灭另一种技术泡沫,那么这使许多人感到不安。

这些都是好点,但我很快意识到这不仅仅是一种过时的时尚; 我领导的越来越多的项目包括将数据分析集成到核心产品功能中。 最后,这是 IBM Watson Question Answering 系统在 Jeopardy 与两个经验丰富的冠军对决,我深信数据科学以及与云,大数据和人工智能AI)将会留下来,并最终改变我们对计算机科学的思考方式。

数据科学为何在兴起?

数据科学的迅猛发展涉及多个因素。

首先,收集的数据量一直以指数速度增长。 根据 IBM Marketing Cloud 的最新市场研究,大约每天创建了 2.5 亿个字节(让您知道它有多大,即 25 亿个字节),但只分析了这些数据的一小部分,却遗漏了许多机会。

其次,我们正处于几年前开始的认知革命之中。 几乎每个行业都在追赶 AI 潮流,其中包括自然语言处理NLP)和机器学习。 尽管这些领域已经存在了很长时间,但它们最近重新受到关注,以至于它们现已成为大学中最受欢迎的课程之一,并且在开源活动中获得了最大份额。 显然,如果要生存,公司就需要变得更加敏捷,更快地发展并转变为数字业务,并且随着可供决策的时间越来越接近实时,它们必须完全具备数据驱动功能。 如果您还包括 AI 算法需要高质量数据(以及很多数据)才能正常工作的事实,我们就可以开始理解数据科学家扮演的关键角色。

第三,随着云技术的进步以及平台即服务PaaS)的开发,访问大型计算引擎和存储从未如此简单或便宜。 曾经是大公司的权限的大数据工作负载现在可用于较小的组织或拥有信用卡的任何个人; 反过来,这也促进了创新的全面发展。

由于这些原因,毫无疑问,类似于 AI 革命,数据科学将继续存在,并且其增长将持续很长时间。 但是我们也不能忽视这样一个事实,即数据科学尚未充分发挥其潜力并产生了预期的结果,特别是在帮助公司转变为数据驱动型组织的过程中。 通常,挑战是实现下一步,即将数据科学和分析转变为一项核心业务活动,最终实现清晰,明智,明智的业务决策。

与开发者有什么关系?

这是一个非常重要的问题,在接下来的章节中我们将花费大量时间进行开发。 让我回顾一下我的职业生涯。 我从事开发人员的大部分时间都可以追溯到 20 多年前,从事计算机科学的许多方面。

我首先构建了各种工具,这些工具通过自动将用户界面翻译成多种语言的过程来帮助软件国际化。 然后我研究了用于 Eclipse 的 LotusScript(Lotus Notes 的脚本语言)编辑器,该编辑器将直接与基础编译器交互。 该编辑器提供了一流的开发功能,例如提供建议的内容辅助,实时语法错误报告等。 然后,我花了几年时间为 Lotus Domino 服务器构建基于 Java EE 和 OSGI 的中间件组件。 在此期间,我领导一个团队,通过将 Lotus Domino 编程模型引入当时可用的最新技术来对其进行现代化。 我对软件开发,前端,中间件,后端数据层,工具等的各个方面都很满意; 我被某些人称为全栈开发人员。

直到我看到 IBM Watson Question Answering 系统的演示,它在 2011 年在 Jeopardy 游戏中击败了长期冠军 Brad Rutter 和 Ken Jennings。哇! 这是一个突破性的技术,它是一种能够回答自然语言问题的计算机程序。 我很感兴趣,在进行了一些研究之后,与参与该项目的一些研究人员会面,并了解了用于构建该系统的技术,例如 NLP,机器学习和通用数据科学,我意识到了如果将技术应用于业务的其他部分,这种技术有多少潜力。

几个月后,我有机会加入 IBM 新组建的 Watson 部门,领导一个工具团队,其任务是为 Watson 系统建立数据提取和准确率分析功能。 我们最重要的要求之一就是确保我们的客户易于使用这些工具,这就是为什么回想起来,将这一责任赋予开发人员团队是正确的做法。 从我的角度来看,从事这项工作既充满挑战,又富有。 我离开了一个熟悉的世界,在那里我擅长基于众所周知的模式设计架构并实现前端,中间件或后端软件组件,而这个世界主要专注于处理大量数据; 获取,清理,分析,可视化并构建模型。 我花了前六个月的时间从消防水龙头喝酒,阅读和学习有关 NLP,机器学习,信息检索和统计数据科学的知识,至少足以使用我正在构建的功能。

那时,我与研究团队进行了互动,将这些算法推向市场,我意识到开发人员和数据科学家需要更好地协作的重要性。 的传统方法是让数据科学家孤立地解决复杂的数据问题,然后将结果“扔给开发人员”,让开发人员对其进行操作是不可持续的,并且无法扩展,考虑到要处理的数据量保持指数级增长,所需的上市时间不断缩小。

相反,他们的角色需要转向一个团队,这意味着数据科学家必须像软件开发人员那样工作和思考,反之亦然。 确实,这在纸上看起来非常好:一方面,数据科学家将受益于久经考验的软件开发方法(例如敏捷)及其快速迭代和频繁的反馈方法,而且还将受益于符合企业需求的严格的软件开发生命周期,例如安全性,代码审查,源代码控制等。 另一方面,开发人员将开始以新的方式考虑数据:分析旨在发现见解,而不仅仅是具有查询和 CRUD API 的持久层(创建读取更新删除的缩写)。

将这些概念付诸实践

在担任 Watson Core Tooling 首席架构师为 Watson Questioning System 构建自助工具 4 年之后,我加入了 Watson Data Platform 组织的 Developer Advocacy 团队,该团队的任务扩展是创建一个平台,来将产品组合带给 IBM 公共云的数据和认知服务。 我们的任务非常简单:赢得开发人员的支持并帮助他们在数据和 AI 项目上取得成功。

作品有多个方面:教育,传福音和行动主义。 前两个非常简单,但是行动主义的概念与此讨论有关,值得更详细地说明。 顾名思义,行动主义就是在需要变革的地方带来变革。 对于我们的由 15 名开发人员拥护者组成的团队来说,这意味着在开发人员尝试使用数据时(无论他们是刚刚起步还是已经在运行高级算法),他们步履蹒跚,他们感到痛苦,并确定了应该解决的差距。 为此,我们构建了具有现实用例的大量示例数据管道并使其开源。

这些项目至少都需要满足三个要求:

  • 用作输入的原始数据必须公开可用
  • 提供清晰的说明以在合理的时间内在云上部署数据管道
  • 开发人员应该能够将项目用作类似情况的起点,也就是说,代码必须具有高度可定制性和可重用性

我们从这些练习中获得的经验和见解非常宝贵:

  • 了解哪种数据科学工具最适合每个任务
  • 最佳实践框架和语言
  • 部署和操作分析的最佳实践架构

指导我们选择的指标很多:准确率,可伸缩性,代码可重用性,但最重要的是,改善了数据科学家与开发人员之间的协作。

深入探讨具体示例

早期,我们想建立一个数据管道,通过对包含特定主题标签的推文进行情感分析,从 Twitter 提取见解,并将结果部署到实时仪表板中。 该应用是我们的理想起点,因为数据科学分析不太复杂,并且该应用涵盖了现实生活场景的许多方面:

  • 高容量,高吞吐量的流数据
  • 将 NLP 情感分析用于数据丰富
  • 基本数据汇总
  • 数据可视化
  • 部署到实时仪表板

为了进行试验,第一个实现是一个简单的 Python 应用,该应用使用 tweepy 库(Python 的官方 Twitter 库)连接到 Twitter 并获得一系列推文和 textblob(用于基本 NLP 的简单 Python 库),以丰富情感分析。

然后将结果保存到 JSON 文件中进行分析。 这个原型是使事情开始并快速进行实验的好方法,但是经过几次迭代,我们很快意识到我们需要认真对待并构建满足企业需求的架构。

数据管道蓝图

在较高的层次上,可以使用以下通用蓝图来描述数据管道:

Data pipeline blueprint

数据管道工作流程

数据管道的主要目标是在可扩展,可重复的过程中以高度自动化的方式来操作(即提供直接业务价值)数据科学分析结果。 分析的示例可以是一个推荐引擎,以诱使消费者购买更多产品,例如,亚马逊推荐的列表,或者显示可以帮助以下方面的信息的仪表板:关键表现指标KPI)。 首席执行官为公司制定未来决策。

数据管道的构建涉及多个人:

  • 数据工程师:他们负责设计和操作信息系统。 换句话说,数据工程师负责与数据源进行接口,以原始格式获取数据,然后对其进行按摩(有些人称此数据为问题),直到准备好进行分析为止。 在 Amazon 推荐系统示例中,他们将实现流处理管道,该流处理管道从记录的电子商务系统中捕获并汇总特定的消费者交易事件并将其存储到数据仓库中。
  • 数据科学家:他们分析数据并建立提取见解的分析方法。 在我们的 Amazon 推荐系统示例中,他们可以使用连接到数据仓库的 Jupyter 笔记本加载数据集并使用协同过滤算法
  • 开发人员:他们负责将分析操作化为针对业务用户(业务分析师,C-Suite,最终用户等)的应用。 同样,在 Amazon 推荐系统中,开发人员将在用户完成购买或通过定期电子邮件后显示推荐产品的列表。
  • 业务用户:包括使用数据科学分析输出的所有用户,例如,业务分析人员分析仪表板以监视业务的健康状况,或者使用提供以下建议的应用监视最终用户: 接下来要买什么。

注意

在现实生活中,同一个人扮演的角色不止这里描述的一种,这并不罕见。 这可能意味着一个人在与数据管道进行交互时有多种不同的需求。

如上图所示,构建数据科学管道在本质上是迭代的,并且遵循定义明确的流程:

  1. 获取数据:此步骤包括从各种来源获取原始格式的数据:结构化(RDBMS,记录系统等)或非结构化(网页,报告等):

    • 数据清理:检查完整性,填充丢失的数据,修复错误的数据,以及清除数据
    • 数据准备:充实,检测/删除异常值并应用业务规则
  2. 分析:此步骤将描述性活动(理解数据)和描述性活动(构建模型)结合在一起:

    • 探索:查找统计属性,例如中央趋势,标准差,分布和变量识别,例如单变量和双变量分析,变量之间的相关性等。
    • 可视化:此步骤对于正确分析数据并形成假设非常重要。 可视化工具应提供合理水平的交互性,以促进对数据的理解。
    • 构建模型:应用推断统计信息来形成假设,例如为模型选择特征。 此步骤通常需要专业知识,并且需要大量解释。
  3. 部署:将分析阶段的输出操作化:

    • 沟通:生成报告和仪表板,以清楚地传达分析输出,以供业务部门用户使用(C-Suite,业务分析师等)
    • 发现:设置业务结果目标,着重于发现可以带来新收入来源的新见解和商机
    • 实现:为最终用户创建应用
  4. 测试:此活动应真正包含在的每个步骤中,但是在这里,我们谈论的是根据现场使用情况创建反馈回路:

    • 创建衡量模型的准确率的指标
    • 优化模型,例如,获取更多数据,查找新功能等等

成为数据科学家需要什么样的技能?

在行业中,现实情况是数据科学太新了,公司还没有一个明确的职业道路。 您如何获得数据科学家职位的聘用? 需要多少年的经验? 您需要具备哪些技巧? 数学,统计学,机器学习,信息技术,计算机科学等等?

好吧,答案可能是所有事情的一点点,再加上一项更关键的技能:特定领域的专业知识。

关于是否将通用数据科学技术应用于任何数据集而没有深入了解其含义的争论正在展开,这是否会导致期望的业务成果。 许多公司都倾向于确保数据科学家拥有大量的领域专业知识,其基本原理是,如果没有它,您可能会在不知不觉中在任何步骤上引入偏见,例如在填补数据清理阶段或特征选择过程中的空白时,以及最终建立的模型很适合给定的数据集,但最终仍然毫无价值。 想象一下,一位没有化学背景的数据科学家,正在为一家开发新药的制药公司研究有害分子之间的相互作用。 这也可能就是为什么我们看到专门针对特定领域的统计课程的繁多的原因,例如生物学的生物统计学,或用于分析与供应链相关的运营管理的供应链分析等等。

总而言之,数据科学家理论上应该在以下方面有所精通:

  • 数据工程/信息检索
  • 计算机科学
  • 数学与统计
  • 机器学习
  • 数据可视化
  • 商业情报
  • 特定领域的专业知识

注意

如果您正在考虑学习这些技能,但又没有时间参加传统课程,我强烈建议您使用在线课程。

我特别推荐此课程

经典的 Drew 的 Conway Venn 图很好地展示了什么是数据科学以及为什么数据科学家有点独角兽:

What kind of skills are required to become a data scientist?

德鲁的康威数据科学维恩图

到现在为止,我希望可以很清楚地发现,符合上述描述的完美数据科学家更多地是个例外,而不是规范,而且角色通常涉及多个角色。 是的,是的,我要提出的观点是数据科学是一项团队运动,这一想法将贯穿本书。

IBM Watson DeepQA

IBM DeepQA 研究项目就是一个例证,它证明了数据科学是一项团队运动的想法,该项目源于 IBM 的一项巨大挑战,即要建立一个能够针对预定的域知识回答自然语言问题的人工智能系统。 问答系统QA)应该足够好,可以与 Jeopardy 受欢迎的电视游戏节目。

众所周知,被称为 IBM Watson 的该系统在 2011 年赢得了对抗两个最老牌 Jeopardy 冠军:肯·詹宁斯和布拉德·鲁特。 以下照片是从 2011 年 2 月播出的实际游戏中拍摄的:

IBM Watson DeepQA

IBM Watson 在 Jeopardy 中与 Ken Jennings 和 Brad Rutter 作战!

资料来源:https://upload.wikimedia.org/wikipedia/e

正是在这段时间里,我与构建 IBM Watson QA 计算机系统的研究团队进行了互动,我仔细研究了 DeepQA 项目架构,并亲眼目睹了实际用于多少数据科学领域。

下图描述了 DeepQA 数据管道的高级架构:

IBM Watson DeepQA

沃森 DeepQA 架构图

资料来源:https://researcher.watson.ibm.com/researcher/files/us-mi

如上图所示,用于回答问题的数据管道由以下高级步骤组成:

  1. 问题和主题分析(自然语言处理):此步骤使用深度解析组件,该组件检测组成问题的单词之间的依存关系和层次结构。 目标是对问题有更深入的了解并提取基本属性,例如:

    • 焦点:问题是什么?
    • 词汇答案类型LAT):预期答案的类型是什么,例如,一个人,一个地点等。 在为候选答案评分时,此信息非常重要,因为它为与 LAT 不匹配的答案提供了早期过滤器。
    • 命名实体解析:这将实体解析为标准化名称,例如,Big AppleNew York
    • 回指解析度:这将代词与该问题的先前用语联系起来,例如,句子On Sept. 1, 1715 Louis XIV died in this city, site of a fabulous palace he built所建立的代词he是指路易十四。
    • 关系检测:这可以检测问题中的关系,例如,She divorced Joe DiMaggio in 1954,其中的关系是Joe DiMaggio Married X。 这些类型的关系(主谓宾)可用于查询三元组存储并产生高质量的候选答案。
    • 问题类别:这会将问题映射到 Jeopardy 中使用的预定义类型之一,例如,类事实,多项选择题,难题等。
  2. 主要搜索和假设生成(信息检索):此步骤在很大程度上依赖于问题分析步骤的结果,以组装适用于不同可用答案源的一组查询。 答案源的示例包括各种全文本搜索引擎,例如 IndriApache Lucene/Solr,面向文档和面向标题的搜索(Wikipedia),三重存储等等。 然后,搜索结果用于生成候选答案。 例如,面向标题的结果将直接用作候选,而文档搜索将需要对段落进行更详细的分析(再次使用 NLP 技术)以提取可能的候选答案。

  3. 假设和证据评分(NLP 和信息检索):对于每个候选答案,将进行另一轮搜索,以使用不同的评分技术来查找其他支持证据。 此步骤还用作预筛选测试,其中消除了一些候选答案,例如与步骤 1 中计算得出的 LAT 不匹配的答案。此步骤的输出是一组机器学习特征,对应于找到的支持性证据。 这些特征将用作一组机器学习模型的输入,以对候选答案进行评分。

  4. 最终合并和评分(机器学习):在此最后步骤中,系统识别出同一答案的变体并将其合并在一起。 它还使用步骤 3 中生成的特征,使用机器学习模型来选择按其各自分数排名的最佳答案。这些机器学习模型已经针对一组代表性问题进行了训练,这些问题具有针对已被收集的文档的正确答案。 预先吃了。

当我们继续讨论数据科学和 AI 如何改变计算机科学领域时,我认为了解最新技术水平非常重要。 IBM 沃森(Watson)是这些旗舰项目之一,为我们在 Jeopardy 游戏中击败肯·詹宁斯(Ken Jennings)和布拉德·鲁特(Brad Rutter)奠定了基础,为我们取得更大的进步铺平了道路。

返回我们对 Twitter hashtags 项目的情感分析

我们构建的快速数据流水线原型使我们对数据有了很好的了解,但是随后我们需要设计更健壮的架构并使应用企业做好准备。 我们的主要目标仍然是获得构建数据分析的经验,而不是在数据工程部分花费太多时间。 这就是为什么我们尝试尽可能地利用开源工具和框架的原因:

  • Apache Kafka:这是一个可扩展的流平台,用于以可靠且容错的方式处理大量推文 。
  • Apache Spark:这是一个内存中的集群计算框架。 Spark 提供了可简化并行计算复杂性的编程接口。
  • Jupyter 笔记本:这些基于 Web 的交互式文档(笔记本)使用户可以远程连接到计算环境(内核)以创建高级数据分析。 Jupyter Kernels 支持多种编程语言(Python,R,Java/Scala 等)以及多种计算框架(Apache Spark,Hadoop 等)。

在情感分析部分,我们决定将使用 textblob Python 库编写的代码替换为 Watson Tone Analyzer 服务,这是一项基于云的休息服务,可提供高级情感分析,包括情感,语言和社交语气的检测。 即使 Tone Analyzer 不是开源的,也可以在 IBM Cloud 上使用可用于开发和试用的免费版本。

我们的架构现在看起来像这样:

Back to our sentiment analysis of Twitter hashtags project

Twitter 情感分析数据管道架构

在上图中,我们可以将工作流分解为以下步骤:

  1. 产生一系列推文并将其发布到 Kafka 主题中,可以将其视为将事件分组在一起的渠道。 反过来,接收者组件可以订阅此主题/频道以使用这些事件。
  2. 通过情感,语言和社交语调得分丰富推文:使用 Spark Streaming 从组件1订阅 Kafka 主题,然后将文本发送到 Watson Tone Analyzer 服务。 将得到的色调分数添加到数据中,以进行进一步的下游处理。 该组件使用 Scala 实现,为方便起见,使用 Jupyter Scala 笔记本运行。
  3. 数据分析和探索:在这一部分,我们决定使用 Python 笔记本仅仅是因为 Python 提供了更具吸引力的库生态系统,尤其是在数据可视化方面。
  4. 将结果发布回 Kafka。
  5. 将实时仪表板实现为 Node.js 应用。

由三个人组成的团队,花了我们大约 8 周的时间才能使仪表板使用实时 Twitter 情感数据。 的原因很长,原因有很多:

  • Kafka 和 Spark Streaming 等一些框架和服务对我们来说是新的,我们必须学习如何使用它们的 API。
  • 仪表板前端使用 Mozaïk 框架作为独立的 Node.js 应用构建,这使构建强大的实时仪表板变得容易。 但是,我们发现代码存在一些局限性,迫使我们不得不深入研究实现并编写补丁程序,从而增加了总体进度的延迟。

结果显示在以下屏幕截图中:

Back to our sentiment analysis of Twitter hashtags project

Twitter 情感分析实景仪表板

建立第一个可用于企业的数据管道的经验教训

充分利用开源框架,库和工具绝对可以帮助我们更高效地实现数据管道。 例如,Kafka 和 Spark 非常易于部署且易于使用,当我们陷入困境时,我们始终可以通过使用问答网站(例如 StackOverflow)来依靠开发人员社区寻求帮助。

另一个不错的选择是使用基于云的托管服务进行情感分析,例如 IBM Watson Tone Analyzer。 它使我们能够抽象出训练和部署模型的复杂性,从而使整个步骤比我们自己实现的过程更可靠,当然也更准确。

集成起来非常容易,因为我们只需要发出 REST 请求(也称为 HTTP 请求,请参阅这个页面了解有关 REST 架构的更多信息 )以获取答案。 现在,大多数现代 Web 服务都符合 REST 架构,但是,我们仍然需要了解每种 API 的规范,这可能需要很长时间才能正确。 通常,通过使用 SDK 库可以简化此步骤,该 SDK 库通常是免费提供的,并且以 Python,R,Java 和 Node.js 等大多数流行语言提供。 通过提取生成 REST 请求的代码,SDK 库提供对服务的更高级别的编程访问。 SDK 通常会提供一个代表服务的类,其中每种方法都将封装 REST API,同时注意用户认证和其他标头。

在工具方面,Jupyter 笔记本给我们留下了深刻的印象,它提供了出色的功能,例如协作和完全交互性(我们将在后面详细介绍笔记本)。

但是,并非所有事情都很顺利,因为我们在几个关键领域苦苦挣扎:

  • 为某些关键任务(例如数据充实和数据分析)选择哪种编程语言。 即使团队经验很少,我们最终还是使用了 Scala 和 Python,这主要是因为它们在数据科学家中非常受欢迎,并且还因为我们想学习它们。
  • 创建用于数据探索的可视化需要太多时间。 使用可视化库(例如 Matplotlib 或 Bokeh)编写简单的图表需要编写太多代码。 反过来,这减慢了我们进行快速实验的需求。
  • 将分析运入实时仪表板太难了,无法扩展。 如前所述,我们需要编写一个成熟的独立 Node.js 应用,该应用使用来自 Kafka 的数据,并需要在 IBM Cloud 上部署为云铸造应用 。 可以理解,这项任务需要很长的时间才能完成第一次,但是我们也发现很难更新。 将数据写入 Kafka 的分析更改也需要与仪表板应用上的更改同步。

数据科学策略

如果数据科学要继续发展并逐步发展成为一项核心业务活动,则企业必须找到一种方法来在整个组织的各个层次上对其进行扩展,并克服我们前面讨论的所有困难挑战。 为了达到此目的,我们确定了计划数据科学策略的架构师应重点关注的三个重要支柱,即数据,服务和工具:

Data science strategy

数据科学的三大支柱

  • 数据是您最宝贵的资源:您需要一种适当的数据策略,以确保数据科学家可以轻松访问所需的精选内容。 正确地对数据进行分类,设置适当的管理策略以及使元数据可搜索,将减少数据科学家花费在获取数据然后请求使用数据的时间。 这不仅将提高他们的生产率,还将提高他们的工作满意度,因为他们将花费更多的时间进行实际的数据科学工作。

    设置一种数据策略,使数据科学家能够轻松访问与其相关的高质量数据,从而提高了工作效率和士气,并最终提高了成功率。

  • 服务:每个计划进行数据科学的架构师都应该考虑面向服务的架构SOA)。 与将所有功能捆绑到一个部署中的传统整体式应用相反,面向服务的系统将功能分解为服务,这些服务旨在完成一些事情,但要做好,并具有高性能和可伸缩性。 然后,这些系统彼此独立部署和维护,从而为整个应用基础结构提供可伸缩性和可靠性。 例如,您可能有一项服务,该服务运行用于创建深度学习模型的算法,另一项服务将持久化模型并让应用运行它以对客户数据进行预测,依此类推。

    优势显而易见:可重用性高,易于维护,缩短上市时间,可扩展性等等。 此外,这种方法非常适合云策略,因为工作负载的大小超出了现有容量,因此可以为您提供增长的途径。 您还希望优先考虑开源技术,并尽可能地在开放协议上进行标准化。

    将进程分解为较小的功能可将可伸缩性,可靠性和可重复性注入系统。

  • 工具确实很重要!如果没有适当的工具,某些任务将变得非常难以完成(至少这是我用来解释为什么无法在房子周围固定东西的理由)。 但是,您还希望保持工具的简单,标准化和合理集成,以便不熟练的用户可以使用它们(即使我获得了正确的工具,我也不确定我是否能够完成房屋装修任务,除非它足够简单才能使用)。 一旦减少使用这些工具的学习曲线,非数据科学家用户将更容易使用它们。

    使工具更易于使用有助于打破孤岛,并增强数据科学,工程和业务团队之间的协作。

Jupyter 笔记本是我们战略的中心

本质上,笔记本是由可编辑单元格组成的 Web 文档,可让您针对后端引擎交互地运行命令。 顾名思义,我们可以将它们视为纸质便签本的数字版本,用于书写笔记和实验结果。 该概念同时非常强大和简单:用户以他/她选择的语言输入代码(大多数笔记本的实现都支持多种语言,例如 Python,Scala,R 等),运行单元,并在成为文档一部分的单元格下方的输出区域中以交互方式获取结果。 结果可以是任何类型:文本,HTML 和图像,这非常适合以图形方式显示数据。 就像将与传统 REPL读取-解释-打印-循环的程序)一起使用,是因为笔记本可以连接到功能强大的计算引擎(例如 Apache SparkPython Dask 集群使您可以进行大数据实验(如果需要)。

在笔记本中,在下面的单元格中可以看到在单元格中创建的任何类,函数或变量,从而使您能够逐段编写复杂的分析,迭代测试假设并解决问题,然后再进行下一阶段。 此外,用户还可以使用流行的 Markdown 语言编写富文本格式,或者使用 LaTeX 编写数学表达式,以便为其他用户描述实验。

下图显示了示例 Jupyter 笔记本的各个部分,其中包含 Markdown 单元格以解释实验内容,用 Python 编写的可创建 3D 图的代码单元以及实际的 3D 图表结果:

Jupyter Notebooks at the center of our strategy

充足的 Jupyter 笔记本

为什么笔记本如此受欢迎?

在过去的几年中,笔记本电脑作为与数据科学相关的活动的首选工具,其受欢迎程度迅猛增长。 有很多原因可以解释它,但我认为主要的原因是它的多功能性,使其成为数据科学家必不可少的工具,不仅对于构建数据管道的大多数角色(包括业务分析师和开发人员)都是必不可少的工具。

对于数据科学家而言,笔记本是进行迭代实验的理想之选,因为它使他们能够快速加载,浏览和可视化数据。 笔记本也是一种出色的协作工具; 它们可以导出为 JSON 文件,并可以在团队中轻松共享,从而可以重复进行相同的实验并在需要时进行调试。 此外,由于笔记本也是 Web 应用,因此可以轻松地将它们集成到基于云的多用户环境中,从而提供更好的协作体验。

这些环境还可以通过使用 Apache Spark 等框架将笔记本电脑与计算机集群连接,从而按需访问大型计算资源。 这些基于云的笔记本服务器的需求正在快速增长,因此,我们看到越来越多的 SaaS软件即服务)解决方案,以商业化为例,都使用 IBM Data Science ExperienceDataBricks 并使用 JupyterHub 开源代码。

对于业务分析师来说,笔记本电脑可以用作演示工具,在大多数情况下,其[...] Markdown 支持为提供足够的功能,以取代传统的 PowerPoint。 生成的图表可以直接用于有效地传达复杂分析的结果; 不再需要复制和粘贴,并且算法的更改会自动反映在最终演示中。 例如,某些笔记本实现(例如 Jupyter)可将单元格布局自动转换为幻灯片显示,从而使整个体验更加无缝。

注意

作为参考,以下是在 Jupyter 笔记本中制作这些幻灯片的步骤:

  • 使用视图 | 单元格工具栏 | 幻灯片,首先在幻灯片子幻灯片片段跳过注释
  • 使用nbconvert jupyter命令将笔记本转换为 Reveal.js 支持的 HTML 幻灯片:
  • (可选)您可以启动 Web 应用服务器以在线访问以下幻灯片:

jupyter nbconvert <pathtonotebook.ipynb> --to slides
 jupyter nbconvert <pathtonotebook.ipynb> --to slides –post serve

对于开发人员来说,情况就不那么清楚了。 一方面,开发人员喜欢 REPL 编程,而笔记本提供了交互式 REPL 的所有优点,并且可以通过连接到远程后端而获得额外的好处。 通过在浏览器中运行,结果可以包含图形,并且由于可以保存图形,因此可以在不同情况下重用笔记本的全部或部分内容。 因此,对于开发人员而言,只要可以选择语言,笔记本电脑便可以提供一种很好的方法来进行测试和测试,例如微调算法或集成新的 API。 另一方面,即使开发人员最终负责将分析操作化为可满足客户需求的应用,开发人员也很少采用笔记本来进行数据科学活动,从而可以补充数据科学家的工作。

为了改善软件开发生命周期并缩短实现价值的时间,他们需要开始使用与数据科学家相同的工具,编程语言和框架,包括 Python 及其拥有丰富的库和笔记本生态系统的 Python,这些已成为非常重要的数据科学工具。 承认开发人员必须与中间的数据科学家会面,并加快了解数据科学背后的理论和概念。 根据我的经验,我强烈建议使用 MOOC大规模开放在线课程的缩写),例如 CourseraEdX,它们为每个级别提供了各种各样的课程。

但是,很广泛地使用了笔记本,很显然,它们虽然功能强大,但主要是为数据科学家设计的,这给开发人员带来了陡峭的学习曲线。 他们还缺乏对开发人员至关重要的应用开发功能。 正如我们在“Twitter Hashtags 项目的情感分析”中所看到的那样,基于笔记本中创建的分析来构建应用或仪表板可能非常困难,并且需要难以实现的架构,并且在基础架构上占用了大量资源。

为了解决这些空白,我决定创建 PixieDust 库并将其开源。 正如我们将在下一章中看到的那样,PixieDust 的主要目标是通过提供简单的 API 来加载和降低新用户(无论是数据科学家还是开发人员)的入门成本。 可视化数据。 PixieDust 还为开发人员框架提供了 API,可轻松构建可直接在笔记本中运行并也可作为 Web 应用部署的应用,工具和仪表板。

总结

在本章中,我以开发人员的身份介绍了数据科学,并讨论了为什么我认为数据科学与 AI 和 Cloud 一起具有定义下一个计算时代的潜力的原因。 我还讨论了在充分发挥其潜力之前必须解决的许多问题。 虽然这本书并未假装提供解决所有这些问题的灵丹妙药,但它确实试图回答使数据科学民主化这一棘手但至关重要的问题,更具体地说,是弥合了数据科学家与开发人员之间的鸿沟

在接下来的几章中,我们将深入研究 PixieDust 开源库,并了解它如何帮助 Jupyter 笔记本用户在处理数据时更加高效。 我们还将深入研究 PixieApp 应用开发框架,该框架使开发人员能够利用笔记本中实现的分析来构建应用和仪表板。

在其余各章中,我们将深入研究许多示例,这些示例说明数据科学家和开发人员如何有效地协作以构建端到端数据管道,迭代分析并在短时间内将其部署到最终用户。 样例应用将涵盖许多行业用例,例如图像识别,社交媒体和财务数据分析,其中包括数据科学用例,例如描述性分析,机器学习,自然语言处理和流数据。

我们不会深入讨论示例应用中涵盖的所有算法背后的理论(这超出了本书的范围,将涉及多于一本书),但我们将强调如何利用开源生态系统来快速发展。 完成手头的任务(模型构建,可视化等)并将结果可操作到应用和仪表板中。

注意

所提供的示例应用主要是用 Python 编写的,并附带完整的源代码。 该代码已经过广泛测试,可以在您自己的项目中重复使用和自定义了。*

二、使用 Jupyter 笔记本和 PixieDust 的大规模数据科学

“最好的代码行是您不必编写的代码!”

匿名

在上一章中,我根据实际经验给出了开发人员对数据科学的看法,并讨论了在企业中成功进行部署所需的三个战略支柱:数据,服务和工具。 我还讨论了这样一个想法,即数据科学不仅是数据科学家的唯一权限,而且是对开发人员具有特殊作用的团队运动。

在本章中,我将介绍一个基于 Jupyter 笔记本,Python 和 PixieDust 开源库的解决方案,该解决方案着重于三个简单的目标:

  • 通过降低非数据科学家的进入门槛使数据科学民主化
  • 开发人员与数据科学家之间的协作不断增强
  • 简化数据科学分析的操作

注意

此解决方案仅关注工具支柱,而不关注数据和服务,尽管应该在第 6 章,“使用 TensorFlow 的图像识别”开始讨论示例应用时发现它们,但应该独立实现。

为什么选择 Python?

像许多开发人员一样,当用于构建数据密集型项目时,使用 Python 并不是我的首选。 老实说,在 Java 领域已经工作了很多年,尽管学习曲线相当陡峭,但 Scala 最初对我来说更具吸引力。 Scala 是一种非常强大的语言,完美地结合了 Java 所欠缺的面向对象和函数式编程(直到 Java 8 开始引入 Lambda 表达式为止)。

Scala 还提供了一种非常简洁的语法,可以将其转换为更少的代码行,更高的生产率并最终减少错误。 这非常方便,尤其是当您大部分工作是操纵数据时。 喜欢 Scala 的另一个原因是,在使用大数据框架(例如 Apache Spark)时,它们具有更好的 API 覆盖范围,这些框架本身是用 Scala 编写的。 还有很多其他理由更喜欢 Scala,例如它是一个强大的类型化系统,并且它与 Java 互操作性,在线文档和高性能。

因此,对于像我这样开始涉足数据科学的开发人员来说,Scala 似乎是一个更自然的选择,但是,扰流警报却使我们最终专注于 Python。 这种选择有多种原因:

  • Python 作为一种语言,本身也有很多发展。 它是一种动态规划语言,具有与 Scala 相似的优点,例如函数式编程,简洁的语法等。

  • 在过去的几年中,Python 见证了数据科学家的飞速发展,已取代长期的竞争对手 R 作为数据科学的整体首选语言,这可以通过在 Google 趋势中快速搜索术语“Python 数据科学”,“Python 机器学习”,“R 数据科学”和“R 机器学习”来证明:

    Why choose Python?

    2017 年的兴趣趋势

在一个良性循环中,Python 的受欢迎程度不断提高,带动了一个广泛且不断发展的范围广泛的库,可以使用 PIP Python 包安装程序轻松将其导入到您的项目中。 数据科学家现在可以访问许多功能强大的开源 Python 库,以进行数据处理,数据可视化,统计,数学,机器学习和自然语言处理。

即使初学者也可以使用流行的 Scikit-learn 包快速构建机器学习分类器,而无需成为机器学习专家,也可以使用 MatplotlibBokeh 快速绘制丰富的图表。

此外,Python 也已成为 IEEE Spectrum 2017 调查

Why choose Python?

编程语言的使用情况统计

GitHub 上也证实了这种趋势,其中 Python 现在在存储库总数中排名第三,仅次于 Java 和 JavaScript:

Why choose Python?

通过编程语言的 GitHub 存储库统计信息

上图显示了一些有趣的统计数据,展示了 Python 开发人员社区的活跃程度。 在 GitHub 上活动的与 Python 相关的存储库的大小为第三大,每个存储库的总代码推送和未解决的问题也相类似。

Python 在网络上也变得无处不在,它通过 Web 开发框架为众多知名网站提供支持,例如 DjangoTornadoTurboGears。 最近有迹象表明,Python 也在所有主要的云提供商中进入了云服务领域,其中包括中提供了某些功能。

Python 显然在数据科学领域有着光明的前途,尤其是与诸如 Jupyter 笔记本之类的强大工具结合使用时,Python 在数据科学家社区中已变得非常流行。 笔记本电脑的价值主张是,它们非常易于创建,并且非常适合快速运行的实验。 此外,笔记本电脑还支持多种高保真序列化格式,可以捕获指令,代码和结果,然后可以很容易地与团队中的其他数据科学家共享这些代码,也可以将其作为开源供所有人使用。 例如,我们看到 Jupyter 笔记本在 GitHub 上激增,数量超过 250 万并且还在增加。

以下屏幕快照显示了 GitHub 搜索任何扩展名为.ipynb,的文件的结果,该文件是序列化 Jupyter 笔记本(JSON 格式)的最流行格式:

Why choose Python?

GitHub 上的 Jupyter 笔记本的搜索结果

这很棒,但是 Jupyter 笔记本经常被认为仅仅是数据科学家工具。 我们将在接下来的章节中看到它们可以提供更多的功能,它们还可以帮助所有类型的团队解决数据问题。 例如,它们可以帮助业务分析师快速加载和可视化数据集,使开发人员可以直接在笔记本电脑中与数据科学家合作,以利用他们的分析和构建功能强大的仪表板,或者允许 DevOps 轻松地将这些仪表板部署到可扩展,在企业中就绪的微服务,可以作为独立的 Web 应用或可嵌入的组件运行。 基于将数据科学工具带给非数据科学家的愿景,创建了 PixieDust 开源项目。

介绍 PixieDust

提示

有趣的事实

我经常被问到我是如何想到 PixieDust 这个名字的,我只是想让笔记本对于非数据科学家来说就像魔术一样简单。

PixieDust 是一个开源项目,主要由三个组件组成,旨在解决本章开头所述的三个目标。

  • 一个适用于 Jupyter 笔记本的辅助 Python 库,它提供了简单的 API,可将各种来源的数据加载到流行的框架(如 Pandas 和 Apache Spark DataFrame)中,然后以交互方式可视化和浏览数据集。
  • 一个基于 Python 的简单编程模型,通过创建功能强大的仪表板 PixieApps,开发人员可以将分析结果直接“产品化”到笔记本中。 正如我们将在下一章中看到的那样,PixieApps 与传统的 BI商业智能的缩写)仪表板有所不同,因为开发人员可以直接使用 HTML 和 CSS 来创建任意复杂的布局。 此外,他们可以将对笔记本中创建的任何变量,类或函数的访问权限嵌入其业务逻辑中。
  • 一个安全的微服务 Web 服务器,称为 PixieGateway,可以将 PixieApps 作为独立的 Web 应用运行,也可以作为可以嵌入到任何网站中的组件运行。 使用图形向导可以从 Jupyter 笔记本轻松部署 PixieApps,而无需更改任何代码。 此外,PixieGateway 支持将由 PixieDust 创建的任何图表共享为可嵌入的网页,从而使数据科学家可以轻松地在笔记本电脑外部传达结果。

请务必注意,PixieDust display() API 主要支持两种流行的数据处理框架:

  • pandas:迄今为止,最流行的 Python 数据分析包,Pandas 提供了两种主要数据结构:用于处理两个- 一维列状数据集和序列。

    注意

    当前,PixieDust display()仅支持 Pandas DataFrame

  • Apache Spark DataFrame:这是高级数据结构,用于操纵整个 Spark 集群中的分布式数据集。 SparkDataFrame构建在较低级 RDD弹性分布式数据集的简称)之上,并添加了支持 SQL 查询的功能。

PixieDust display()支持的另一种较不常用的格式是 JSON 对象数组。 在这种情况下,PixieDust 将使用这些值来构建行,并将键用作列,例如,如下所示:

my_data = [
{"name": "Joe", "age": 24},
{"name": "Harry", "age": 35},
{"name": "Liz", "age": 18},
...
]

此外,PixieDust 在数据处理和渲染级别都具有高度可扩展性。 例如,您可以添加要由可视化框架渲染的新数据类型,或者如果您想利用自己喜欢的绘图库,则可以轻松地将其添加到 PixieDust 支持的渲染器列表中(更多信息请参见下一章)。

您还将发现 PixieDust 包含一些与 Apache Spark 相关的额外工具,例如:

  • 包管理器:这使您可以在 Python 笔记本中安装 Spark 包。
  • Scala 桥接:您可以使用%%scala魔术在 Python 笔记本中直接使用 Scala 。 变量自动从 Python 传输到 Scala,反之亦然。
  • Spark 作业进度监视器:通过直接在单元格输出中显示进度条来跟踪任何 Spark 作业的状态。

在深入研究三个 PixieDust 组件中的每一个之前,最好通过在云上注册托管解决方案(例如,位于这个页面)来访问 Jupyter 笔记本或在本地计算机上安装开发版本。

注意

您可以按照以下说明在本地安装笔记本服务器

要在本地启动笔记本服务器,只需从终端运行以下命令:

jupyter notebook --notebook-dir=<<directory path where notebooks are stored>>

笔记本主页将在浏览器中自动打开。 有许多配置选项可控制启动笔记本服务器的方式。 这些选项可以添加到命令行或保留在笔记本配置文件中。 如果您想尝试所有可能的配置选项,可以使用--generate-config选项生成配置文件,如下所示:

jupyter notebook --generate-config

这将生成以下 Python 文件<home_directory>/.jupyter/jupyter_notebook_config.py,其中包含一组已禁用的自动记录的选项。 例如,如果不想在 Jupyter 笔记本启动时自动打开浏览器,请找到包含sc.NotebookApp.open_browser变量的行,取消注释,然后将其设置为False

## Whether to open in a browser after starting. The specific browser used is
#  platform dependent and determined by the python standard library 'web browser'
#  module, unless it is overridden using the --browser (NotebookApp.browser)
#  configuration option.
c.NotebookApp.open_browser = False

进行更改后,只需保存jupyter_notebook_config.py文件并重新启动笔记本服务器。

下一步是使用pip工具安装 PixieDust 库:

  1. 从笔记本计算机本身,在单元格中输入以下命令:

    !pip install pixiedust
    
    

    注意

    注意:感叹号语法特定于 Jupyter 笔记本,它表示其余命令将作为系统命令执行。 例如,您可以使用!ls列出当前工作目录下的所有文件和目录。

  2. 使用单元格 | 运行单元格菜单或工具栏上的运行图标。 您还可以使用以下键盘快捷键来运行单元格:

    • Ctrl + Enter:运行并保持当前单元格处于选中状态
    • Shift + Enter:运行并选择下一个单元格
    • Alt + Enter:运行并创建新的空白

    下方的单元格

  3. 重新启动内核以确保pixiedust库已正确加载到内核中。

以下屏幕截图显示了首次安装pixiedust后的结果:

Introducing PixieDust

在 Jupyter 笔记本上安装 PixieDust 库

提示

我强烈建议您使用 Anaconda,它提供了出色的 Python 包管理功能。 如果像我一样喜欢试验不同版本的 Python 和库依赖关系,建议您使用 Anaconda 虚拟环境。

它们是轻量级的 Python 沙箱,非常易于创建和激活(请参见这个页面):

  • 创建一个新环境:conda create --name env_name
  • 列出所有环境:conda env list
  • 激活环境:source activate env_name

我还建议您有选择地熟悉源代码,该源代码位于这个页面这个页面

现在,我们准备在下一部分中以sampleData()开头探索 PixieDust API。

SampleData——用于加载数据的简单 API

将数据加载到笔记本中是数据科学家可以执行的最多重复任务之一,但是根据所使用的框架或数据源,编写代码可能既困难又耗时。

让我们举一个具体的示例,尝试从一个开放的数据站点(例如这个页面)中将 CSV 文件加载到 Pandas 和 Apache SparkDataFrame中。

注意

注意:继续,假定所有代码都在 Jupyter 笔记本中运行。

对于 Pandas 来说,代码非常简单,因为它提供了直接从 URL 加载的 API:

import pandas
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_df = pandas.read_csv(data_url)
building_df

最后一条调用building_df,的语句将在输出单元格中打印其内容。 如果没有打印,这是可能的,因为 Jupyter 会将调用变量的单元格的最后一条语句解释为打印该指令的指令:

SampleData – a simple API for loading data

Pandas DataFrame的默认输出

但是,对于 Apache Spark,我们需要先将数据下载到文件中,然后使用 Spark CSV 连接器将其加载到DataFrame中:

#Spark CSV Loading
from pyspark.sql import SparkSession
try:
    from urllib import urlretrieve
except ImportError:
    #urlretrieve package has been refactored in Python 3
    from urllib.request import urlretrieve

data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
urlretrieve (data_url, "building.csv")

spark = SparkSession.builder.getOrCreate()
building_df = spark.read\
  .format('org.apache.spark.sql.execution.datasources.csv.CSVFileFormat')\
  .option('header', True)\
  .load("building.csv")
building_df

由于building_df现在是 Spark DataFrame,因此输出略有不同:

SampleData – a simple API for loading data

SparkDataFrame的默认输出

即使此代码不是那么大,也必须每次都重复,并且很可能需要花费时间进行 Google 搜索以记住正确的语法。 数据也可以采用其他格式,例如 JSON,这将要求为 Pandas 和 Spark 调用不同的 API。 数据的格式也可能不正确,并且可能在 CSV 文件中包含错误的行或 JSON 语法错误。 不幸的是,所有这些问题并非罕见,并助长了数据科学的 80/20 规则,该规则指出,数据科学家平均花费 80% 的时间来获取,清理和加载数据,而只有 20% 的时间用于实际分析。

PixieDust 提供了一个简单的sampleData API,以帮助改善这种情况。 当不带参数调用时,它将显示准备分析的预整理数据集的列表:

import pixiedust
pixiedust.sampleData()

结果如下所示:

SampleData – a simple API for loading data

PixieDust 内置数据集

可以根据组织的需要定制预定义的策划数据集列表,这是朝着数据支柱迈出的重要一步,如上一章所述。

然后,用户可以使用预构建数据集的 ID 再次再次调用sampleData API,并在 Jupyter Kernel 中的 Spark 框架可用的情况下获取 Spark DataFrame;如果没有,则退回到 Pandas DataFrame

在以下示例中,我们在连接了 Spark 的笔记本电脑上调用sampleData()。 我们还调用enableSparkJobProgressMonitor()以显示有关操作中涉及的 Spark 作业的实时信息。

注意

注意:Spark 作业是在 Spark 集群中具有特定数据子集的特定节点上运行的进程。 在从数据源加载大量数据的情况下,将为每个 Spark 作业分配一个特定的子集(实际大小取决于集群中的节点数和整体数据的大小)。 与其他工作。

在一个单独的单元中,我们运行以下代码以启用 Spark Job Progress Monitor:

pixiedust.enableSparkJobProgressMonitor()

结果如下:

Successfully enabled Spark Job Progress Monitor

然后,我们调用sampleData来加载cars数据集:

cars = pixiedust.sampleData(1)

结果如下所示:

SampleData – a simple API for loading data

使用 PixieDust sampleData API 加载内置数据集

用户还可以传递指向可下载文件的任意 URL。 PixieDust 当前支持 JSON 和 CSV 文件。 在这种情况下,PixieDust 将自动下载文件,将缓存在临时区域中,检测格式,然后根据 Spark 是否在笔记本电脑中可用加载到 Spark 或 Pandas DataFrame中。 请注意,即使使用forcePandas 关键字参数可以使用 Spark,用户也可以强制加载到 Pandas 中:

import pixiedust
data_url = "https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD"
building_dataframe = pixiedust.sampleData(data_url, forcePandas=True)

结果如下:

Downloading 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD' from https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD
Downloaded 13672351 bytes
Creating pandas DataFrame for 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD'. Please wait...
Loading file using 'pandas'
Successfully created pandas DataFrame for 'https://data.cityofnewyork.us/api/views/e98g-f8hy/rows.csv?accessType=DOWNLOAD'

sampleData() API 也足够聪明,可以识别指向 ZIP 和 GZ 类型的压缩文件的 URL。 在这种情况下,它将自动解压缩原始二进制数据并加载存档中包含的文件。 对于 ZIP 文件,它查看存档中的第一个文件;对于 GZ 文件,它只是解压缩内容,因为 GZ 文件不是存档,并且不包含多个文件。 然后sampleData() API 将从解压缩的文件中加载DataFrame

例如,我们可以直接从伦敦开放数据网站提供的 ZIP 文件中加载自治市镇信息,并使用display() API 将结果显示为饼图,如下所示:

import pixiedust
london_info = pixiedust.sampleData("https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip")

结果如下(假设您的笔记本计算机已连接到 Spark,否则将加载 Pandas DataFrame):

Downloading 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip' from https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip
Extracting first item in zip file...
File extracted: london-borough-profiles.csv
Downloaded 948147 bytes
Creating pySpark DataFrame for 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip'. Please wait...
Loading file using 'com.databricks.spark.csv'
Successfully created pySpark DataFrame for 'https://files.datapress.com/london/dataset/london-borough-profiles/2015-09-24T15:50:01/London-borough-profiles.zip'

然后,我们可以在london_info``DataFrame上调用display(),如下所示:

display(london_info)

我们在图表菜单中选择饼图,然后在选项对话框中,将Area name列拖放到区域和Crime rates per thousand population 2014/15区域中,如以下屏幕截图所示:

SampleData – a simple API for loading data

用于可视化london_info数据帧的图表选项

选项对话框中单击 OK 按钮后,我们得到以下结果:

SampleData – a simple API for loading data

通过指向压缩文件的 URL 创建的饼图

很多时候,您找到了一个不错的数据集,但文件包含错误或对您而言很重要的数据为错误格式或埋在一些非结构化文本中,需要将其提取到自己的列中。 此过程也称为数据整理,可能非常耗时。 在下一节中,我们将研究 PixieDust 的扩展名pixiedust_rosie,该扩展提供了wrangle_data方法,该方法可以帮助完成此过程。

使用pixiedust_rosie整理数据

在大多数情况下,在受控实验中工作与在现实世界中工作不同。 我的意思是,在开发过程中,我们通常会选择(或者我应该说是制造)一个旨在表现出来的样本数据集。 它具有正确的格式,符合架构规范,没有数据丢失,依此类推。 目标是专注于验证假设并构建算法,而不是集中在数据清理上,这可能非常痛苦且耗时。 但是,在开发过程中尽早获得尽可能接近真实数据的数据具有不可否认的优势。 为了帮助完成此任务,我与 IBM 的两个同事 Jamie Jennings 和 Terry Antony 合作,他们自愿为 PixieDust 扩展了名为pixiedust_rosie的扩展。

该 Python 包实现了一个简单的wrangle_data()方法,以自动执行原始数据的清理。 pixiedust_rosie包当前支持 CSV 和 JSON,但是将来会添加更多格式。 底层数据处理引擎使用 Rosie 模式语言RPL)开源组件,这是一个正则表达式引擎,旨在使开发人员更容易使用,性能更高, 并可以扩展到大数据。 您可以在此处找到有关 Rosie 的更多信息

首先,您需要使用以下命令安装pixiedust_rosie包:

!pip install pixiedust_rosie

pixiedust_rosie包依赖于pixiedustrosie,,如果尚未安装在系统上,它们将自动下载。

wrangle_data()方法与sampleData() API 非常相似。 当不带参数调用时,它将显示预整理的数据集列表,如下所示:

import pixiedust_rosie
pixiedust_rosie.wrangle_data()

这将产生以下结果:

Wrangling data with pixiedust_rosie

可用于wrangle_data()的预整理数据集的列表

您还可以使用预整理数据集的 ID 或 URL 链接来调用它,例如,如下所示:

url = "https://github.com/ibm-watson-data-lab/pixiedust_rosie/raw/master/sample-data/Healthcare_Cost_and_Utilization_Project__HCUP__-_National_Inpatient_Sample.csv"
pixiedust_rosie.wrangle_data(url)

在前面的代码中,我们在url变量引用的 CSV 文件上调用wrangle_data()。 该函数首先在本地文件系统中下载文件,然后对数据的子集执行自动数据分类,以推断数据模式。 然后启动模式编辑器 PixieApp,它提供了一组向导屏幕,可让用户配置模式。 例如,用户将能够删除和重命名列,更重要的是,通过提供 Rosie 模式,可以将现有列分解为新列。

下图说明了工作流程:

Wrangling data with pixiedust_rosie

wrangle_data()工作流程

wrangle_data()向导的第一个屏幕显示了由 Rosie 数据分类器推断的架构,如以下屏幕截图所示:

Wrangling data with pixiedust_rosie

wrangle_data()模式编辑器

前面的架构窗口小部件显示了列名称Rosie Type(特定于 Rosie 的高级类型表示)和Column Type(映射至受支持的 Pandas 类型)。 每行还包含三个操作按钮:

  • 删除列:这将从架构中删除列。 此列将不会出现在最终的 Pandas DataFrame中。
  • 重命名列:这将更改列的名称。
  • 转换列:这通过将列分解为新列来对其进行转换。

用户可以随时预览数据(如前面的 SampleData 小部件中所示)以验证架构配置是否按预期进行。

当用户单击转换列按钮时,将显示一个新屏幕,让用户指定用于构建新列的模式。 在某些情况下,数据分类器将能够自动检测模式,在这种情况下,将添加一个按钮询问用户是否应应用建议。

以下屏幕截图显示了转换所选列屏幕,并带有自动建议:

Wrangling data with pixiedust_rosie

转换列屏幕

此屏幕显示四个小部件,其中包含以下信息:

  • Rosie 模式输入是您可以输入代表该列数据的自定义 Rosie 模式的地方。 然后,您使用提取变量按钮告诉模式编辑器应将模式的哪一部分提取到新列中(稍后将对此进行详细说明)。
  • 有一个帮助窗口小部件,提供指向 RPL 文档的链接。
  • 当前列的数据预览。
  • 应用了 Rosie 模式的数据预览。

当用户单击提取变量按钮时,小部件将更新为以下内容:

Wrangling data with pixiedust_rosie

将 Rosie 变量提取到列中

此时,用户可以选择编辑定义,然后单击创建列按钮以将新列添加到架构。 然后更新新列样本小部件以显示数据外观的预览。 如果模式定义包含错误的语法,则此小部件中将显示错误:

Wrangling data with pixiedust_rosie

应用模式定义后预览新列

当用户单击提交列按钮时,将再次显示主模式编辑器屏幕,并添加新列,如以下屏幕快照所示:

Wrangling data with pixiedust_rosie

具有新列的模式编辑器

最后一步是单击完成按钮以将架构定义应用于原始文件,并创建一个 Pandas DataFrame,它将在笔记本中用作变量。 此时,系统会向用户显示一个对话框,其中包含一个可以编辑的默认变量名,如以下屏幕截图所示:

Wrangling data with pixiedust_rosie

编辑结果 Pandas 数据帧的变量名

单击完成按钮后,pixiedust_rosie应用模式定义遍历整个数据集。 完成后,它将使用生成的代码在当前单元的下面创建一个新单元格,该代码在新生成的 Pandas DataFrame上调用display() API,如下所示:

#Code generated by pixiedust_rosie
display(wrangled_df)

运行前面的单元格将使您能够浏览和可视化新数据集。

我们在本节中探讨的wrangle_data()函数是帮助数据科学家花更少的时间清理数据并花更多时间分析数据的第一步。 在下一部分中,我们将讨论如何帮助数据科学家进行数据探索和可视化。

可视化——用于数据可视化的简单交互式 API

数据可视化是另一个非常重要的数据科学任务,对于探索和形成假设来说是必不可少的。 幸运的是,Python 生态系统具有许多强大的库,专门用于数据可视化,例如以下热门示例:

但是,类似于数据加载和清理,在笔记本中使用这些库可能既困难又耗时。 这些库中的每一个都有各自的编程模型,并且 API 并不总是易于学习和使用的,特别是如果您不是经验丰富的开发人员。 另一个问题是这些库没有与常用数据处理框架(例如 Pandas(可能是 Matplotlib 除外)或 Apache Spark)的高层接口,因此,在绘制数据之前需要进行大量数据准备。

为解决此问题,PixieDust 提供了一个简单的display() API,该 API 使 Jupyter 笔记本用户可以使用交互式图形界面来绘制数据而无需任何编码。 这个 API 实际上不会创建图表,但是会在根据用户选择通过调用其 API 委托给渲染器之前完成准备数据的所有繁重工作。

display() API 支持多种数据结构(Pandas,Spark 和 JSON)以及多种渲染器(Matplotlib,Seaborn,Bokeh 和 Brunel)。

作为说明,让我们使用内置的汽车性能数据集,并通过调用display() API 开始可视化数据:

import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True) #car performance data
display(cars)

第一次在单元格上调用该命令时,将显示表格视图,并且当用户浏览菜单时,所选选项将以 JSON 格式存储在单元格元数据中,以便下次单元格运行时可以再次使用它们。 所有可视化的输出布局遵循相同的模式:

  • 有一个可扩展的顶层菜单,可在图表之间进行切换。

  • 有一个下载菜单,用于在本地计算机上下载文件。

  • 有一个过滤器切换按钮,使用户可以通过过滤数据来优化他们的探索。 我们将在“过滤”部分中讨论过滤器功能。

  • 有一个Expand/Collapse Pixiedust Output按钮,用于折叠/扩展输出内容。

  • 有一个选项按钮可调用一个对话框,该对话框具有特定于当前可视化的配置。

  • 有一个“共享”按钮,可让您在网络上发布可视化内容。

    注意

    注意:仅当您已部署 PixieGateway 时,才可以使用此按钮,我们将在第四章,“将 PixieApps 部署到使用 Web 的 PixieGateway 服务器”中进行详细讨论。

  • 在可视化的右侧有一组上下文选项。

  • 存在主要的可视化区域。

Display – a simple interactive API for data visualization

表格渲染器的可视化输出布局

要开始创建图表,首先在菜单中选择适当的类型。 开箱即用,PixieDust 支持六种类型的图表:条形图线形图散点图饼图映射直方图。 正如我们将在第 5 章,“最佳做法和高级 PixieDust 概念”中看到的那样,PixieDust 还提供了 API,可让您通过添加新菜单或向现有菜单添加选项来自定义这些菜单:

Display – a simple interactive API for data visualization

PixieDust 图表菜单

首次调用图表菜单时,将显示一个选项对话框,以配置一组基本配置选项,例如XY轴所使用的类型,聚合等等。 为了节省您的时间,该对话框将预先填充 PixieDust 从DataFrame自动检查的数据模式。

在以下示例中,我们将创建一个条形图,显示按马力计算的平均里程消耗:

Display – a simple interactive API for data visualization

条形图对话框选项

单击 OK 将在单元格输出区域中显示交互式界面:

Display – a simple interactive API for data visualization

条形图可视化

画布在中心区域显示图表,并在与所选图表类型相关的一侧显示一些上下文选项。 例如,我们可以在群集组合框中选择字段来源,以按来源国显示明细:

Display – a simple interactive API for data visualization

集群条形图可视化

如前所述,PixieDust display()实际上并不创建图表,而是根据所选选项准备数据,并使用正确的参数来繁重地调用渲染器引擎的 API。 该设计背后的目标是使每种图表类型都支持多个渲染器,而无需任何额外的编码,从而为用户提供了尽可能多的探索自由。

只要安装了相应的库,PixieDust 即可使用以下渲染器。 对于未安装的渲染器,将在 PixieDust 日志中生成警告,并且相应的渲染器不会显示在菜单中。 我们将在第 5 章,“最佳实践和高级 PixieDust 概念”中详细介绍 PixieDust 登录。

  • Matplotlib

  • Seaborn

    注意

    需要使用以下库来安装该库: !pip install seaborn.

  • Bokeh

    注意

    需要使用以下库来安装该库: !pip install bokeh.

  • Brunel

    注意

    需要使用以下库来安装该库: !pip install brunel.

  • Google 地图

  • Mapbox

    注意

    注意:Google Map 和 Mapbox 需要您可以在各自站点上获得的 API 密钥。

您可以使用渲染器组合框在渲染器之间切换。 例如,如果我们想要更多的交互性来探索图表(例如缩放和平移),则可以使用 Bokeh 渲染器而不是 Matplotlib,它只能为我们提供静态图像:

Display – a simple interactive API for data visualization

使用 Bokeh 渲染器的群集条形图

另一个值得一提的图表类型是地图,当您的数据包含地理空间信息(例如经度,纬度或国家/州信息)时,这会很有意思。 PixieDust 支持多种类型的地理映射渲染引擎,包括流行的 Mapbox 引擎。

注意

在使用 Mapbox 渲染器之前,建议从以下位置的 Mapbox 站点获取 API 密钥。 但是,如果您没有,则 PixieDust 将提供一个默认密钥。

要创建地图,请使用 NE Mass 数据集中的百万美元房屋销售,如下所示:

import pixiedust
homes = pixiedust.sampleData(6, forcePandas=True) #Million dollar home sales in NE Mass
display(homes)

首先,在图表下拉按钮中选择地图,然后在选项对话框中,选择LONGITUDELATITUDE作为键,然后在提供的输入中输入 Mapbox 访问令牌。 您可以在区域中添加多个字段,它们将作为工具提示显示在地图上:

Display – a simple interactive API for data visualization

Mapbox 图表的“选项”对话框

单击 OK 按钮时,您将获得交互式地图,您可以使用样式(简单,正弦或密度图),颜色和底图(亮,卫星,暗, 和户外)选项:

Display – a simple interactive API for data visualization

交互式 Mapbox 可视化

每种图表类型都有其自己的上下文选项集,这些选项不言自明,在这一点上,我鼓励您使用它们中的每一个。 如果您发现问题或有增强想法,则可以始终在 GitHub 上的这个页面创建一个新问题,或者提交一个带有您的代码更改的请求更好(这里有更多有关该操作的信息)。

为避免每次单元格运行时都重新配置图表,PixieDust 将图表选项作为 JSON 对象存储在单元格元数据中,最终将其保存在笔记本中。 您可以通过选择视图 | 单元格工具栏 | 编辑元数据菜单手动检查此数据,如以下屏幕截图所示:

Display – a simple interactive API for data visualization

显示编辑元数据按钮

编辑元数据按钮将显示在单元格的顶部,单击该按钮将显示 PixieDust 配置:

Display – a simple interactive API for data visualization

编辑单元元数据对话框

当我们在下一节讨论 PixieApps 时,此 JSON 配置将非常重要。

过滤

为了更好地浏览数据,PixieDust 还提供了一个内置的简单图形界面,可让您快速过滤正在可视化的数据。 您可以通过单击顶层菜单中的过滤器切换按钮来快速调用过滤器。 为简单起见,过滤器仅支持仅基于一列构建谓词,这在大多数情况下足以验证简单假设(基于反馈,将来可能会增强此功能以支持多个谓词)。 过滤器用户界面会自动让您选择要过滤的列,并根据其类型显示不同的选项:

  • 数值类型:用户可以选择一个数学比较器并为操作数输入一个值。 为了方便起见,UI 还将显示与所选列相关的统计值,这些统计值可在选择操作数值时使用:

    Filtering

    过滤汽车数据集的mpg数字列

  • 字符串类型:用户可以输入表达式以匹配列值,该表达式可以是正则表达式或纯字符串。 为了方便起见,UI 还显示了有关如何构建正则表达式的基本帮助:

Filtering

过滤汽车数据集的名称字符串类型

单击应用按钮时,当前可视化将更新以反映过滤器配置。 重要的是要注意,该过滤器不仅适用于当前单元格,还适用于整个单元格。 因此,当在图表类型之间切换时,它将继续适用。 筛选器配置也保存在单元元数据中,因此在保存笔记本并重新运行单元时将保留它。

例如,以下屏幕快照将cars数据集可视化为条形图,仅显示mpg大于23,的行,根据统计框,这是数据集的平均值,并按年份进行聚类。 在选项对话框中,我们选择mpg列作为键,并选择origin作为值:

Filtering

汽车数据集的已过滤条形图

总而言之,在本节中,我们讨论了 PixieDust 如何帮助完成三个困难且耗时的数据科学任务:数据加载,数据整理和数据可视化。 接下来,我们将了解 PixieDust 如何帮助增加数据科学家与开发人员之间的协作。

通过 PixieApps 弥合开发人员与数据科学家之间的鸿沟

解决硬数据问题只是数据科学团队的任务之一。 他们还需要确保正确执行数据科学结果,以为组织提供业务价值。 数据分析的运营在很大程度上取决于用例。 例如,这可能意味着创建仪表板,为决策者综合见解,或者将诸如推荐引擎之类的机器学习模型集成到 Web 应用中。

在大多数情况下,这是数据科学与软件工程相遇的地方(或者就像人们所说的,橡胶与道路相遇)。 团队之间的持续协作(而不是一次交接)是成功完成任务的关键。 通常,他们还必须应对不同的语言和平台,从而导致软件工程团队重写大量代码。

当我们需要构建实时仪表板以可视化结果时,我们在 Twitter 主题标签项目的情感分析中亲身体验了它。 数据分析是使用 Pandas,Apache Spark 和一些绘图库(例如 Matplotlib 和 Bokeh)以 Python 编写的,而仪表板是用 Node.jsD3 编写的。

我们还需要在分析和仪表板之间建立数据接口,并且由于我们需要系统是实时的,因此我们选择使用 Apache Kafka 来流化分析结果格式的事件。

下图概括了一种方法,我将其称为切换模式,其中数据科学团队构建分析并将结果部署在数据接口层中。 然后,结果将由应用使用。 数据层通常由数据工程师处理,这是我们在第 1 章“开发人员对数据科学的观点”中讨论的角色之一:

Bridging the gap between developers and data scientists with PixieApps

数据科学与工程学之间的交接

这种移交模式的问题在于它不利于快速迭代。 数据层中的任何更改都需要与软件工程团队同步,以避免破坏应用。 PixieApps 背后的想法是在构建应用的同时尽可能靠近数据科学环境,在我们的示例中为 Jupyter 笔记本。 使用这种方法,可以直接从运行在 Jupyter 笔记本中的 PixieApp 调用分析功能,从而使数据科学家和开发人员可以轻松地进行协作并反复进行快速改进。

PixieApp 定义了一个简单的编程模型,用于构建单页应用,可直接访问 IPython 笔记本内核(这是运行笔记本代码的 Python 后端进程)。 本质上,PixieApp 是一个 Python 类,它封装了表示形式和业务逻辑。 该演示文稿由一组称为路由的特殊方法组成,这些方法返回任意 HTML 片段。 每个 PixieApp 都有一个默认路由,该路由返回起始页面的 HTML 片段。 开发人员可以使用自定义 HTML 属性来调用其他路由,并动态更新页面的全部或部分。 例如,一条路由可能会调用从笔记本内部创建的机器学习算法或使用 PixieDust 显示框架生成图表。

下图显示了 PixieApps 与 Jupyter 笔记本客户端前端和 IPython 内核交互的高层架构:

Bridging the gap between developers and data scientists with PixieApps

PixieApp 与 Jupyter 内核的交互

作为 PixieApp 外观的预览,这是一个hello world示例应用,其中有一个按钮显示了我们在上一节中创建的汽车DataFrame的条形图:

#import the pixieapp decorators
from pixiedust.display.app import *

#Load the cars dataframe into the Notebook
cars = pixiedust.sampleData(1)

@PixieApp   #decorator for making the class a PixieApp
class HelloWorldApp():
    #decorator for making a method a
    #route (no arguments means default route)
    @route()
    def main_screen(self):
        return """
        <button type="submit" pd_options="show_chart=true" pd_target="chart">Show Chart</button>
        <!--Placeholder div to display the chart-->
        <div id="chart"></div>
        """

    @route(show_chart="true")
    def chart(self):
        #Return a div bound to the cars dataframe
        #using the pd_entity attribute
        #pd_entity can refer a class variable or
        #a global variable scoped to the notebook
        return """
        <div pd_render_onload pd_entity="cars">
            <pd_options>
                {
                  "title": "Average Mileage by Horsepower",
                  "aggregation": "AVG",
                  "clusterby": "origin",
                  "handlerId": "barChart",
                  "valueFields": "mpg",
                  "rendererId": "bokeh",
                  "keyFields": "horsepower"
                }
            </pd_options>
        </div>
        """
#Instantiate the application and run it
app = HelloWorldApp()
app.run()

当前面的代码在笔记本单元中运行时,我们得到以下结果:

Bridging the gap between developers and data scientists with PixieApps

Hello World PixieApp

您可能对前面的代码有很多问题,但请不要担心。 在下一章中,我们将介绍所有 PixieApp 技术细节,包括如何在端到端管道中使用它们。

用于实现数据科学分析的架构

在上一节中,我们了解了 PixieApps 与 PixieDust 显示框架的结合如何提供一种简便的方法来构建功能强大的仪表板,这些仪表板可直接与您的数据分析连接,从而允许算法和用户界面之间的快速迭代。 这对于快速原型制作非常有用,但是笔记本电脑不适合在目标角色是业务用户的生产环境中使用。 一个显而易见的解决方案是使用传统的三层 Web 应用架构重写 PixieApp,如下所示:

  • 用于表示层的 React
  • Web 层的 Node.js
  • 针对 Web 分析层的数据访问库,用于机器学习评分或运行任何其他分析作业

但是,这将仅对现有流程进行少量改进,在这种情况下,将仅包括使用 PixieApp 进行迭代实现的能力。

更好的解决方案是直接将 PixieApps 部署和运行为 Web 应用,包括周围笔记本电脑中的分析功能,而我们在使用它时无需更改任何代码。

使用此模型,Jupyter 笔记本将成为简化开发生命周期的中心工具,如下图所示:

Architecture for operationalizing data science analytics

数据科学管道开发生命周期

  1. 数据科学家使用 Python 笔记本来加载,丰富和分析数据并创建分析(机器学习模型,统计信息等)
  2. 开发人员在同一个笔记本中创建一个 PixieApp 来实现这些分析
  3. 准备就绪后,开发人员将 PixieApp 发布为 Web 应用,业务部门用户可以轻松地以交互方式使用它,而无需访问笔记本电脑

PixieDust 使用 PixieGateway 组件提供了该解决方案的实现。 PixieGateway 是一个 Web 应用服务器,负责加载和运行 PixieApps。 它构建在 Jupyter 内核网关之上,该网关本身构建在 Tornado Web 框架之上,因此遵循如图所示的架构。 下图:

Architecture for operationalizing data science analytics

PixieGateway 架构图

  1. PixieApp 直接从笔记本中发布到 PixieGateway 服务器中,并生成一个 URL。 在后台,PixieGateway 分配了一个 Jupyter 内核来运行 PixieApp。 根据配置,PixieApp 可以与其他应用共享内核实例,也可以根据需要拥有专用的内核。 PixieGateway 中间件可以通过管理多个内核实例的生命周期来水平扩展,这些实例本身可以是服务器本地的,也可以是群集上的远程的。

    注意

    注意:远程内核必须是 Jupyter 内核网关。

    使用发布向导,用户可以选择定义应用的安全性。 提供多个选项,包括基本认证,OAuth 2.0 和承载令牌。

  2. 业务用户使用步骤 1 中的 URL 从浏览器访问应用。

  3. PixieGateway 提供了一个全面的管理控制台,用于管理服务器,包括配置应用,配置和监视内核,访问日志以进行故障排除等。

  4. PixieGateway 使用 IPython 消息传递协议通过 WebSocket 或 ZeroMQ,具体取决于内核是本地的还是远程的。

在将分析产品化时,此解决方案相对于传统的三层 Web 应用架构进行了重大改进,因为它可以将 Web 和数据层折叠为一个 Web 分析层,如下图所示:

Architecture for operationalizing data science analytics

经典三层与 PixieGateway Web 架构之间的比较

在经典的三层架构中,开发人员必须维护多个 REST 端点,以调用数据层中的分析并按摩数据以符合表示层要求以正确显示数据。 结果,必须将大量工程设计添加到这些端点,从而增加了开发和代码维护的成本。 相反,在 PixieGateway 两层架构中,开发人员不必担心创建端点,因为服务器负责使用内置的通用端点将请求分发到适当的内核。 用另一种方式解释,PixieApp Python 方法自动成为表示层的端点,而无需任何代码更改。 该模型有助于快速迭代,因为重新发布后,Python 代码中的任何更改都直接反映在应用中。

PixieApps 非常适合快速构建单页应用和仪表板。 但是,您可能还想生成更简单的一页报告并与用户共享。 为此,PixieGateway 还允许您使用共享按钮共享由display() API 生成的图表,从而产生 URL 链接到包含该图表的网页。 反过来,用户可以通过复制和粘贴为页面生成的代码将图表嵌入网站或博客文章中。

注意

注意:我们将在第 4 章“使用 PixieGateway 服务器将 PixieApps 部署到 Web 上”中,详细介绍 PixieGateway,包括如何在本地和在云上安装新实例。

为了演示此功能,我们使用之前创建的DataFrame汽车:

Architecture for operationalizing data science analytics

共享图表对话框

如果共享成功,则下一页将显示生成的 URL 和代码片段以嵌入到 Web 应用或博客文章中:

Architecture for operationalizing data science analytics

确认共享图表

单击链接将转到页面:

Architecture for operationalizing data science analytics

将图表显示为网页

总结

在本章中,我们讨论了为什么我们的数据科学工具策略以 Python 和 Jupyter 笔记本为中心的原因。 我们还介绍了 PixieDust 功能,这些功能可通过以下功能提高用户的工作效率:

  • 数据加载和清理
  • 无需任何编码的数据可视化和探索
  • 一个基于 HTML 和 CSS 的简单编程模型称为 PixieApp,用于构建与笔记本直接交互的工具和仪表板
  • 一种点击机制,可将图表和 PixieApp 直接发布到 Web

在下一章中,我们将深入研究 PixieApp 编程模型,并通过大量代码示例讨论 API 的各个方面。

三、PixieApp 深入了解

“每个视觉都是开玩笑,直到第一个人完成它;一旦意识到,它就变得司空见惯。”

Robert H Goddard

在本章中,我们将对 PixieApp 框架进行技术上的深入研究。 您既可以将以下信息用作入门教程,又可以用作 PixieApp 编程模型的参考文档。

在深入探讨 PixieApp 的基本概念(例如路由和请求)之前,我们将首先对它进行剖析。 为了帮助跟进,我们将逐步构建一个Github Tracking示例应用,该示例应用将在引入功能和最佳实践时应用这些功能和最佳实践,从构建数据分析到将其集成到 PixieApp 中。

在本章的最后,您应该能够将学习到的经验教训应用到您自己的用例中,包括编写自己的 PixieApp。

PixieApp 的剖析

注意

:PixieApp 编程模型不需要任何 JavaScript 经验,但是,希望读者熟悉以下内容:

术语 PixieApp 代表 Pixie 应用,并且旨在强调其与 PixieDust 功能(尤其是display() API)的紧密集成。 其主要目标是使开发人员易于构建可以调用 Jupyter 笔记本中实现的数据分析的用户界面。

PixieApp 遵循单页应用SPA)设计模式,它向用户显示并带有欢迎屏幕,该屏幕会动态更新以响应用户交互。 更新可以是部分刷新,例如在用户单击控件后更新图形,也可以是完全刷新,例如在多步过程中更新新屏幕。 在每种情况下,更新都是通过使用特定机制触发的路由在服务器端进行控制的,我们将在后面讨论。 触发后,路由将执行代码以处理请求,然后发出 HTML 片段,该片段将在客户端应用于正确的目标 DOM 元素

以下序列图显示了运行 PixieApp 时客户端和服务器端如何交互:

Anatomy of a PixieApp

序列图显示了 PixieApp 的信息流

启动 PixieApp 时(通过调用run方法),将调用默认路由,并返回相应的 HTML 片段。 当用户与应用交互时,将执行更多请求,从而触发关联的路由,从而相应刷新 UI。

从实现的角度来看,PixieApp 只是一个普通的 Python 类,已经用@PixieApp装饰器装饰了。 在封面下, PixieApp装饰器检测该类以添加运行应用所需的方法和字段,例如run方法。

注意

有关 Python 装饰器的更多信息,请参见

要开始启动,下面的代码显示了一个简单的Hello World PixieApp:

#import the pixieapp decorators
from pixiedust.display.app import *

@PixieApp   #decorator for making the class a PixieApp
class HelloWorldApp():
    @route()  #decorator for making a method a route (no arguments means default route)
    def main_screen(self):
        return """<div>Hello World</div>"""

#Instantiate the application and run it
app = HelloWorldApp()
app.run()

注意

您可以在此处找到代码

上面的代码显示了 PixieApp 的结构,如何定义路由以及如何实例化和运行该应用。 由于 PixieApps 是常规的 Python 类,因此它们可以从其他类(包括其他 PixieApps)继承而来,这对于大型项目来说很方便,使代码模块化和可重用。

路由

路由用于动态更新全部或部分客户端屏幕。 可以根据以下规则在任何类方法上使用@route装饰器轻松定义它们:

  • 需要一个路由方法来返回一个字符串,该字符串表示更新的 HTML 片段。

    注意

    注意:允许在片段中使用 CSS 和 JavaScript。

  • @route装饰器可以具有一个或多个关键字参数,这些参数必须为字符串类型。 可以将这些关键字参数视为请求参数,PixieApp 框架在内部使用这些参数根据以下规则将的请求分发到最匹配的路由:

    • 带有最多参数的路由始终首先​​被评估。

    • 所有参数都必须匹配才能选择路由。

    • 如果未找到路由,则默认路由被选为后备路由。

    • 可以使用通配符(即*)配置路由,在这种情况下,状态参数的任何值都将是匹配项。

      以下是一个示例:

             @route(state1="value1", state2="value2")
      
  • PixieApp 必须具有一个且只有一个默认路由,即没有参数的路由,即@route()

以不引起冲突的方式配置路由非常重要,尤其是在您的应用具有分层状态的情况下。 例如,与state1="load"关联的路由可能负责加载数据,然后与(state1="load", state2="graph")关联的第二路由可能负责绘制数据。 在这种情况下,同时指定了state1state2的请求将匹配第二条路由,因为路由评估是从最具体到最不具体的,并在第一个匹配的路由处停止。

为了明确起见,下图显示了如何将请求与路由匹配:

Routes

将请求与路由匹配

定义为路由的方法的预期约定是返回 HTML 片段,其中可以包含 Jinja2 模板构造。 Jinja2 是功能强大的 Python 模板引擎,提供了丰富的功能来动态生成文本,包括对 Python 变量,方法和控制结构的访问,例如if...elsethe for循环等。 涵盖的所有功能将超出本书的范围,但是让我们讨论一些经常使用的重要结构:

注意

注意:如果您想了解有关 Jinja2 的更多信息,可以在这里阅读完整的文档

  • 变量:您可以使用双花括号来访问范围内的变量,例如"<div>This is my variable {{my_var}}</div>"。 在渲染期间,my_var变量将替换为其实际值。 您还可以使用.(点)表示法访问复杂对象,例如"<div>This is a nested value {{my_var.sub_value}}</div>"

  • for循环:您可以使用{%for ...%}...{%endfor%}表示法通过迭代一系列项目(列表,元组,字典等)来动态生成文本,例如:

    {%for message in messages%}
    <li>{{message}}</li>
    {%endfor%}
    
  • if语句:您可以使用{%if ...%}...{%elif ...%}...{%else%}…{%endif%}表示法有条件地输出文本,例如:

    {%if status.error%}
    <div class="error">{{status.error}}</div>
    {%elif status.warning%}
    <div class="warning">{{status.warning}}</div>
    {%else%}
    <div class="ok">{{status.message}}</div>
    {%endif%}
    

了解变量和方法如何进入路由返回的 JinJa2 模板字符串的范围也很重要。 PixieApp 自动提供对三种类型的变量和方法的访问:

  • 类变量和方法:可以使用this关键字进行访问。

    注意

    注意:我们之所以不使用更具 Pythonic 风格的self关键字,是因为 Jinja2 本身已经采用了该关键字。

  • 方法参数:当路由参数使用*值并且您希望在运行时可以访问该值时,此方法很有用。 在这种情况下,您可以使用与路由参数中定义的名称相同的名称向方法本身添加参数,并且 PixieApp 框架将自动传递正确的值。

    注意

    注意:参数的顺序实际上并不重要。 您也不必使用路由中定义的每个参数,如果仅对使用参数的子集感兴趣,这将很方便。

    该变量也将在 Jinja2 模板字符串的范围内,如示例所示:

    @route(state1="*", state2="*")
    def my_method(self, state1, state2):
        return "<div>State1 is {{state1}}. State2 is {{state2}}</div>"
    

    注意

    您可以在此处找到代码文件

  • 方法的局部变量:只要您将@templateArgs装饰器添加到方法中,PixieApp 就会自动将方法中定义的所有局部变量放在 Jinja2 模板字符串的范围内,如示例所示:

    @route()
    @templateArgs
    def main_screen(self):
        var1 = self.compute_something()
        var2 = self.compute_something_else()
        return "<div>var1 is {{var1}}. var2 is {{var2}}</div>"
    

    注意

    您可以在此处找到代码

生成对路由的请求

如之前提到的,PixieApp 遵循 SPA 设计模式。 加载第一个屏幕后,与多页 Web 应用一样,使用动态请求而不是 URL 链接完成与服务器的所有后续交互。 有三种方法可以生成对路由的内核请求:

  • 使用pd_options自定义属性来定义要传递到服务器的状态列表,如以下示例所示:

    pd_options="state1=value1;state2=value2;..;staten=valuen"
    
  • 如果已经有一个包含pd_options值的 JSON 对象(例如,调用display()的情况),则必须将其转换为pd_options HTML 属性期望的格式,这可能会很耗时。 在这种情况下,将pd_options指定为子元素会更方便,这允许将选项直接作为 JSON 对象传递(并避免转换数据的额外工作),如以下示例所示:

    <div>
        <pd_options>
            {"state1":"value1","state2":"value2",...,
            "staten":"valuen"}
        </pd_options>
    </div>
    
  • 通过调用invoke_route方法以编程方式,如以下示例所示:

    self.invoke_route(self.route_method, state1='value1', state2='value2')
    

注意

注意:如果要从 Jinja2 模板字符串调用此方法,请记住使用this而不是self,因为self已经被 Jinja2 本身使用。

当需要根据用户选择动态计算中传递的状态值时,您需要使用$val(arg)特殊指令,该指令充当将在执行内核请求时解析的宏 。

$val(arg)指令采用一个参数,该参数可以是以下之一:

  • 页面上 HTML 元素的 ID,例如输入或组合框,例如以下示例:

    <div>
        <pd_options>
            {"state1":"$val(my_element_id)","state2":"value2"}
        <pd_options>
    </div>
    
  • 必须返回所需值的 JavaScript 函数,如以下示例所示:

    <script>
        function resValue(){
                return "my_query";
        }
    </script>
    ...
    <div pd_options="state1=$val(resValue)"></div>
    

注意

注意:大多数 PixieDust 自定义属性支持使用$val指令的动态值。

GitHub 项目跟踪示例应用

让我们将到目前为止所学到的应用于实现示例应用。 为了解决问题,我们想使用 GitHub Rest API 搜索项目并将结果加载到 Pandas DataFrame中进行分析。

初始代码显示了欢迎屏幕,其中带有一个简单的输入框以输入 GitHub 查询和一个按钮来提交请求:

from pixiedust.display.app import *

@PixieApp
class GitHubTracking():
    @route()
    def main_screen(self):
        return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
            <input id="query{{prefix}}" type="text" class="form-control" placeholder="Search projects on GitHub">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button">Submit Query</button>
            </span>
        </div>
    </div>
</div>
"""

app = GitHubTracking()
app.run()

注意

您可以在此处找到代码文件

前面的代码中需要注意的几件事:

  • Jupyter 笔记本提供了 Bootstrap CSS 框架jQuery JS 框架。我们可以轻松地在代码中使用,而无需安装它们。
  • FontAwesome 图标默认情况下在笔记本电脑中也可用。
  • PixieApp 代码可以在笔记本的多个单元中执行。 由于我们依赖 DOM 元素 ID,因此重要的是要确保两个元素没有相同的 ID,这会导致不良的副作用。 为此,建议始终包含由 PixieDust 框架提供的唯一标识符{{prefix}},例如"query{{prefix}}"

结果显示在以下屏幕截图中:

A GitHub project tracking sample application

GitHub Tracking 应用的欢迎屏幕

下一步是创建一个采用用户值并返回结果的新路由。 此路由将由提交查询按钮调用。

为简单起见,以下代码未使用 Python 库与 GitHub 进行接口,例如 PyGithub,相反,我们将直接调用 GitHub 网站上记录的 REST API:

注意

注意:当您看到以下表示法[[GitHubTracking]]时,这意味着该代码应添加到GitHubTracking PixieApp 类中,并且为避免一遍又一遍地重复周围的代码,它被省略了。 如有疑问,您可以始终参考本节末尾指定的完整笔记本。

import requests
import pandas
[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
    response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
    frames = [pandas.DataFrame(response.json()['items'])]
    while response.ok and "next" in response.links:
        response = requests.get(response.links['next']['url'])
        frames.append(pandas.DataFrame(response.json()['items']))

    pdf = pandas.concat(frames)
    response = requests.get( "https://api.github.com/search/repositories?q={}".format(query))
    if not response.ok:
        return "<div>An Error occurred: {{response.text}}</div>"
    return """<h1><center>{{pdf|length}} repositories were found</center></h1>"""

注意

您可以在此处找到代码文件

在前面的代码中,我们创建了一个名为do_search的路由,该路由带有一个名为query的参数,用于构建 GitHub 的 API URL。 使用requests Python 模块向此 URL 发出 GET 请求,我们获得了一个 JSON 有效负载,我们将其转换为 Pandas DataFrame。 根据 GitHub 文档,Search API 分页,并将下一页存储在链接的标题中。 该代码使用while循环遍历每个链接并将下一页加载到新的DataFrame中。 然后,我们将所有DataFrame合并为一个pdf。 我们剩下要做的就是构建将显示结果的 HTML 片段。 该片段使用 Ji​​nja2 表示法{{...}}来访问定义为局部变量的pdf变量,这仅能起作用是因为我们在do_search方法中使用了@templateArgs装饰器。 注意,我们还使用了名为length的 Jinja2 过滤器来显示找到的存储库数量:{{pdf|length}}

注意

有关过滤器的更多信息,请访问以下网站

当用户单击提交查询按钮时,我们仍然需要调用do_search路由。 为此,我们将pd_options属性添加到<button>元素,如下所示:

<div class="input-group col-sm-6">
    <input id="query{{prefix}}" type="text"
     class="form-control"
     placeholder="Search projects on GitHub">
    <span class="input-group-btn">
        <button class="btn btn-default" type="button" pd_options="query=$val(query{{prefix}})">
            Submit Query
        </button>
    </span>
</div>

我们在pd_options属性中使用$val()指令来动态检索 ID 等于"query{{prefix}}"的输入框的值,并将其存储在query参数中。

在表格中显示搜索结果

前面的代码会一次加载所有数据,因此不建议这样做,因为我们可能会有大量匹配。 同样,一次性显示所有内容会使 UI 缓慢且不实用。 值得庆幸的是,我们可以使用以下步骤轻松地构建分页表,而无需花费太多精力:

  1. 创建名为do_retrieve_page的路由,该路由以 URL 作为参数并返回表主体的 HTML 片段
  2. 将第一个,上一个,下一个和最后一个 URL 保留为 PixieApp 类中的字段
  3. 使用FirstPrevNextLast按钮创建一个分页小部件(由于可用,我们将使用 Bootstrap)
  4. 使用要显示的列标题创建表占位符

现在,我们将更新do_search,的代码,如下所示:

注意

注意:以下代码引用了do_retrieve_page方法,稍后我们将对其进行定义。 在添加do_retrieve_page方法之前,请不要尝试按原样运行此代码。

[[GitHubTracking]]
@route(query="*")
@templateArgs
def do_search(self, query):
    self.first_url = "https://api.github.com/search/repositories?q={}".format(query)
    self.prev_url = None
    self.next_url = None
    self.last_url = None

    response = requests.get(self.first_url)
    if not response.ok:
        return "<div>An Error occurred: {{response.text}}</div>"

    total_count = response.json()['total_count']
    self.next_url = response.links.get('next', {}).get('url', None)
    self.last_url = response.links.get('last', {}).get('url', None)
    return """
<h1><center>{{total_count}} repositories were found</center></h1>
<ul class="pagination">
    <li><a href="#" pd_options="page=first_url" pd_target="body{{prefix}}">First</a></li>
    <li><a href="#" pd_options="page=prev_url" pd_target="body{{prefix}}">Prev</a></li>
    <li><a href="#" pd_options="page=next_url" pd_target="body{{prefix}}">Next</a></li>
    <li><a href="#" pd_options="page=last_url" pd_target="body{{prefix}}">Last</a></li>
</ul>
<table class="table">
    <thead>
        <tr>
            <th>Repo Name</th>
            <th>Lastname</th>
            <th>URL</th>
            <th>Stars</th>
        </tr>
    </thead>
    <tbody id="body{{prefix}}">
        {{this.invoke_route(this.do_retrieve_page, page='first_url')}}
    </tbody>
</table>
"""

注意

您可以在此处找到代码文件

前面的代码示例显示了 PixieApps 的一个非常重要的属性,即您可以通过简单地将数据存储到类变量中来维护应用整个生命周期中的状态。 在这种情况下,我们使用self.first_urlself.prev_urlself.next_urlself.last_url。 这些变量对分页小部件中的每个按钮使用pd_options属性,并在每次调用do_retrieve_page路由时更新。 do_search返回的片段现在返回带有主体占位符的表,该表由body{{prefix}},标识,该表成为每个按钮的pd_target。 我们还使用invoke_route方法来确保在首次显示表格时获得第一页。

我们之前已经看到路由返回的 HTML 片段用于替换整个页面,但是在前面的代码中,我们使用pd_target="body{{prefix}}"属性表示 HTML 片段将被注入到表的 BODY 元素中,它具有body{{prefix}} ID。 如果需要,您还可以通过创建一个或多个<target>元素作为可点击源元素的子元素来为用户操作定义多个目标。 每个<target>元素本身都可以使用所有 PixieApp 自定义属性来配置内核请求。

这是一个例子:

<button type="button">Multiple Targets
    <target pd_target="elementid1" pd_options="state1=value1"></target>
    <target pd_target="elementid2" pd_options="state2=value2"></target>
</button>

回到我们的 GitHub 示例应用,do_retrieve_page方法现在看起来像这样:

[[GitHubTracking]]
@route(page="*")
@templateArgs
def do_retrieve_page(self, page):
    url = getattr(self, page)
    if url is None:
        return "<div>No more rows</div>"
    response = requests.get(url)
    self.prev_url = response.links.get('prev', {}).get('url', None)
    self.next_url = response.links.get('next', {}).get('url', None)
    items = response.json()['items']
    return """
{%for row in items%}
<tr>
    <td>{{row['name']}}</td>
    <td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
    <td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
    <td>{{row['stargazers_count']}}</td>
</tr>
{%endfor%}
        """

注意

您可以在此处找到代码文件

page参数是一个字符串,其中包含我们要显示的url类变量的名称。 我们使用标准的getattr Python 函数从页面获取url值。 然后,我们在 GitHub API url上发出 GET 请求,以 JSON 格式检索有效负载,并将其传递给 Jinja2 模板以生成将插入表中的行集。 为此,我们使用 Jinja2 中可用的{%for…%}循环控制结构生成<tr><td> HTML 标记。

以下屏幕快照显示了查询的搜索结果:pixiedust

Displaying the search results in a table

屏幕显示查询产生的 GitHub 存储库列表

注意

在第 1 部分中,我们展示了如何创建GitHubTracking PixieApp,如何调用 GitHub 查询 REST API 以及如何使用分页在表中显示结果。 您可以在此处找到带有源代码的完整笔记本:

https://github.com/DTAIEB/Thoughtful-Data-Science/blob/master/chapter%203/GitHub%20Tracking%20Application/GitHub%20Sample%20Application%20-%20Part%201.ipynb

在的下一部分中,我们将探索更多 PixieApp 功能,这些功能将允许我们深入用户到特定存储库并可视化有关存储库的各种统计信息,从而使我们能够改进应用。

第一步是在搜索结果表的每一行中添加一个按钮,以触发一条新路径以可视化所选存储库统计信息。

以下代码是do_search函数的一部分,并在表头中添加了新列:

<thead>
    <tr>
        <th>Repo Name</th>
        <th>Lastname</th>
        <th>URL</th>
        <th>Stars</th>
 <th>Actions</th>
    </tr>
</thead>

为了完成该表,我们更新了do_retrieve_page方法以添加一个包含<button>元素的新单元格,该单元格具有与analyse_repo_owneranalyse_repo_name相匹配的新路径的pd_options参数。 这些参数的值是从row元素中提取的,该元素用于对从 GitHub 请求接收的有效负载进行迭代:

{%for row in items%}
<tr>
    <td>{{row['name']}}</td>
    <td>{{row.get('owner',{}).get('login', 'N/A')}}</td>
    <td><a href="{{row['html_url']}}" target="_blank">{{row['html_url']}}</a></td>
    <td>{{row['stargazers_count']}}</td>
 <td>
 <button pd_options=
 "analyse_repo_owner={{row["owner"]["login"]}};
 analyse_repo_name={{row['name']}}"
 class="btn btn-default btn-sm" title="Analyze Repo">
 <i class="fa fa-line-chart"></i>
 </button>
 </td>
</tr>
{%endfor%}

有了此简单代码更改之后,通过再次运行单元重新启动 PixieApp,我们现在可以看到每个存储库的按钮,即使我们尚未实现相应的路由,也将在下一步实现。 提醒一下,当找不到匹配的路由时,将触发默认路由。

以下屏幕截图显示了带有添加按钮的表:

Displaying the search results in a table

为每一行添加操作按钮

下一步是创建与“回购可视化”页面关联的路由。 该页面的设计非常简单:用户可以从组合框中选择要在页面上可视化的数据类型。 GitHub REST API 提供了对多种类型数据的访问,但是对于此示例应用,我们将使用提交活动数据,该数据属于统计信息类别(请参阅这个页面,以获取此 API 的详细说明)。

提示

作为练习,请随时通过添加其他类型的 API 的可视化来改进此示例应用,例如 Traffic API

还需要注意的是,即使大多数 GitHub API 都无需认证即可工作,但如果您不提供凭据,服务器可能会限制响应。 要验证请求的身份,您需要使用 GitHub 密码或通过选择 GitHub 设置页面上的开发人员设置菜单,然后单击个人来生成个人访问令牌。 访问令牌菜单,然后单击生成新令牌按钮

在单独的笔记本单元中,我们将为 GitHub 用户 ID 和令牌创建两个变量:

github_user = "dtaieb"
github_token = "XXXXXXXXXX"

这些变量将在以后用于验证请求。 请注意,即使这些变量是在其自己的单元中创建的,它们也对整个笔记本可见,包括 PixieApp 代码。

为了提供良好的代码模块化和重用性,我们将在新类中实现 Repo Visualization 页面,并使我们的主要 PixieApp 类继承自该类并自动重用其路由。 当您开始拥有大型项目并将其分解为多个类时,请牢记这种模式。

“回购可视化”页面的主要路径返回一个 HTML 片段,该片段具有一个下拉菜单和一个用于可视化的<div>占位符。 使用 Bootstrap dropdown创建下拉菜单。 为了使代码更易于维护,菜单项是通过在元组数组上使用 Jinja2 {%for.. %}循环生成的,称为analyses的元组和序列,其中包含描述和将数据加载到 Pandas DataFrame中的函数。 再次在这里,我们在自己的单元格中创建此数组,该数组将在 PixieApp 类中引用:

analyses = [("Commit Activity", load_commit_activity)]

注意

注意load_commit_activity函数将在本节稍后讨论。

出于此示例应用的目的,该数组仅包含一个与提交活动相关的元素,但是将来您可能添加的任何元素都将由 UI 自动选择。

do_analyse_repo路由具有两个参数:analyse_repo_owneranalyse_repo_name,,足以访问 GitHub API。 我们还需要将这些参数保存为类变量,因为在生成可视化效果的路由中将需要它们:

@PixieApp
class RepoAnalysis():
    @route(analyse_repo_owner="*", analyse_repo_name="*")
    @templateArgs
    def do_analyse_repo(self, analyse_repo_owner, analyse_repo_name):
        self._analyse_repo_owner = analyse_repo_owner
        self._analyse_repo_name = analyse_repo_name
        return """
<div class="container-fluid">
    <div class="dropdown center-block col-sm-2">
        <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
            Select Repo Data Set
            <span class="caret"></span>
        </button>
        <ul class="dropdown-menu" style="list-style:none;margin:0px;padding:0px">
            {%for analysis,_ in this.analyses%}
                <li>
                    <a href="#" pd_options="analyse_type={{analysis}}" pd_target="analyse_vis{{prefix}}"
                     style="text-decoration: none;background-color:transparent">
                        {{analysis}}
                    </a>
                </li>
            {%endfor%}
        </ul>
    </div>
    <div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""

注意

您可以在此处找到代码文件

注意

上面的代码中有两点需要注意:

  • 即使analyses变量未定义为类变量,Jinja2 模板也使用this关键字引用analyses数组。 之所以有效,是因为 PixieApp 的另一个重要功能:在笔记本本身定义的任何变量都可以被引用,就好像它们是 PixieApp 的类变量一样。
  • 我将analyse_repo_owneranalyse_repo_name存储为具有不同名称的类变量,例如_analyse_repo_owner_analyse_repo_name。 这很重要,因为使用相同的名称会对路由匹配算法产生副作用,该算法还会查看类变量以查找参数。 使用相同的名称将导致始终找到此路由,这不是理想的效果。

操作按钮链接由<a>标记定义,并使用pd_options访问具有一个称为analyse_type以及pd_target指向"analyse_vis{{prefix}}"占位符<div>,的自变量的路由。 在以下相同的 HTML 片段中定义。

使用pd_entity属性调用 PixieDust display() API

当使用pd_options属性创建内核请求时,PixieApp 框架将当前 PixieApp 类用作目标。 但是,您可以通过指定pd_entity属性来更改此目标。 例如,您可以指向另一个 PixieApp,或更有趣的是,指向display() API 支持的数据结构,例如 Pandas 或 Spark DataFrame。 在这种情况下,如果您提供了display() API 预期的正确选项,则生成的输出将为图表本身(对于 Matplotlib,为图像;对于 Mapbox,为 IFRAME;对于 Mapbox,则为 SVG 散景)。 一种获取正确选项的简单方法是在其自己的单元格中调用display() API,使用菜单将其配置为所需的图表,然后通过单击编辑元数据复制可用的单元格元数据 JSON 片段。 按钮。 (您可能首先需要使用菜单视图 | 单元格工具栏 | 编辑元数据来启用按钮)。

您也可以指定pd_entity,不带任何值。 在这种情况下,PixieApp 框架将使用传递为用于启动 PixieApp 应用的run方法的第一个参数的实体。 例如,以cars为 Pandas 的my_pixieapp.run(cars)或通过pixiedust.sampleData()方法创建的 Spark DataFramepd_entity的值也可以是返回实体的函数调用。 当您要在渲染之前动态计算实体时,这很有用。 与其他变量一样,pd_entity的范围可以是 PixieApp 类或在笔记本中声明的任何变量。

例如,我们可以在其自己的单元格中创建一个函数,该函数将前缀作为参数并返回 Pandas DataFrame。 然后我们将其用作我的 PixieApp 中的pd_entity值,如以下代码所示:

def compute_pdf(key):
    return pandas.DataFrame([
        {"col{}".format(i): "{}{}-{}".format(key,i,j) for i in range(4)} for j in range(10)
    ])

注意

您可以在此处找到代码文件

在前面的代码中,我们使用 Python 列表推导快速基于key参数生成模拟数据。

注意

Python 列表推导是我最喜欢的 Python 语言功能之一,因为它们使您可以使用表达简洁的语法来创建,转换和提取数据。

我可以然后创建一个 PixieApp,使用compute_pdf函数作为pd_entity将数据呈现为表格:

from pixiedust.display.app import *
@PixieApp
class TestEntity():
    @route()
    def main_screen(self):
        return """
        <h1><center>
            Simple PixieApp with dynamically computed dataframe
        </center></h1>
        <div pd_entity="compute_pdf('prefix')" pd_options="handlerId=dataframe" pd_render_onload></div>
        """
test = TestEntity()
test.run()

注意

您可以在此处找到代码文件

在前面的代码中,为简单起见,我将键硬编码为'prefix',然后将其作为练习使用输入控件和$val()指令使其可用户定义。

值得注意的另一件事是在显示图表的 DIV 中使用pd_render_onload属性。 此属性告诉 PixieApp 在将元素加载到浏览器 DOM 中后立即执行该元素定义的内核请求。

以下屏幕快照显示了先前 PixieApp 的结果:

Invoking the PixieDust display() API using pd_entity attribute

在 PixieApp 中动态创建DataFrame

回到我们的Github Tracking应用,现在让我们将pd_entity值应用于从 GitHub Statistics API 加载的DataFrame。 我们创建了一个称为load_commit_activity,的方法,该方法负责将数据加载到 Pandas DataFrame中,并将其与显示图表所需的pd_options一起返回:

from datetime import datetime
import requests
import pandas
def load_commit_activity(owner, repo_name):
    response = requests.get(
        "https://api.github.com/repos/{}/{}/stats/commit_activity".format(owner, repo_name),
        auth=(github_user, github_token)
    ).json()
    pdf = pandas.DataFrame([
        {"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response
    ])

    return {
        "pdf":pdf,
        "chart_options": {
          "handlerId": "lineChart",
          "keyFields": "week",
          "valueFields": "total",
          "aggregation": "SUM",
          "rendererId": "bokeh"
        }
    }

注意

您可以在此处找到代码文件

前面的代码将 GET 请求发送到 GitHub,并使用在笔记本开始时设置的github_usergithub_token变量进行认证。 响应是一个 JSON 有效负载,我们将使用它创建一个 Pandas DataFrame。 在创建DataFrame之前,我们需要将 JSON 有效负载转换为正确的格式。 现在,有效负载如下所示:

[
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485046800},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1485651600},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486256400},
{"days":[0,0,0,0,0,0,0],"total":0,"week":1486861200}
...
]

我们需要删除days键,因为它不需要显示图表,并且,为了正确显示图表,我们需要将week键的值(它是 Unix 时间戳)转换为 Python datetime对象 。 这种转换是通过 Python 列表理解和简单的代码行完成的:

[{"total": item["total"], "week":datetime.fromtimestamp(item["week"])} for item in response]

在当前实现中,load_commit_activity函数在其自己的单元格中定义,但我们也可以将其定义为 PixieApp 的成员方法。 最佳实践是使用自己的单元非常方便,因为我们可以对功能进行单元测试并对其进行快速迭代,而不会产生每次运行完整应用的开销。

要获取pd_options值,我们可以简单地使用示例回购信息运行该函数,然后在单独的单元格中调用display() API:

Invoking the PixieDust display() API using pd_entity attribute

在单独的单元格中使用display()获取可视化配置

要获取上述图表,您需要选择折线图,然后在选项对话框中,将week列拖放到框和total列到框。 您还需要选择 Bokeh 作为渲染器。 完成后,请注意,PixieDust 将自动检测到x轴为日期时间,并将相应地调整渲染。

使用编辑元数据按钮,我们现在可以复制图表选项 JSON 片段:

Invoking the PixieDust display() API using pd_entity attribute

捕获display() JSON 配置

然后在load_commit_activity有效载荷中返回它:

return {
        "pdf":pdf,
        "chart_options": {
          "handlerId": "lineChart",
          "keyFields": "week",
          "valueFields": "total",
          "aggregation": "SUM",
          "rendererId": "bokeh"
        }
    }

现在,我们准备在RepoAnalysis类中实现do_analyse_type路由,如以下代码所示:

[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
    fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
    if len(fn) == 0:
        return "No loader function found for {{analyse_type}}"
    vis_info = fn[0](self._analyse_repo_owner, self._analyse_repo_name)
    self.pdf = vis_info["pdf"]
    return """
    <div pd_entity="pdf" pd_render_onload>
        <pd_options>{{vis_info["chart_options"] | tojson}}</pd_options>
    </div>
    """

注意

您可以在此处找到代码文件

路由有一个名为analyse_type,的参数,我们将其用作在analyses数组中查找load函数的键(注意,我再次使用列表推导来快速进行搜索)。 然后,我们调用传递回购所有者和名称的此函数来获取vis_info JSON 有效负载,并将 Pandas DataFrame存储到名为pdf的类变量中。 然后,返回的 HTML 片段将pdf用作pd_entity值,将vis_info["chart_options"]用作pd_optio ns。 在这里,我使用tojson Jinja2 过滤器来确保在生成的 HTML 中正确进行了转义 。 即使已在栈上声明了vis_info变量,也允许我使用它,因为我为函数使用了@templateArgs装饰器。

在测试改进的应用之前,要做的最后一件事是确保主要的GitHubTracking PixieApp 类继承自RepoAnalysis PixieApp:

@PixieApp
class GitHubTracking(RepoAnalysis):
    @route()
    def main_screen(self):
        <<Code omitted here>>

    @route(query="*")
    @templateArgs
    def do_search(self, query):
        <<Code omitted here>>

    @route(page="*")
    @templateArgs
    def do_retrieve_page(self, page):
        <<Code omitted here>>

app = GitHubTracking()
app.run()

注意

您可以在此处找到代码文件

“回购分析”页面的屏幕快照如下所示:

Invoking the PixieDust display() API using pd_entity attribute

GitHub 回购提交活动可视化

注意

如果您想进一步试验,可以在此处找到 GitHub 跟踪应用第 2 部分的完整笔记本

使用pd_script调用任意 Python 代码

在此部分中,我们研究pd_script定制属性,该属性使您可以在触发内核请求时运行任意 Python 代码。 有一些规则可以控制 Python 代码的执行方式:

  • 该代码可以使用self关键字以及在笔记本中定义的任何变量,函数和类访问 PixieApp 类,如以下示例所示:

    <button type="submit" pd_script="self.state='value'">Click me</button>
    
  • 如果指定了pd_target,则在target元素中将输出使用print函数的任何语句。 如果不存在pd_target,则不是这种情况。 换句话说,您不能使用pd_script进行整页刷新(您必须使用pd_options属性),例如:

    from pixiedust.display.app import *
    
    def call_me():
        print("Hello from call_me")
    
    @PixieApp
    class Test():
        @route()
        def main_screen(self):
            return """
            <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}">Click me</button>
    
            <div id="target{{prefix}}"></div>
            """
    Test().run()
    

    注意

    您可以在此处找到的代码文件

  • 如果代码包含多行,建议使用pd_script子元素,它使您可以使用多行编写 Python 代码。 使用此格式时,请确保代码遵循缩进的 Python 语言规则,例如:

    @PixieApp
    class Test():
        @route()
        def main_screen(self):
            return """
            <button type="submit" pd_script="call_me()" pd_target="target{{prefix}}">
                <pd_script>
                    self.name="some value"
                    print("This is a multi-line pd_script")
                </pd_script>
                Click me
            </button>
    
            <div id="target{{prefix}}"></div>
            """
    Test().run()
    

注意

您可以在此处找到代码文件

pd_script的一种常见用例是在触发内核请求之前更新服务器上的某些状态。 通过添加复选框在折线图和数据统计摘要之间切换可视化,让我们将此技术应用于Github Tracking应用。

do_analyse_repo返回的 HTML 片段中,我们添加了用于在图表和统计信息摘要之间切换的复选框元素:

[[RepoAnalysis]]
...
return """
<div class="container-fluid">
    <div class="col-sm-2">
        <div class="dropdown center-block">
            <button class="btn btn-primary
             dropdown-toggle" type="button"
             data-toggle="dropdown">
                Select Repo Data Set
                <span class="caret"></span>
            </button>
            <ul class="dropdown-menu"
             style="list-style:none;margin:0px;padding:0px">
                {%for analysis,_ in this.analyses%}
                    <li>
                        <a href="#"
                        pd_options="analyse_type={{analysis}}"
                        pd_target="analyse_vis{{prefix}}"
                        style="text-decoration: none;background-color:transparent">
                            {{analysis}}
                        </a>
                    </li>
                {%endfor%}
            </ul>
        </div>
        <div class="checkbox">
            <label>
                <input id="show_stats{{prefix}}" type="checkbox"
                  pd_script="self.show_stats=('$val(show_stats{{prefix}})' == 'true')">
                Show Statistics
            </label>
        </div>
    </div>
    <div id="analyse_vis{{prefix}}" class="col-sm-10"></div>
</div>
"""

checkbox元素中,包含pd_script属性,该属性根据checkbox元素的状态修改服务器上的变量状态。 我们使用$val()指令检索show_stats_{{prefix}}元素的值,并将其与true string进行比较。 当用户单击复选框时,服务器上的状态将立即更改,并且当用户下次单击菜单时,将显示统计信息而不是图表。

现在,我们需要更改do_analyse_type路由以动态配置pd_entitychart_options

[[RepoAnalysis]]
@route(analyse_type="*")
@templateArgs
def do_analyse_type(self, analyse_type):
    fn = [analysis_fn for a_type,analysis_fn in analyses if a_type == analyse_type]
    if len(fn) == 0:
        return "No loader function found for {{analyse_type}}"
    vis_info = fn[0](self._analyse_repo_owner, self._analyse_repo_name)
    self.pdf = vis_info["pdf"]
    chart_options = {"handlerId":"dataframe"} if self.show_stats else vis_info["chart_options"]
    return """
    <div pd_entity="get_pdf()" pd_render_onload>
        <pd_options>{{chart_options | tojson}}</pd_options>
    </div>
    """

注意

您可以在这里找到文件

chart_options现在是一个局部变量,如果show_statstrue,则包含显示为表格的选项;如果不是,则包含常规折线图选项。

pd_entity现在设置为get_pdf()方法,该方法负责基于show_stats变量返回适当的DataFrame

def get_pdf(self):
    if self.show_stats:
        summary = self.pdf.describe()
        summary.insert(0, "Stat", summary.index)
        return summary
    return self.pdf

注意

您可以在此处找到代码文件

我们使用 Pandas describe()方法返回包含摘要的DataFrame统计信息,例如计数,均值,标准差等。 我们还确保此DataFrame的第一列包含统计信息的名称。

我们需要做的最后一个更改是初始化show_stats变量,因为如果不这样做,那么第一次检查它时,我们会得到AttributeError异常。

由于使用@PixieApp装饰器的内部机制,因此无法使用__init__方法来初始化变量。 相反,PixieApp 编程模型要求您使用一种称为setup,的方法,该方法可以确保在应用启动时被调用:

@PixieApp
class RepoAnalysis():
    def setup(self):
        self.show_stats = False
    ...

注意

注意:如果您有一个从其他 PixieApps 继承的类,则 PixieApp 框架将使用它们的出现顺序自动从基类中调用所有setup函数。

以下屏幕截图显示了正在显示的摘要统计信息:

Invoking arbitrary Python code with pd_script

GitHub 存储库的摘要统计信息

注意

您可以在此处找到Github Tracking应用第 3 部分的完整笔记本

使用pd_refresh使应用响应更快

我们希望通过使显示统计信息按钮直接显示统计信息表格,而不是让用户再次单击菜单来改善用户体验。 类似于加载提交活动的菜单,我们可以向复选框添加pd_options属性,其中pd_target属性指向analyse_vis{{prefix}}元素。 无需在触发新显示的每个控件中复制pd_options,我们可以将其添加到analyse_vis{{prefix}}一次,并使用pd_refresh属性对其进行更新。

下图显示了两种设计之间的差异:

Making the application more responsive with pd_refresh

有和没有pd_refresh的序列图

在这两种情况下,步骤 1 都是在服务器端更新某些状态。 在步骤 2 中显示的控件调用路由的情况下,请求规范存储在控件本身中,触发步骤 3,该步骤将生成 HTML 片段并将其注入目标元素中 。 使用pd_refresh,控件不知道pd_options来调用路由,相反,它仅使用pd_refresh来向目标元素发信号,从而依次调用路由。 在这种设计中,我们只需要在目标元素中指定一次请求,并且用户控件只需要在触发刷新之前更新状态即可。 这使实现更易于维护。

为了更好地理解两种设计之间的差异,让我们比较RepoAnalysis类中的两种实现。

对于分析菜单,更改如下:

之前,控件触发了analyse_type路由,将{{analysis}}选择作为内核请求的一部分传递给了analyse_vis{{prefix}}

<a href="#" pd_options="analyse_type={{analysis}}"
            pd_target="analyse_vis{{prefix}}"
            style="text-decoration: none;background-color:transparent">
      {{analysis}}
</a>

之后,控件现在将选择状态存储为类字段,并要求analyse_vis{{prefix}}元素刷新自身:

<a href="#" pd_script="self.analyse_type='{{analysis}}'"
 pd_refresh="analyse_vis{{prefix}}"
 style="text-decoration: none;background-color:transparent">
    {{analysis}}
</a>

同样,显示统计信息复选框的更改如下:

在复选框之前,只需在类中设置show_stats状态即可; 用户必须再次单击菜单以获得可视化效果:

<div class="checkbox">
    <label>
        <input type="checkbox"
         id="show_stats{{prefix}}"
pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'">
        Show Statistics
    </label>
</div>

之后,由于具有pd_refresh属性,因此一旦选中该复选框,可视化文件就会更新:

<div class="checkbox">
    <label>
        <input type="checkbox"
         id="show_stats{{prefix}}"
  pd_script="self.show_stats='$val(show_stats{{prefix}})'=='true'"
         pd_refresh="analyse_vis{{prefix}}">
         Show Statistics
    </label>
</div>

最后,analyse_vis{{prefix}}元素的更改如下:

之前,该元素不知道如何更新自身,它依靠其他控件将请求定向到适当的路由:

<div id="analyse_vis{{prefix}}" class="col-sm-10"></div>

之后,元素将携带内核配置以进行自我更新。 任何控件现在都可以更改状态并调用刷新:

<div id="analyse_vis{{prefix}}" class="col-sm-10"
     pd_options="display_analysis=true"
     pd_target="analyse_vis{{prefix}}">
</div>

注意

您可以在以下位置找到Github Tracking应用第 4 部分的完整笔记本

创建可重用的小部件

PixieApp 编程模型提供了一种机制,用于将 HTML 和复杂 UI 构造的逻辑打包到一个小部件中,可以轻松地从其他 PixieApps 中调用该小部件。 创建窗口小部件的步骤如下:

  1. 创建一个将包含小部件的 PixieApp 类。

  2. C 创建一个带有特殊widget属性的路由,如示例所示:

    @route(widget="my_widget")
    

    这将是小部件的起始路径。

  3. 创建从小部件 PixieApp 类继承的使用者 PixieApp 类。

  4. 通过使用pd_widget属性从<div>元素调用窗口小部件。

这是的示例,介绍如何创建小部件和使用者 PixieApp 类:

from pixiedust.display.app import *

@PixieApp
class WidgetApp():
    @route(widget="my_widget")
    def widget_main_screen(self):
        return "<div>Hello World Widget</div>"

@PixieApp
class ConsumerApp(WidgetApp):
    @route()
    def main_screen(self):
        return """<div pd_widget="my_widget"></div>"""

ConsumerApp.run()

注意

您可以在此处找到代码

总结

在本章中,我们介绍了 PixieApp 编程模型的基本构建模块,使您可以直接在笔记本中创建强大的工具和仪表板。

我们还通过展示如何构建Github Tracking示例应用(包括详细的代码示例)来说明 PixieApp 的概念和技术。 最佳做法和更高级的 PixieApp 概念将在第 5 章,“最佳做法和高级 PixieDust 概念”中进行介绍,包括事件,流和调试。

到目前为止,您应该希望对 Jupyter 笔记本,PixieDust 和 PixieApps 如何使数据科学家和开发人员能够通过单一工具(例如 Jupyter 笔记本)进行协作来帮助弥合数据科学家和开发人员之间的差距有所了解。

在下一章中,我们将展示如何从笔记本中释放 PixieApp 并使用 PixieGateway 微服务服务器将其发布为 Web 应用。

四、使用 PixieGateway 服务器将 PixieApp 部署到 Web

“我认为数据是讲故事的最强大机制之一。我收集了大量的数据,然后尝试将其用于讲故事。”

Steven LevittFreakonomics 的合著者

在上一章中,我们讨论了 Jupyter 笔记本与 PixieDust 结合如何通过简单的 API 加速您的数据科学项目,这些 API 使您无需编写大量代码即可加载,清理和可视化数据,以及使数据科学家与 PixieApps 开发人员。 在本章中,我们将展示如何通过使用 PixieGateway 服务器将其发布为 Web 应用,从 Jupyter 笔记本中释放您的 PixieApps 和关联的数据分析。 笔记本电脑的这种操作方式对于想要使用 PixieApps 的业务用户角色(业务分析师,C-Suite 高管等等)特别有吸引力,但他们与数据科学家或开发人员不同,可能不喜欢使用 Jupyter 笔记本这样做。 相反,他们希望将其作为经典的 Web 应用来访问,或者可能类似于 YouTube 视频一样将其嵌入到博客文章或 GitHub 页面中。 使用网站或博客文章,可以轻松传达有价值的见解和从数据分析中提取的其他结果。

到本章末,您将能够在本地安装和配置 PixieGateway 服务器实例以进行测试,也可以在云中的 Kubernetes 容器中进行安装和配置以进行生产。 对于不熟悉 Kubernetes 的那些读者,我们将在下一部分中介绍这些基础知识。

我们将在本章中介绍的 PixieGateway 服务器的另一个主要功能是可以轻松共享使用 PixieDust display() API 创建的图表的功能。 我们将展示如何将其发布为网页,您的团队只需单击一个按钮即可访问。 最后,我们将介绍 PixieGateway 管理控制台,该控制台可让您管理应用,图表,内核,服务器日志以及对内核执行临时代码请求的 Python 控制台。

注意

注意:PixieGateway 服务器是 PixieDust 的子组件,可以在这里找到其源代码

Kubernetes 概述

Kubernetes 是一个可扩展开源系统,用于自动化和协调容器化应用的部署和管理,在云服务供应商中非常流行。 尽管支持其他类型的容器,但它最常用于 Docker 容器。 在开始之前,您需要访问已配置为作为 Kubernetes 群集的一组计算机; 您可以在此处找到有关如何创建此类集群的教程

如果您没有计算机资源,那么一个好的解决方案是使用提供 Kubernetes 服务的公共云供应商,例如 Amazon AWS EKSMicrosoft AzureIBM Cloud Kubernetes Service

为了更好地了解 Kubernetes 集群是如何工作的,让我们看下图所示的高级架构:

Overview of Kubernetes

Kubernetes 高级架构

在栈的顶部,我们具有kubectl命令行工具,该工具使用户能够通过向 Kubernetes 主节点发送命令来管理 Kubernetes 集群。 kubectl命令使用以下语法:

kubectl [command] [TYPE] [NAME] [flags]

在哪里:

  • command:这指定操作,例如creategetdescribedelete
  • TYPE:这指定资源类型,例如podsnodesservices
  • NAME:这指定资源的名称
  • flags:这指定了特定于操作的可选标志

注意

有关如何使用kubectl,的更多信息,请访问以下网站

工作节点中存在的另一个重要组件是 kubelet,它通过从 kube API 服务器读取 pod 配置来控制 pod 的生命周期。 它还负责与主节点的通信。 kube-proxy 根据主节点中指定的策略在所有 Pod 之间提供负载平衡功能,从而确保整个应用的高可用性。

在下一节中,我们将讨论安装和配置 PixieGateway 服务器的不同方法,包括使用 Kubernetes 集群的一种方法。

安装和配置 PixieGateway 服务器

在深入探讨技术细节之前,最好部署一个 PixieGateway 服务器实例进行尝试。

您可以尝试的安装有主要两种类型:本地安装和服务器安装。

本地安装:使用此方法进行测试和开发。

对于这一部分,我强烈建议您使用 Anaconda 虚拟环境,因为它们可以很好地隔离环境,从而您可以使用不同版本和配置的 Python 包进行实验。

如果要管理多个环境,则可以使用以下命令获取所有可用环境的列表:

conda env list

首先,通过终端使用以下命令选择所需的环境:

source activate <<my_env>>

您应该在终端中看到您的环境名称,这表明您已正确激活它。

接下来,通过运行以下命令从 PyPi 安装pixiegateway包:

pip install pixiegateway

注意

注意:您可以在此处找到有关 PyPi 上pixiegateway包的更多信息

一旦所有依赖项都已安装,就可以启动服务器了。 假设您要使用8899 port,则可以使用以下命令启动 PixieGateway 服务器:

jupyter pixiegateway --port=8899

示例输出应如下所示:

(dashboard) davids-mbp-8:pixiegateway dtaieb$ jupyter pixiegateway --port=8899
Pixiedust database opened successfully
Pixiedust version 1.1.10
[PixieGatewayApp] Jupyter Kernel Gateway at http://127.0.0.1:8899

注意

注意:要停止 PixieGateway 服务器,只需从终端使用Ctrl + C

现在,您可以通过以下 URL 打开 PixieGateway 管理控制台:http://localhost:8899/admin

注意

注意:遇到挑战时,请以admin作为用户,并使用空白(无密码)作为密码。 我们将在本章后面的“PixieGateway 服务器配置”部分中介绍如何配置安全性和其他属性。

使用 Kubernetes 和 Docker 的服务器安装:如果您需要在生产环境中运行 PixieGateway,并希望通过网络向多个用户提供已部署 PixieApps 的访问权限,请使用此安装方法。

以下说明将使用 IBM Cloud Kubernetes 服务,但它们可以轻松地适用于其他提供商:

  1. 如果您还没有一个 IBM Cloud 帐户,请创建一个 IBM Cloud 帐户并从目录中创建一个容器服务实例。

    注意

    注意:精简版计划可免费进行测试。

  2. 下载并安装 Kubernetes CLIIBM Cloud CLI

    注意

    注意:可以在以下位置找到有关 Kubernetes 容器的其他入门文章

  3. 登录到 IBM Cloud,然后定位 Kubernetes 实例所在的组织和空间。 安装并初始化container-service插件:

    bx login -a https://api.ng.bluemix.net
    bx target -o <YOUR_ORG> -s <YOUR_SPACE></YOUR_SPACE>
    bx plugin install container-service -r Bluemix
    bx cs init
    
    
  4. 检查已创建您的集群,如果未创建,请创建一个:

    bx cs clusters
    bx cs cluster-create --name my-cluster
    
    
  5. 下载将由kubectl命令使用的集群配置,该命令将在本地计算机上执行,稍后:

    bx cs cluster-config my-cluster
    
    

    前面的命令将生成一个临时 YML 文件,其中包含群集信息和环境变量export语句,在开始使用kubectl命令之前,您必须先运行该语句,如示例所示:

     export KUBECONFIG=/Users/dtaieb/.bluemix/plugins/container-
     service/clusters/davidcluster/kube-config-hou02-davidcluster.yml
    
    

    注意

    注意:YAML 是一种非常流行的数据序列化格式,通常用于系统配置。 您可以在这里找到更多信息

  6. 现在,您可以使用kubectl为您的 PixieGateway 服务器创建部署和服务。 为了方便起见,PixieGateway GitHub 存储库已经具有deployment.ymlservice.yml的通用版本,您可以直接参考。 我们将在本章稍后的“PixieGateway 服务器配置”部分中介绍如何为 Kubernetes 配置这些文件:

    kubectl create -f https://github.com/ibm-watson-data-lab/pixiegateway/raw/master/etc/deployment.yml
    kubectl create -f https://github.com/ibm-watson-data-lab/pixiegateway/raw/master/etc/service.yml
    
    
  7. 使用kubectl get命令验证集群的状态是的一个好主意:

    kubectl get pods
    kubectl get nodes
    kubectl get services
    
    
  8. 最后,您需要服务器的公共 IP 地址,您可以通过在终端中查看使用以下命令返回的输出的Public IP列来找到:

    bx cs workers my-cluster
    
    
  9. 如果一切顺利,您现在可以通过在http://<server_ip>>:32222/admin打开管理控制台来测试部署。 这次,管理控制台的默认凭据为admin/changeme,我们将在下一部分中说明如何更改它们。

Kubernetes 安装说明中使用的deployment.yml文件引用了一个 Docker 映像,该映像已预先安装和配置了 PixieGateway 二进制文件及其所有依赖项。 PixieGateway Docker 映像可从这个页面获得。

在本地工作时,建议的方法是遵循前面介绍的本地安装步骤。 但是,对于喜欢使用 Docker 映像的读者,可以通过简单的 Docker 命令将其直接安装在本地笔记本电脑上,而无需 Kubernetes 在本地试用 PixieGateway Docker 映像:

docker run -p 9999:8888 dtaieb/pixiegateway-python35

上面的命令假定您已经安装了 Docker,并且当前正在本地计算机上运行它。 如果不是,则可以从以下链接下载安装程序

如果不存在 Docker 映像,它将自动被拉出,并且容器将启动,在本地端口8888处启动 PixieGateway 服务器。 命令中的-p开关将容器本地的8888 port映射到主机本地的9999 port。 使用给定的配置,您可以通过以下 URL 访问 PixieGateway 服务器的 Docker 实例:http://localhost:9999/admin

注意

您可以在此处找到有关 Docker 命令行的更多信息

注意

注意:使用此方法的另一个原因是为 PixieGateway 服务器提供自己的自定义 Docker 映像。 如果您已经构建了 PixieGateway 的扩展并将其作为已配置的 Docker 映像提供给您的用户,这将非常有用。 关于如何从基本映像构建 Docker 映像的讨论不在本书的讨论范围内,但是您可以在此处找到详细信息

PixieGateway 服务器配置

PixieGateway 服务器的配置与配置 Jupyter 内核网关非常相似。 大多数选项是使用 Python 配置文件配置的; 首先,您可以使用以下命令生成模板配置文件:

jupyter kernelgateway --generate-config

jupyter_kernel_gateway_config.py模板文件将在~/.jupyter目录下生成(~表示用户主目录)。 您可以在此处找到有关标准 Jupyter 内核网关选项的更多信息

当您在本地工作并且可以轻松访问文件系统时,可以使用jupyter_kernel_gateway_config.py文件。 使用 Kubernetes 安装时,建议将选项配置为环境变量,您可以使用预定义的env类别直接在deployment.yml文件中进行设置。

现在让我们看一下 PixieGateway 服务器的每个配置选项。 此处同时使用 Python 和 Environment 方法提供了一个列表:

注意

注意:提醒一下,Python 方法意味着在jupyter_kernel_gateway_config.py Python 配置文件中设置参数,而 Environment 方法意味着在 Kubernetes deployment.yml文件中设置参数。

  • 管理控制台凭据:为管理控制台配置用户 ID /密码:

    • PythonPixieGatewayApp.admin_user_idPixieGatewayApp.admin_password
    • 环境ADMIN_USERIDADMIN_PASSWORD
  • 存储连接器:为各种资源(例如图表和笔记本)配置永久存储。 默认情况下,PixieGateway 使用本地文件系统。 例如,它将发布的笔记本存储在~/pixiedust/gateway目录下。 对于本地测试环境,使用本地文件系统可能很好,但是在使用 Kubernetes 安装时,您将需要显式使用持久卷,它可能很难使用。 如果没有采用持久性策略,则在重新启动容器时,将删除持久文件,并且所有已发布的图表和 PixieApps 都将消失。 PixieGateway 提供了另一个选项,它是配置一个存储连接器,使您可以使用所选的机制和后端持久化数据。

    要为图表配置存储连接器,必须在以下任一配置变量中指定标准类名:

    • PythonSingletonChartStorage.chart_storage_class
    • 环境PG_CHART_STORAGE

    引用的连接器类必须继承pixiegateway.chartsManager包中定义的ChartStorage抽象类(可以在此处找到实现)。

    PixieGateway 提供了到 Cloudant/CouchDB NoSQL 数据库的现成连接器。 要使用此连接器,您需要将连接器类设置为pixiegateway.chartsManager.CloudantChartStorage。 您还需要指定辅助配置变量以指定服务器和凭据信息(我们显示了 Python/Environment 表单):

    • CloudantConfig.host/PG_CLOUDANT_HOST
    • CloudantConfig.port/PG_CLOUDANT_PORT
    • CloudantConfig.protocol/PG_CLOUDANT_PROTOCOL
    • CloudantConfig.username/PG_CLOUDANT_USERNAME
    • CloudantConfig.password/PG_CLOUDANT_PASSWORD
  • 远程内核:指定远程 Jupyter 内核网关的配置。

    目前,仅在 Python 模式下支持此配置选项。 您需要使用的变量名称是ManagedClientPool.remote_gateway_config。 期望值是一个包含服务器信息的 JSON 对象,可以通过两种方式指定:

    • protocolhostport
    • notebook_gateway指定服务器的标准 URL

    根据内核配置,还可以使用两种方式提供安全性:

    • auth_token
    • userpassword

    在以下示例中可以看到:

    c.ManagedClientPool.remote_gateway_config={
        'protocol': 'http',
        'host': 'localhost',
        'port': 9000,
        'auth_token':'XXXXXXXXXX'
    }
    
    c.ManagedClientPool.remote_gateway_config={
        'notebook_gateway': 'https://YYYYY.us-south.bluemix.net:8443/gateway/default/jkg/',
        'user': 'clsadmin',
        'password': 'XXXXXXXXXXX'
    }
    

    注意

    注意,在前面的示例中,您需要在变量前面加上c.。 这是来自底层 Jupyter/IPython 配置机制的要求。

    作为参考,以下是使用 Python 和 Kubernetes 环境变量格式的完整配置示例文件:

  • 以下是jupyter_kernel_gateway_config.py的内容:

    c.PixieGatewayApp.admin_password = "password"
    
    c.SingletonChartStorage.chart_storage_class = "pixiegateway.chartsManager.CloudantChartStorage"
    c.CloudantConfig.host="localhost"
    c.CloudantConfig.port=5984
    c.CloudantConfig.protocol="http"
    c.CloudantConfig.username="admin"
    c.CloudantConfig.password="password"
    
    c.ManagedClientPool.remote_gateway_config={
        'protocol': 'http',
        'host': 'localhost',
        'port': 9000,
        'auth_token':'XXXXXXXXXX'
    }
    
  • 以下是Deployment.yml的内容:

    apiVersion: extensions/v1beta1
    kind: Deployment 
    metadata:
      name: pixiegateway-deployment
    spec:
      replicas: 1
      template:
        metadata:
          labels:
            app: pixiegateway
        spec:
          containers:
            - name: pixiegateway
              image: dtaieb/pixiegateway-python35
              imagePullPolicy: Always
              env:
                - name: ADMIN_USERID
                  value: admin
                - name: ADMIN_PASSWORD
                  value: changeme
                - name: PG_CHART_STORAGE
                  value: pixiegateway.chartsManager.CloudantChartStorage
                - name: PG_CLOUDANT_HOST
                  value: XXXXXXXX-bluemix.cloudant.com
                - name: PG_CLOUDANT_PORT
                  value: "443"
                - name: PG_CLOUDANT_PROTOCOL
                  value: https
                - name: PG_CLOUDANT_USERNAME
                  value: YYYYYYYYYYY-bluemix
                - name: PG_CLOUDANT_PASSWORD
                  value: ZZZZZZZZZZZZZ
    

PixieGateway 架构

现在是重新查看第 2 章,“使用 Jupyter 笔记本和 PixieDust 的数据科学”提出的 PixieGateway 架构图的好时机。 该服务器被实现为 Jupyter 内核网关的自定义扩展(称为 Personality)。

反过来,PixieGateway 服务器提供了扩展点,以自定义某些行为,我们将在本章稍后讨论。

PixieGateway 服务器的高级架构图如下所示:

PixieGateway architecture

PixieGateway 架构图

如图所示,PixieGateway 为三种类型的客户端提供 REST 接口:

  • Jupyter 笔记本服务器:此调用一组专用的 REST API,用于共享图表并将 PixieApps 发布为 Web 应用
  • 运行 PixieApp 的浏览器客户端:特殊的 REST API 管理关联内核中 Python 代码的执行
  • 运行管理控制台的浏览器客户端:一组专用的 REST API,用于管理各种服务器资源和统计信息,例如 PixieApps 和内核实例

在后端,PixieGateway 服务器管理一个或多个负责运行 PixieApps 的 Jupyter Kernel 实例的生命周期。 在运行时,每个 PixieApp 都会使用一组特定的步骤部署在内核实例上。 下图显示了服务器上运行的所有 PixieApp 用户实例的典型拓扑:

PixieGateway architecture

运行的 PixieApp 实例的拓扑

在服务器上部署 PixieApp 时,将分析 Jupyter 笔记本每个单元中包含的代码并将其分为两个部分:

  • 预热代码:这是在主要 PixieApp 定义上方的所有单元格中定义的所有代码。 第一次在内核上启动 PixieApp 应用时,此代码仅运行一次,直到重新启动内核或从运行代码中显式调用它之前,该代码才再次运行。 这很重要,因为它将帮助您更好地优化性能。 例如,您应该始终将代码放置在“预热”部分中,该代码将加载大量数据,这些数据不会发生太大变化或可能需要很长时间进行初始化。

  • 运行代码:这是将在每个用户会话的自己实例中运行的代码。 运行代码通常是从包含 PixieApp 类声明的单元格中提取的。 发布者通过对 Python 代码进行静态分析并专门寻找以下两个条件来自动发现此单元:必须同时满足以下两个条件:

    • 该单元格包含一个带有@PixieApp注解的类

    • 该单元实例化该类并调用其run()方法

      @PixieApp
      class MyApp():
          @route()
          def main_screen(self):
          return "<div>Hello World</div>"
      
      app = MyApp()
      app.run()
      

    例如,以下代码必须位于其自己的单元格中才能成为运行代码:

    正如我们在第 3 章,“PixieApp”下看到的那样,可以在同一笔记本中声明多个 PixieApp,这些笔记本将用作子 PixieApp 或主要的 PixieApp。 在这种情况下,我们需要确保它们是在自己的单元格中定义的,并且您不要尝试实例化它们并调用其run()方法。

    规则是,只有一个主要的 PixieApp 类可以为其调用run()方法,并且包含该代码的单元格被 PixieGateway 视为运行代码。

    注意

    注意:在 PixieGateway 服务器执行的静态分析期间,未标记为代码的单元格(例如 Markdown,Raw NBConvert 或 Heading)将被忽略。 因此,将它们放在笔记本电脑中是安全的。

对于每个客户端会话,PixieGateway 将使用运行代码(在上图中以彩色六边形表示)实例化 PixieApp 主类的实例。 根据当前负载的,PixieGateway 将决定在一个特定的内核实例中应运行多少个 PixieApp,并在需要时自动生成一个新的内核来服务额外的用户。 例如,如果五个用户使用相同的 PixieApp,则三个实例可能正在特定的内核实例中运行,而另外两个实例将在另一个内核实例中运行。 PixieGateway 通过对多个内核之间的 PixieApps 实例进行负载平衡来持续监控使用模式,以优化工作负载分配。

为了帮助理解笔记本代码的分解方式,下图反映了如何从笔记本中提取预热和运行代码并进行转换以确保多个实例在同一内核中和平共存:

注意

提醒一下,包含主 PixieApp 的单元还必须具有将其实例化并调用run()方法的代码。

PixieGateway architecture

PixieApp 生命周期:预热和运行代码

由于给定的内核实例可以使用其主要 PixieApp 托管多个笔记本,因此我们需要确保在执行两个主要 PixieApp 的预热代码时不会发生意外的名称冲突。 例如,title变量可以在两个 PixieApps 中使用,并且如果单独使用,则第二个变量的值将覆盖第一个变量的值。 为避免这种冲突,通过注入名称空间使预热代码中的所有变量名称唯一。

发布后,title = 'some string'语句变为ns1_title = 'some string'。 PixieGateway 发布者还将在整个代码中更新对title的所有引用,以反映新名称。 所有这些重命名都是在运行时自动完成的,开发人员无需执行任何特定操作。

稍后我们将介绍管理控制台的“PixieApp 详细信息”页面时,将显示真实的代码示例。

提示

如果您已将主要 PixieApp 的代码打包为在笔记本中导入的 Python 模块,则仍需要声明继承自它的包装 PixieApp 的代码。 这是因为 PixieGateway 会进行静态代码分析,寻找@PixieApp表示法,如果找不到,将无法正确识别主 PixieApp。

例如,假设您有一个从awesome package导入的名为AwesomePixieApp的 PixieApp。 在这种情况下,您可以将以下代码放在自己的单元格中:

from awesome import AwesomePixieApp
@PixieApp
class WrapperAwesome(AwesomePixieApp):
    pass
app = WrapperAwesome()
app.run()

发布应用

在本部分中,我们将在第 3 章,“引擎盖下的 PixieApp”中创建的Github Tracking应用发布到 PixieGateway 实例中。

注意

您可以从以下 GitHub 位置使用完整的笔记本

在笔记本中,像往常一样运行该应用,并使用单元格输出左上方的发布按钮开始该过程:

Publishing an application

调用发布对话框

发布对话框具有多个选项卡菜单:

  • 选项

    • PixieGateway 服务器:例如,http://localhost:8899
    • 页面标题:在浏览器中显示时,将用作页面标题的页面简短说明
  • 安全:通过网络访问时配置 PixieApp 的安全性:

    • 没有安全
    • 令牌:必须将安全令牌作为查询参数添加到 URL,例如,http://localhost:8899/GitHubTracking?token=941b3990d5c0464586d67e48705b9deb

    注意

    注意:目前,PixieGateway 不提供任何认证/授权机制。 第三方授权,例如 OAuth 2.0JWT 等未来将被添加。

  • 导入:显示由 PixieDust 发布者自动检测到的 Python 包依赖项列表。 这些导入的包(如果尚不存在)将自动安装在运行应用的内核上。 当检测到特定的依赖项时,PixieDust 会查看当前系统以获取版本和安装位置,例如 PyPi 或自定义安装 URL(例如 GitHub 存储库)。

  • 内核规范:您可以在此处为 PixieApp 选择内核规范。 默认情况下,PixieDust 选择 PixieGateway 服务器上可用的默认内核,但是,例如,如果您的笔记本依赖于 Apache Spark,则您应该能够选择支持它的内核。 使用管理控制台部署 PixieApp 之后,也可以更改此选项。

这是 PixieApp 发布对话框的示例屏幕截图:

Publishing an application

PixieApp 发布对话框

单击发布按钮将启动发布过程。 完成后(取决于笔记本电脑的大小非常快),您将看到以下屏幕:

Publishing an application

成功发布屏幕

然后,您可以通过单击提供的链接来测试该应用,可以将其复制并与团队中的用户共享。 以下屏幕快照显示了Github Tracking应用在 PixieGateway 上作为 Web 应用运行的三个主屏幕:

Publishing an application

作为 Web 应用运行的 PixieApp

现在您已经知道如何发布 PixieApp,下面让我们回顾一些开发人员最佳实践和规则,这些经验和规则将帮助您优化打算作为 Web 应用发布的 PixieApp:

  • 为每个用户会话创建一个 PixieApp 实例,因此,为了提高性能,请确保它不包含长时间运行的代码或不加载大量静态数据(不经常更改的数据)的代码。 而是将其放在“预热代码”部分中,并根据需要从 PixieApp 进行引用。

  • 不要忘记在同一单元格中添加运行 PixieApp 的代码。 否则,在网络上运行时将得到空白页。 作为一种好的做法,建议将 PixieApp 实例分配到其自己的变量中。 例如,执行以下操作:

    app = GitHubTracking()
    app.run()
    

    那不是下面的

    GitHubTracking().run()
    
  • 您可以在同一笔记本中声明多个 PixieApp 类,如果您使用子 PixieApp 或 PixieApp 继承,则需要此类。 但是,只有其中一个可以是 PixieGateway 将运行的主要 PixieApp。 它是具有实例化并运行 PixieApp 的额外代码的代码。

  • Docstring 添加到您的 PixieApp 类中是一个好主意,它对应用进行了简短描述。 正如我们在本章稍后的 PixieGateway 管理控制台部分中所见,此文档字符串将显示在 PixieGateway 管理控制台中,如以下示例所示:

    @PixieApp
    class GitHubTracking(RepoAnalysis):
        """
        GitHub Tracking Sample Application
        """
        @route()
        def main_screen(self):
            return """
        ...
    

PixieApp URL 中的编码状态

在某些情况下,您可能希望在 URL 中捕获 PixieApp 的状态作为查询参数,以便可以将其标记为书签和/或与其他人共享。 这个想法是,当使用查询参数时,PixieApp 不是从主屏幕启动,而是自动激活与参数相对应的路由。 例如,在Github Tracking应用中,您可以使用http://localhost:8899/pixieapp/GitHubTracking?query=pixiedust绕过初始屏幕,直接跳转到显示与给定查询匹配的存储库列表的表。

通过将persist_args特殊参数添加到路由,可以在激活路由时将查询参数自动添加到 URL。

do_search()路由如下所示:

@route(query="*", persist_args='true')
@templateArgs
def do_search(self, query):
    self.first_url = "https://api.github.com/search/repositories?q={}".format(query)
    self.prev_url = None
    self.next_url = None
    self.last_url = None
    ...

注意

您可以在此处找到代码文件

关键字persist_args不会影响路由的激活方式。 只有才能在激活时自动将适当的查询参数添加到 URL。 您可以尝试在笔记本中进行此简单更改,将 PixieApp 重新发布到 PixieGateway 服务器,然后进行尝试。 在第一个屏幕上单击“提交”按钮后,您会注意到 URL 会自动更新为包含查询参数。

注意

注意persist_args参数在笔记本中运行时也可以使用,尽管实现不同,因为我们没有 URL。 而是使用pixieapp键将参数添加到单元元数据,如以下屏幕截图所示:

Encoding state in the PixieApp URL

显示 PixieApp 参数的单元元数据

如果您正在使用persist_args函数,则可能会发现在进行迭代开发时,总是去单元格元数据删除参数变得很麻烦。 作为一种快捷方式,PixieApp 框架在右上方的工具栏中添加了一个主页按钮,只需单击一下即可重置参数。

作为的替代方案,您还可以避免在笔记本中运行时将路由参数完全保存在单元元数据中(但在 Web 上运行时仍然保存)。 为此,您需要使用web作为persist_args参数的值,而不是true

@route(query="*", persist_args='web')
…

通过将图表发布为网页来共享

在此部分中,我们展示了如何轻松共享由display() API 创建的图表并将其发布为网页。

使用第 2 章“使用 Jupyter 笔记本和 PixieDust 的大规模数据科学”的示例,让我们加载汽车性能数据集并使用display()创建图表:

import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True) #car performance data
display(cars)

注意

您可以在此处找到代码文件

在 PixieDust 输出界面中,选择条形图菜单,然后在选项对话框中,为选择horsepower,为选择mpg,如以下屏幕截图所示:

Sharing charts by publishing them as web pages

PixieDust Chart 选项

然后,我们使用共享按钮调用图表共享对话框,如以下屏幕截图所示,该屏幕截图使用 Bokeh 作为渲染器:

注意

注意:图表共享可与任何渲染器一起使用,我建议您与其他渲染器(例如 Matplotlib 和 Mapbox)一起尝试。

Sharing charts by publishing them as web pages

调用共享图表对话框

共享图表对话框中,您可以为图表指定 PixieGateway 服务器和可选描述:

注意

请注意,为方便起见,PixieDust 将自动记住上一次使用的设备。

Sharing charts by publishing them as web pages

共享图表对话框

单击共享按钮上的将启动发布过程,该过程会将图表内容带到 PixieGateway,然后将唯一的 URL 返回到网页。 类似于 PixieApp,然后您可以与团队共享此 URL:

Sharing charts by publishing them as web pages

图表共享确认对话框

确认对话框包含图表的唯一 URL 和 HTML 片段,可用于将图表嵌入到自己的网页(例如博客文章和仪表板)中。

单击链接上的将显示以下 PixieGateway 页面:

Sharing charts by publishing them as web pages

图表页面

前一页显示有关图表的元数据,例如作者说明日期以及嵌入式 HTML 片段。 请注意,如果图表具有交互性(例如 Bokeh,Brunel 或 Mapbox),则将其保留在 PixieGateway 页面中。

例如,在前面的屏幕截图中,用户仍然可以滚轮缩放,框缩放和平移以浏览图表或将图表下载为 PNG 文件。

将图表嵌入到自己的页面中也非常容易。 只需将嵌入式 HTML 片段复制到 HTML 中的任意位置,如以下示例所示:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Example page with embedded chart</title>
    </head>
    <body>
        <h1> Embedded a PixieDust Chart in a custom HTML Page</h1>
        <div>
            <object type="text/html" width="600" height="400"
                data="http://localhost:8899/embed/04089782-7543-42a6-8dd1-e4d1cb06596a/600/400"> 
                <a href="http://localhost:8899/embed/04089782-7543-42a6-8dd1-e4d1cb06596a">View Chart</a>
            </object>
        </div>
    </body>
</html>

注意

您可以在此处找到代码文件

提示

嵌入式图表对象必须使用与浏览器相同的安全级别或更高的安全级别。 否则,浏览器将抛出混合内容错误。 例如,如果宿主页面是通过 HTTPS 加载的,那么嵌入式图表也必须通过 HTTPS 加载,这意味着您需要在 PixieGateway 服务器中启用 HTTPS。 您也可以访问这个页面为 PixieGateway 服务器配置 SSL/TLS 证书。 另一个易于维护的解决方案是为提供 TLS 终止的 Kubernetes 集群配置 Ingress 服务。

为方便起见,我们在此处为 PixieGateway 服务提供模板入口 YAML 文件。 您将需要使用 TLS 主机和提供商提供的密码来更新此文件。 例如,如果您正在使用 IBM Cloud Kubernetes 服务,则只需在<your cluster name>占位符中输入集群名称。 我们在此处为 PixieGateway 服务提供模板入口 YAML 文件。 入口服务是提高安全性,可靠性和防御 DDOS 攻击的好方法。 例如,您可以设置各种限制,例如每个唯一 IP 地址允许的每秒请求/连接数或允许的最大带宽。 有关更多信息,请参见这个页面

PixieGateway 管理控制台

管理员控制台是管理资源并对它们进行故障排除的好工具。 您可以使用/admin URL 访问它。 请注意,您将需要使用您配置的用户/密码进行认证(请参阅“PixieGateway 服务器配置”部分,以获取本章中有关如何配置用户/密码的说明;默认情况下,用户为admin密码为<空白>)。

管理控制台的用户界面由专注于特定任务的多个菜单组成。 让我们一一看一下:

  • PixieApps:

    • 有关所有已部署的 PixieApps 的信息:URL,描述等
    • 安全管理
    • 操作,例如删除和下载

    PixieGateway admin console

    管理控制台 PixieApp 管理页面

  • 图表:

    • 有关所有已发布图表的信息:链接,预览等
    • 对于示例的操作,删除,下载和嵌入片段

    PixieGateway admin console

    管理控制台图表管理页面

  • 内核统计:

    以下屏幕截图显示了内核统计屏幕:

    PixieGateway admin console

    管理控制台“内核统计信息”页面

    该屏幕显示了当前在 PixieGateway 中运行的所有内核的实时表。 每行包含以下信息:

    • 内核名称:这是带有钻取链接的内核名称,其中显示内核规范日志Python 控制台
    • 状态:将状态显示为idlebusy
    • 繁忙比率:这是一个介于 0 到 100% 之间的值,表示自启动以来的内核利用率。
    • 正在运行的应用:这是正在运行的 PixieApps 的列表。 每个 PixieApp 是一个向下钻取链接,该链接显示预热代码并运行 PixieApp 的代码。 这对于解决错误非常有用,因为您可以看到 PixieGateway 正在运行什么代码。
    • 用户计数:这是此内核中具有打开会话的用户数。
  • 服务器日志:

    完全访问龙卷风服务器日志以进行故障排除

    PixieGateway admin console

    管理控制台服务器日志页面

Python 控制台

通过单击内核状态屏幕中的内核链接来调用 Python 控制台。 管理员可以使用它对内核执行任何代码,这对于解决问题很有用。

例如,以下屏幕截图显示了如何调用 PixieDust 日志:

Python Console

从 PixieGateway 管理员 Python 控制台显示 PixieDust 日志

显示 PixieApp 的预热并运行代码

当加载页面时发生执行错误时,PixieGateway 将在浏览器中显示完整的 Python 追溯。 但是,可能很难找到该错误,因为其根本原因可能在于启动 PixieApp 时执行的预热代码。 一种重要的调试技术是查看预热并运行 PixieGateway 执行的代码以发现任何异常。

如果错误仍然不明显,您可以例如在一个临时的笔记本中复制预热并运行代码,然后尝试从那里运行它,希望您可以重现该错误并找出问题所在。

您可以通过单击内核状态屏幕上的 PixieApp 链接来访问预热和运行代码,这将带您进入以下屏幕:

Displaying warmup and run code for a PixieApp

显示预热并运行代码

请注意,预热和运行代码不包含原始代码格式,因此可能更难以阅读。 您可以通过复制该问题并将代码粘贴到临时笔记本中并再次重新格式化来缓解此问题。

总结

阅读完本章后,您应该能够安装,配置和管理 PixieGateway 微服务服务器,将图表作为网页发布,以及将 PixieApp 从笔记本部署到 Web 应用。 无论您是在 Jupyter 笔记本中从事分析工作的数据科学家还是开发人员针对企业用户编写和部署应用的开发人员,我们在本章中均已展示 PixieDust 如何帮助您更有效地完成任务并减少操作分析所需的时间。

在下一章中,我们将研究与 PixieDust 和 PixieApp 编程模型相关的高级主题和最佳实践,这在其余各章中讨论行业用例和示例数据管道时将非常有用。

五、最佳实践和高级 PixieDust 概念

“我们相信上帝,所有其他人都带来了数据。”

W. Edwards Deming

在本书的其余各章中,我们将深入研究行业用例的架构,包括示例数据管道的实现,并大量应用到目前为止所学的技术。 在开始查看代码之前,让我们用一些最佳实践和高级 PixieDust 概念完善我们的工具箱,这将对示例应用的实现有用:

  • 使用@captureOutput装饰器调用第三方 Python 库

  • 增加 PixieApp 的模块化和代码重用

  • PixieDust 对流数据的支持

  • 通过 PixieApp 事件添加仪表板明细

  • 使用自定义显示渲染器扩展 PixieDust

  • 调试:

    • 在 Jupyter 笔记本上使用 pdb 运行逐行 Python 代码调试
    • 使用 PixieDebugger 进行视觉调试
    • 使用 PixieDust 日志记录框架对问题进行故障排除
    • 客户端 JavaScript 调试的提示
  • 在 Python 笔记本中运行 Node.js

使用@captureOutput装饰器集成第三方 Python 库的输出

假设您想在已经使用了一段时间的第三方库中重用 PixieApp,以便执行某些任务,例如,使用 Scikit-learn 机器学习库计算集群并将其显示为图形。 问题在于大多数时候,您正在调用一个高级方法,该方法不返回数据,而是直接在单元格输出区域上绘制某些内容,例如图表或报表。 从 PixieApp 路由调用此方法将不起作用,因为路由协定是返回将由框架处理的 HTML 片段字符串。 在这种情况下,该方法很可能不返回任何内容,因为它直接将结果写入单元格输出中。 解决方案是在路由方法中使用@captureOutput装饰器(它是 PixieApp 框架的一部分)。

使用@captureOutput创建词云图像

为了更好地演示前面描述的@captureOutput场景,让我们举一个具体的示例,在该示例中我们要构建一个使用wordcloud Python 库的 PixieApp,以根据用户通过网址提供的文本文件生成文字云图片。

我们首先通过在其自己的单元格中运行以下命令来安装wordcloud库:

!pip install wordcloud

注意

注意:请确保在wordcloud库的安装完成后重新启动内核。

PixieApp 的代码如下所示:

from pixiedust.display.app import *
import requests
from wordcloud import WordCloud
import matplotlib.pyplot as plt

@PixieApp
class WordCloudApp():
    @route()
    def main_screen(self):
        return """
        <div style="text-align:center">
            <label>Enter a url: </label>
            <input type="text" size="80" id="url{{prefix}}">
            <button type="submit"
                pd_options="url=$val(url{{prefix}})"
                pd_target="wordcloud{{prefix}}">
                Go
            </button>
        </div>
        <center><div id="wordcloud{{prefix}}"></div></center>
        """

    @route(url="*")
    @captureOutput
    def generate_word_cloud(self, url):
        text = requests.get(url).text
        plt.axis("off")
        plt.imshow(
            WordCloud(max_font_size=40).generate(text),
            interpolation='bilinear'
        )

app = WordCloudApp()
app.run()

注意

您可以在此处找到代码

注意,只需将@captureOutput装饰器添加到generate_word_cloud路由,我们不再需要返回 HTML 片段字符串。 我们可以简单地调用 Matplotlib imshow()函数,该函数将图像发送到系统输出。 PixieApp 框架将负责捕获输出并将其打包为 HTML 片段字符串,并将其插入正确的 DIV 占位符中。 结果如下:

注意

注意:我们使用来自 GitHub 上wordcloud存储库的以下输入 URL

注意

使用的另一个好的链接是

Create a word cloud image with @captureOutput

简单的 PixieApp,可从文本生成词云

任何直接绘制到单元格输出的函数都可以与@captureOutput装饰器一起使用。 例如,您可以对 HTML 或 JavaScript 类使用 Matplotlib show()方法或 IPython display()方法。 您甚至可以使用display_markdown()方法通过 Markdown 标记语言输出富文本,如以下代码所示:

from pixiedust.display.app import *
from IPython.display import display_markdown

@PixieApp
class TestMarkdown():
    @route()
    @captureOutput
    def main_screen(self):
        display_markdown("""
# Main Header:
## Secondary Header with bullet
1\. item1
2\. item2
3\. item3

Showing image of the PixieDust logo
![alt text](https://github.com/pixiedust/pixiedust/raw/master/docs/_static/PixieDust%202C%20\(256x256\).png "PixieDust Logo")
    """, raw=True)

TestMarkdown().run()

这将产生以下结果:

Create a word cloud image with @captureOutput

PixieApp 使用@captureOutput 和 Markdown

改进模块化和代码重用

将分解为较小的自包含组件始终是良好的开发实践,因为它使代码可重用且易于维护。 PixieApp 框架提供了两种创建和运行可重用组件的方式:

  • 具有pd_app属性的动态调用其他 PixieApps
  • 将应用的一部分打包为可重用的小部件

使用pd_app属性,您可以通过其完全限定的类名动态调用另一个 PixieApp(从此处开始,将其称为子 PixieApp)。 子 PixieApp 的输出通过使用runInDialog=true选项放置在宿主 HTML 元素(通常是 DIV 元素)或对话框中。 您也可以使用pd_options属性初始化子 PixieApp,在这种情况下,框架将调用相应的路由。

为了更好地理解pd_app的工作原理,让我们通过重构在自己的 PixieApp 中生成WordCloud图像的代码(我们称为WCChildApp)来重写WordCloud应用。

以下代码将WCChildApp实现为常规 PixieApp,但请注意,它不包含默认路由。 它只有一条称为generate_word_cloud的路由,应该由另一个 PixieApp 使用url参数调用该路由:

from pixiedust.display.app import *
import requests
from wordcloud import WordCloud
import matplotlib.pyplot as plt

@PixieApp
class WCChildApp():
    @route(url='*')
    @captureOutput
    def generate_word_cloud(self, url):
        text = requests.get(url).text
        plt.axis("off")
        plt.imshow(
            WordCloud(max_font_size=40).generate(text),
            interpolation='bilinear'
        )

注意

您可以在此处找到代码文件

现在,我们可以构建主 PixieApp,当用户在指定 URL 后单击前进按钮时,它将调用WCChildApp

@PixieApp
class WordCloudApp():
    @route()
    def main_screen(self):
        return """
        <div style="text-align:center">
            <label>Enter a url: </label>
            <input type="text" size="80" id="url{{prefix}}">
            <button type="submit"
                pd_options="url=$val(url{{prefix}})"
                pd_app="WCChildApp"
                pd_target="wordcloud{{prefix}}">
                Go
            </button>
        </div>
        <center><div id="wordcloud{{prefix}}"></div></center>
        """

app = WordCloudApp()
app.run()

注意

您可以在此处找到代码文件

在前面的代码中,Go按钮具有以下属性:

  • pd_app="WCChildApp":使用子 PixieApp 的类名。 请注意,如果您的子 PixieApp 生活在导入的 Python 模块中,则需要使用完全限定的名称。
  • pd_options="url=$val(url{{prefix}})":将用户输入的 URL 作为初始化选项存储到子 PixieApp。
  • pd_target="wordcloud{{prefix}}":告诉 PixieDust 将子 PixieApp 的输出放在 ID 为wordcloud{{prefix}}的 DIV 中。

通过封装组件的逻辑和表示,pd_app属性是一种将代码模块化的强大方法。 pd_widget属性提供了另一种获得相似结果的方法,但是这次该组件不是在外部调用,而是通过继承调用。

每种方法都有优点和缺点:

  • pd_widget技术被实现为路由,并且肯定比pd_app,轻巧,后者需要创建一个全新的 PixieApp 实例。 请注意,pd_widgetpd_app(通过parent_pixieapp变量)都可以访问主机应用中包含的所有变量。

  • pd_app属性提供了组件之间更清晰的分隔,并且比小部件具有更大的灵活性。 例如,您可以具有一个按钮,该按钮可以根据某些用户选择动态调用多个 PixieApp。

    注意

    注意:正如本章稍后将看到的,这实际上是 PixieDust 显示器用于选项对话框的内容。

如果您发现自己需要在 PixieApp 中拥有同一组件的多个副本,请询问自己该组件是否需要在类变量中维护其状态。 如果是这种情况,最好使用pd_app,,但如果不是,那么也可以使用pd_widget

使用pd_widget创建小部件

要创建小部件,可以使用以下步骤:

  1. 创建一个 PixieApp 类,该类包含标记有名为widget的特殊参数的路由
  2. 使主类继承自 PixieApp 小部件
  3. 使用 DIV 元素上的pd_widget属性调用窗口小部件

再次,作为说明,让我们用小部件重写WordCloud应用:

from pixiedust.display.app import *
import requests
from word cloud import WordCloud
import matplotlib.pyplot as plt

@PixieApp
class WCChildApp():
    @route(widget='wordcloud')
    @captureOutput
    def generate_word_cloud(self):
        text = requests.get(self.url).text if self.url else ""
        plt.axis("off")
        plt.imshow(
            WordCloud(max_font_size=40).generate(text),
            interpolation='bilinear'
        )

注意

您可以在此处找到代码文件

注意,在前面的代码中,url现在被引用为类变量,因为我们假设基类将提供它。 代码必须测试url是否为None,,这在启动时就是这种情况。 我们以这种方式实现它,因为pd_widget是无法轻易动态生成的属性(您将不得不使用带有pd_widget属性生成 DIV 片段的辅助路由)。

现在,主要的 PixieApp 类如下所示:

@PixieApp
class WordCloudApp(WCChildApp):
    @route()
    def main_screen(self):
        self.url=None
        return """
        <div style="text-align:center">
            <label>Enter a url: </label>
            <input type="text" size="80" id="url{{prefix}}">
            <button type="submit"
                pd_script="self.url = '$val(url{{prefix}})'"
                pd_refresh="wordcloud{{prefix}}">
                Go
            </button>
        </div>
        <center><div pd_widget="wordcloud" id="wordcloud{{prefix}}"></div></center>
        """

app = WordCloudApp()
app.run()

注意

您可以在此处找到代码文件

包含pd_widget属性的 DIV 在开始时就会呈现,但是由于url仍然是None,因此实际上没有生成词云。 Go按钮具有pd_script属性,该属性将self.url设置为用户提供的值。 它还具有pd_widget DIV 的pd_refresh属性集,该属性将再次调用wordcloud小部件,但是这次 URL 初始化为正确的值。

在本节中,我们已经看到了两种用于模块化代码以供重用的方法,以及两种方法的利弊。 我强烈建议您使用代码,以获取何时使用每种技术的感觉。 如果您觉得这仍然有些模糊,请不要担心。 当我们在前面各章的示例代码中使用这些技术时,希望它将变得更加清晰。

在下一节中,我们将进行调整,并查看 PixieDust 中对流数据的支持。

PixieDust 对流数据的支持

随着物联网设备(物联网)的兴起,能够分析和可视化数据实时流变得越来越重要。 例如,您可能在机器或起搏器等便携式医疗设备中安装了诸如温度计之类的传感器,并将数据连续流传输到诸如 Kafka 的流服务中。 通过为 PixieApp 和display()框架提供简单的集成 API,PixieDust 使得在 Jupyter 笔记本内部处理实时数据更加容易。

在可视化级别,PixieDust 使用 Bokeh 支持进行有效的数据源更新,以将流数据绘制到实时图表中(请注意,目前仅支持折线图和散点图,但将来会添加更多)。 display()框架还支持使用 Mapbox 渲染引擎对流数据进行地理空间可视化。

要激活流可视化,您需要使用从StreamingDataAdapter,继承的类,该类是 PixieDust API 的一部分,是一个抽象类。 此类充当流数据源与可视化框架之间的通用桥梁。

注意

注意:我建议您花一些时间在这里查看StreamingDataAdapter的代码

下图显示了StreamingDataAdapter数据结构如何适合display()框架:

PixieDust support of streaming data

StreamingDataAdapter架构

在实现StreamingDataAdapter的子类时,必须覆盖基类提供的doGetNextData()方法,将重复调用该方法以获取新数据以更新可视化效果。 您还可以选择覆盖getMetadata()方法,以将上下文传递给渲染引擎(我们稍后将使用此方法来配置 Mapbox 渲染)。

doGetNextData()的抽象实现如下所示:

@abstractmethod
def doGetNextData(self):
    """Return the next batch of data from the underlying stream.
    Accepted return values are:
    1\. (x,y): tuple of list/numpy arrays representing the x and y axis
    2\. pandas dataframe
    3\. y: list/numpy array representing the y axis. In this case, the x axis is automatically created
    4\. pandas serie: similar to #3
    5\. json
    6\. geojson
    7\. url with supported payload (json/geojson)
    """
    Pass

注意

您可以在此处找到代码文件

前面的文档字符串解释了允许从doGetNextData()返回的数据的不同类型。

例如,我们要在地图上实时显示虚拟无人机在地球上徘徊的位置。 它的当前位置由 REST 服务提供

有效负载使用 GeoJSON,例如:

{
    "geometry": {
        "type": "Point",
        "coordinates": [
            -93.824908715741202, 10.875051131034805
        ]
    },
    "type": "Feature",
    "properties": {}
}

注意

您可以在此处找到代码文件

为了实时渲染我们的无人机位置,我们创建了一个DroneStreamingAdapter类,该类继承自StreamingDataAdapter,并只需在doGetNextData()方法中返回无人机位置服务 URL,如以下代码所示:

from pixiedust.display.streaming import *

class DroneStreamingAdapter(StreamingDataAdapter):
    def getMetadata(self):
        iconImage = "rocket-15"
        return {
            "layout": {"icon-image": iconImage, "icon-size": 1.5},
            "type": "symbol"
        }
    def doGetNextData(self):
        return "https://wanderdrone.appspot.com/"
adapter = DroneStreamingAdapter()
display(adapter)

注意

您可以在此处找到代码文件

getMetadata()方法中,我们使用火箭 Maki 返回 Mapbox 特定的样式属性图标如此处所述)作为无人机的符号。

使用几行代码,我们能够创建无人机位置的实时地理空间可视化,结果如下:

PixieDust support of streaming data

无人机的实时地理空间映射

注意

您可以在以下位置的 PixieDust 存储库中找到此示例的完整笔记本

为您的 PixieApp 添加流功能

在下一个示例中,我们显示如何使用由 PixieDust 提供的MessageHubStreamingApp PixieApp 可视化来自 Apache Kafka 数据源的流数据。

注意

注意MessageHubStreamingApp与名为 Message Hub 的 IBM Cloud Kafka 服务一起使用,但可以轻松地将其应用于任何其他 Kafka 服务。

如果您不熟悉 Apache Kafka,请不要担心,因为我们将在第 7 章“大数据 Twitter 情感分析”中介绍这方面的内容。

通过此 PixieApp,用户可以选择与服务实例相关联的 Kafka 主题,并实时显示事件。 假设来自选定主题的事件有效负载使用 JSON 格式,则它将呈现从对事件数据进行采样中推断出的架构。 然后,用户可以选择一个特定的字段(必须为数字),并显示一个实时图表,显示该字段随时间变化的平均值。

Adding streaming capabilities to your PixieApp

实时可视化流数据

提供流功能所需的关键 PixieApp 属性是pd_refresh_rate,,它以指定的时间间隔执行特定的内核请求(拉模型)。 在前面的应用中,我们使用它来更新实时图表,如showChart路由返回的以下 HTML 片段所示:

    @route(topic="*",streampreview="*",schemaX="*")
    def showChart(self, schemaX):
        self.schemaX = schemaX
        self.avgChannelData = self.streamingData.getStreamingChannel(self.computeAverages)
        return """
<div class="well" style="text-align:center">
    <div style="font-size:x-large">Real-time chart for {{this.schemaX}}(average).</div>
</div>

<div pd_refresh_rate="1000" pd_entity="avgChannelData"></div>
        """

注意

您可以在此处找到代码文件

前一个 DIV 通过pd_entity属性绑定到avgChannelData实体,并负责创建每秒更新一次的实时图表(pd_refresh_rate = 1000 ms)。 依次通过对getStreamingChannel(),的调用来创建avgChannelData实体,该调用将传递给selfcomputeAverage函数负责更新所有流数据的平均值。 重要的是要注意,avgChannelData是从StreamingDataAdapter继承的类,因此可以传递给display()框架以构建实时图表。

最后一个难题是让 PixieApp 返回display()框架所需的displayHandler。 这是通过覆盖newDisplayHandler()方法来完成的,如下所示:

def newDisplayHandler(self, options, entity):
    if self.streamingDisplay is None:
        self.streamingDisplay = LineChartStreamingDisplay(options, entity)
    else:
        self.streamingDisplay.options = options
    return self.streamingDisplay

注意

您可以在此处找到代码文件

在前面的代码中,我们使用它在pixiedust.display.streaming.bokeh中创建由 PixieDust 提供的LineChartStreamingDisplay实例,并传递avgChannelData实体。

如果要查看该应用的运行情况,则需要在 IBM Cloud 上创建一个消息中心服务实例,并使用它的凭据,请使用以下代码在笔记本中调用此 PixieApp:

from pixiedust.apps.messageHub import *
MessageHubStreamingApp().run(
    credentials={
        "username": "XXXX",
        "password": "XXXX",
        "api_key" : "XXXX",
        "prod": True
    }
)

如果您想了解有关 PixieDust 流的更多信息,可以在这里找到其他流应用示例:

通过 PixieApp 事件添加仪表板明细

PixieApp 框架支持使用浏览器中可用的发布-订阅模式在不同组件之间发送和接收事件。 使用此模型的最大好处是,它可以使用宽松的耦合模式,它允许发送和接收组件彼此保持不可知。 因此,它们的实现可以彼此独立地执行,并且对需求的变化不敏感。 当您的 PixieApp 使用由不同团队构建的不同 PixieApps 的组件时,或者事件来自用户与图表进行交互(例如,单击地图)并且您想要提供向下钻取功能时,这将非常有用。 。

每个事件都携带任意键和值的 JSON 有效负载。 有效负载必须至少具有以下其中一个键(或两者兼有):

  • targetDivId:标识发送事件的元素的 DOM ID
  • type:标识事件类型的字符串

发布者可以通过两种方式触发事件:

  • 声明性:使用pd_event_payload属性指定有效内容。 该属性遵循与pd_options相同的规则:

    • 每个键/值对必须使用key=value表示法进行编码

    • 该事件将由点击或更改事件触发

    • 必须提供对$val()指令的支持,以动态注入用户输入的输入

    • 使用<pd_event_payload>子代输入原始 JSON

      <button type="submit" pd_event_payload="type=topicA;message=Button clicked">
          Send event A
      </button>
      
      <button type="submit">
          <pd_event_payload>
          {
              "type":"topicA",
              "message":"Button Clicked"
          }
          </pd_event_payload>
          Send event A
      </button>
      

    例子:

    或者,我们可以使用以下代码:

    注意

    您可以在此处找到代码文件

  • 过程式:在某些情况下,您可能希望通过 JavaScript 直接触发事件。 在这种情况下,您可以使用pixiedust全局对象的sendEvent(payload, divId)方法。 divId是一个可选参数,用于指定事件的来源。 如果省略了·参数,则默认为当前发送事件的元素的 divId。 因此,您应始终使用来自用户事件(例如,点击和悬停)的 JavaScript 处理器的pixiedust.sendEvent,而不使用divId

    例子:

    <table
    onclick="pixiedust.sendEvent({type:'topicB',text:event.srcElement.innerText})">
        <tr><td>Row 1</td></tr>
        <tr><td>Row 2</td></tr>
        <tr><td>Row 3</td></tr>
    </table>
    

    注意

    您可以在此处找到代码文件

订阅者可以通过声明<pd_event_handler>元素来监听事件,该元素可以接受 PixieApp Kernel 执行属性中的任何一个,例如pd_optionspd_script。 它还必须使用pd_source属性来过滤他们要处理的事件。 pd_source属性可以包含以下值之一:

  • targetDivId:仅接受来自具有指定 ID 的元素的事件
  • type:仅接受指定类型的事件
  • "*":表示将接受任何事件

例子:

<div class="col-sm-6" id="listenerA{{prefix}}">
    Listening to button event
    <pd_event_handler
        pd_source="topicA"
        pd_script="print(eventInfo)"
        pd_target="listenerA{{prefix}}">
    </pd_event_handler>
</div>

注意

您可以在此处找到代码文件

下图显示了组件之间如何交互:

Adding dashboard drill-downs with PixieApp events

在组件之间发送/接收事件

在以下代码示例中,我们通过构建两个发布者(一个按钮元素和一个表,其中每行都是一个事件源)来说明 PixieDust 事件系统。 我们还有两个实现为 DIV 元素的监听器:

from pixiedust.display.app import *
@PixieApp
class TestEvents():
    @route()
    def main_screen(self):
        return """
<div>
    <button type="submit">
        <pd_event_payload>
        {
            "type":"topicA",
            "message":"Button Clicked"
        }
        </pd_event_payload>
        Send event A
    </button>
    <table onclick="pixiedust.sendEvent({type:'topicB',text:event.srcElement.innerText})">
        <tr><td>Row 1</td></tr>
        <tr><td>Row 2</td></tr>
        <tr><td>Row 3</td></tr>
    </table>
</div>
<div class="container" style="margin-top:30px">
    <div class="row">
        <div class="col-sm-6" id="listenerA{{prefix}}">
            Listening to button event
            <pd_event_handler pd_source="topicA" pd_script="print(eventInfo)" pd_target="listenerA{{prefix}}">
            </pd_event_handler>
        </div>
        <div class="col-sm-6" id="listenerB{{prefix}}">
            Listening to table event
            <pd_event_handler pd_source="topicB" pd_script="print(eventInfo)" pd_target="listenerB{{prefix}}">
            </pd_event_handler>
        </div>
    </div>
</div>
        """
app = TestEvents()
app.run()

注意

您可以在此处找到代码文件

上面的代码产生以下结果:

Adding dashboard drill-downs with PixieApp events

PixieApp 事件的用户交互流程

PixieApp 事件使您能够创建具有追溯功能的复杂仪表板。 也很高兴知道您可以利用为display()框架生成的某些图表自动发布的事件。 例如,当用户单击图表上的某个位置时,内置渲染器(例如 Google Maps,Mapbox 和 Table)将自动生成事件。 这对于通过钻取功能快速构建各种交互式仪表板非常有用。

在下一个主题中,我们将讨论如何使用 PixieDust 可扩展性 API 创建自定义可视化。

扩展 PixieDust 可视化效果

PixieDust 设计为高度可扩展的。 您可以根据所显示的实体创建自己的可视化并控制何时可以调用它。 PixieDust 框架提供了多个可扩展层。 最低功能最强大的类可让您创建自己的Display类。 但是,大多数可视化都有很多共同的属性,例如标准选项(聚合,最大行数,标题等),或者是一种缓存机制,如果用户仅选择了一个不包含选项的次要选项,则可以防止重新计算所有内容。 需要重新处理数据。

为了防止用户每次都重新发明轮子,PixieDust 提供了第二个可扩展层,称为渲染器,其中包括此处介绍的所有功能。

下图说明了不同的层:

Extending PixieDust visualizations

PixieDust 扩展层

要开始使用显示扩展层,您需要通过创建一个继承自pixiedust.display.DisplayHandlerMeta的类来在菜单中显示可视化效果。 此类包含两个需要覆盖的方法:

  • getMenuInfo(self,entity,dataHandler):如果不支持作为参数传递的实体,则返回一个空数组,否则返回包含带有菜单信息的 JSON 对象集的数组。 每个 JSON 对象必须包含以下信息:

    • id:唯一的字符串,用于标识您的工具。
    • categoryId:标识菜单类别或组的唯一字符串。 稍后会提供所有内置类别的完整列表。
    • title:描述菜单的任意字符串。
    • icon:字体真棒图标的名称,或图像的 URL。
  • newDisplayHandler(self,options,entity):当用户激活菜单时,将调用newDisplayHandler()方法。 此方法必须返回从pixiedust.display.Display继承的类实例。 该类的合同用于实现doRender()方法,该方法负责创建可视化文件。

让我们以为 Pandas DataFrame创建自定义表格渲染为例。 我们首先创建用于配置菜单和工厂方法的DisplayHandlerMeta类:

from pixiedust.display.display import *
import pandas
@PixiedustDisplay()
class SimpleDisplayMeta(DisplayHandlerMeta):
    @addId
    def getMenuInfo(self,entity,dataHandler):
        if type(entity) is pandas.core.frame.DataFrame:
            return [
               {"categoryId": "Table", "title": "Simple Table", "icon": "fa-table", "id": "simpleTest"}
            ]
        return []
    def newDisplayHandler(self,options,entity):
        return SimpleDisplay(options,entity)

注意

您可以在此处找到代码文件

注意,前面的SimpleDisplayMeta类需要用@PixiedustDisplay,装饰,这是将该类添加到插件的内部 PixieDust 注册表中所必需的。 在getMenuInfo()方法中,我们首先检查实体类型是否为 Pandas DataFrame,如果不是,则返回一个空数组,表示此插件不支持当前实体,因此不会对菜单有任何贡献。 如果类型正确,我们将返回一个数组,其中包含一个包含菜单信息的 JSON 对象。

工厂方法newDisplayHandler()作为参数传递了optionsentityoptions参数是键/值对的字典,包含用户做出的各种选择。 正如我们将在后面看到的那样,可视化可以定义反映其功能的任意键/值对,并且 PixieDust 框架将自动将它们保留在单元元数据中。

例如,您可以添加一个选项,用于在 UI 中将 HTTP 链接显示为可点击。 在我们的示例中,我们返回一个SimpleDisplay实例,如下所示:

class SimpleDisplay(Display):
    def doRender(self, handlerId):
        self._addHTMLTemplateString("""
<table class="table table-striped">
   <thead>
       {%for column in entity.columns.tolist()%}
       <th>{{column}}</th>
       {%endfor%}
   </thead>
   <tbody>
       {%for _, row in entity.iterrows()%}
       <tr>
           {%for value in row.tolist()%}
           <td>{{value}}</td>
           {%endfor%}
       </tr>
       {%endfor%}
   </tbody>
</table>
        """)

注意

您可以在此处找到代码文件

如前所述,SimpleDisplay类必须继承Display类并实现doRender()方法。 在此方法的实现中,您可以访问self.entityself.options变量来调整信息在屏幕上的呈现方式。 在前面的示例中,我们使用self._addHTMLTemplateString()方法创建将呈现可视化效果的 HTML 片段。 与 PixieApp 路由一样,传递给self._addHTMLTemplateString()的字符串可以利用 Jinja2 模板引擎并可以自动访问诸如entity之类的变量。 如果您不想在 Python 文件中对模板字符串进行硬编码,则可以将提取到其自己的文件中,该文件必须放置在名为templates的目录中,该目录必须与调用的 Python 文件位于同一目录中。 然后,您将需要使用self._addHTMLTemplate()方法,该方法将文件名作为参数(不指定templates目录)。

注意

将 HTML 片段外部化为它自己的文件的另一个优点是,您不必每次进行更改都重新启动内核,这可以节省大量时间。 由于 Python 的工作方式,如果在源代码中嵌入 HTML 片段,就不能说相同的话,在这种情况下,您必须重新启动内核才能对 HTML 片段进行任何更改。

同样重要的是要注意self._addHTMLTemplate()self._addHTMLTemplateString()接受将被传递到 Jinja2 模板的关键字参数。 例如:

self._addHTMLTemplate('simpleTable.html', custom_arg = "Some value")

现在,我们可以运行一个显示cars数据集的单元格:

注意

注意简单表扩展名仅适用于 Pandas,不适用于 Spark DataFrame。 因此,如果您的笔记本计算机连接到 Spark,则在调用sampleData()时需要使用forcePandas = True

Extending PixieDust visualizations

在 Pandas DataFrame上运行自定义可视化插件

如 PixieDust 扩展层架构图所示,您还可以使用渲染器扩展层扩展 PixieDust,它比显示扩展层更具规范性,但提供了更多功能。 框,例如选项管理和临时数据计算缓存。 从用户界面的角度来看,用户可以使用图表区域右上角的渲染器下拉菜单在渲染器之间进行切换。

PixieDust 随附了一些内置渲染器,例如 Matplotlib,Seaborn,Bokeh,Mapbox,Brunel 和 Google Maps,但它并未声明对基础可视化库的任何硬性依赖,包括 Bokeh,Brunel 或 Seaborn。 因此,用户必须手动安装它们,否则它们将不会显示在菜单中。

以下屏幕快照说明了在给定图表的渲染器之间切换的机制:

Extending PixieDust visualizations

在渲染器之间切换

添加新的渲染器类似于添加显示可视化(使用相同的 API),尽管实际上更简单,因为您只需要构建一个类(无需构建元数据类)即可。 这是您需要遵循的步骤:

  1. 创建一个从专门的BaseChartDisplay class继承的展示类。 实现所需的doRenderChart()方法。

  2. 使用@PixiedustRenderer装饰器注册rendererId(在所有渲染器中必须唯一)和要渲染的图表类型。

    请注意,相同的rendererId可以用于渲染器中包含的所有图表。 PixieDust 提供了一组核心图表类型:

    • tableView
    • barChart
    • lineChart
    • scatterPlot
    • pieChart
    • mapView
    • histogram
  3. (可选)使用@commonChartOptions装饰器创建一组动态选项。

  4. (可选)通过覆盖get_options_dialog_pixieapp()方法来自定义从pixiedust.display.chart.options.baseOptions包中的BaseOptions类继承的 PixieApp 类的完全限定名称,从而自定义选项对话框。

例如,让我们使用渲染器扩展层重写先前的自定义SimpleDisplay表可视化:

from pixiedust.display.chart.renderers import PixiedustRenderer
from pixiedust.display.chart.renderers.baseChartDisplay import BaseChartDisplay

@PixiedustRenderer(rendererId="simpletable", id="tableView")
class SimpleDisplayWithRenderer(BaseChartDisplay):
    def get_options_dialog_pixieapp(self):
        return None #No options needed

    def doRenderChart(self):
        return self.renderTemplateString("""
<table class="table table-striped">
   <thead>
       {%for column in entity.columns.tolist()%}
       <th>{{column}}</th>
       {%endfor%}
   </thead>
   <tbody>
       {%for _, row in entity.iterrows()%}
       <tr>
           {%for value in row.tolist()%}
           <td>{{value}}</td>
           {%endfor%}
       </tr>
       {%endfor%}
   </tbody>
</table>
        """)

注意

您可以在此处找到代码文件

我们用@PixiedustRenderer装饰器装饰该类,指定一个称为simpletable,的唯一rendererId并将其与 PixieDust 框架定义的tableView图表类型相关联。 对于get_options_dialog_pixieapp()方法,我们返回None,以表示此扩展名不支持自定义选项。 结果是,将不会显示选项按钮。 在doRenderChart()方法中,我们返回 HTML 片段。 由于我们要使用 Jinja2,因此需要使用self.renderTemplateString方法进行渲染。

现在,我们可以使用cars数据集测试此新渲染器。

注意

同样,在运行代码时,请确保将cars数据集作为 Pandas DataFrame加载。 如果您已经运行了简单表的第一个实现,并且正在重新使用笔记本电脑,则可能仍然会看到旧的简单表菜单。 如果是这种情况,您将需要重新启动内核并重试。

以下屏幕快照显示了作为呈现器的简单表格可视化:

Extending PixieDust visualizations

测试简单表的渲染器实现

您可以在以下位置找到有关此主题的更多材料。 希望到目前为止,您对定制的类型有个好主意,可以编写将定制的可视化集成到display()框架中。

在下一节中,我们将为开发人员讨论一个非常重要的主题:调试。

调试

能够快速调试应用对于项目的成功至关重要。 如果不是这样,我们将失去通过打破数据科学与工程学之间的孤岛而在生产力和协作方面所获得的大部分(如果不是全部)收益。 还需要注意的是,我们的代码在不同的位置运行,即在服务器端运行 Python,在客户端运行 JavaScript,并且调试必须在这两个位置进行。 对于 Python 代码,让我们看一下解决编程错误的两种方法。

使用 pdb 在 Jupyter 笔记本上调试

PDB 是一个交互式命令行 Python 调试器,是每个 Python 发行版的标准配置。

有多种调用调试器的方法:

  • 在启动时,从命令行:

    python -m pdb <script_file>
    
    
  • 以编程方式,在代码中:

    import pdb
    pdb.run("<insert a valid python statement here>")
    
  • 通过使用set_trace()方法在代码中设置一个明确的断点:

    import pdb
    def my_function(arg1, arg2):
        pdb.set_trace()
        do_something_here()
    

    注意

    您可以在此处找到代码文件

  • 发生异常后,通过调用pdb.pm()进行验尸。

进入交互式调试器后,您可以调用命令,检查变量,运行语句,设置断点等。

注意

命令的完整列表可以在这里找到

好消息是 Jupyter 笔记本为交互式调试器提供了一流的支持。 要调用调试器,只需使用%pdb cell 魔术命令打开/关闭它,如果触发了异常,则调试器将自动在有问题的行停止执行。

魔术命令是特定于 IPython 内核的结构。 它们是与语言无关的,因此理论上可以在内核支持的任何语言中使用(例如 Python,Scala 和 R)。

魔术命令有两种类型:

  • 行魔术:语法为%<magic_command_name> [optional arguments],例如%matplotlib inline,它将 Matplotlib 配置为在笔记本输出单元格中内联输出图表。

    它们可以在单元代码中的任何位置调用,甚至可以返回可以分配给 Python 变量的值,例如:

    #call the pwd line magic to get the current working directory
    #and assign the result into a Python variable called pwd
    pwd = %pwd
    print(pwd)
    

    注意

    您可以在此处找到所有线魔术的列表

  • 单元魔术:语法为%%<magic_command_name> [optional arguments]。 例如,我们称 HTML 单元魔术为在输出单元上显示 HTML:

    %%html
    <div>Hello World</div>
    

    单元魔术必须位于单元顶部; 任何其他位置都将导致执行错误。 单元魔术下方的所有内容均作为参数传递给处理器,以根据单元魔术规范进行解释。 例如,HTML 单元魔术师希望单元格内容的其余部分为 HTML。

下面的代码示例调用一个引发ZeroDivisionError异常的函数,并激活了pdb自动调用:

注意

注意:一旦打开pdb,它在笔记本计算机会话期间将保持打开状态。

Debugging on the Jupyter Notebook using pdb

交互式命令行调试

以下是一些重要的pdb命令,可用于解决问题:

  • s(tep):进入正在调用的函数,然后在下一条语句行停止。
  • n(ext):继续的下一行,而不进入嵌套功能。
  • l(list):当前行周围的列表代码。
  • c(ontinue):继续运行该程序,并在下一个断点处停止,或者如果引发另一个异常。
  • d(own):向下移动栈帧。
  • u(p):向上移动栈帧。
  • <any expression>:在当前帧的上下文中求值并显示一个表达式。 例如,您可以使用locals()获取范围为当前帧的所有局部变量的列表。

如果发生异常并且您未设置自动pdb调用,则仍然可以在另一个单元格中使用%debug魔术来在事件发生后调用调试器,如以下屏幕截图所示:

Debugging on the Jupyter Notebook using pdb

使用%debug进行事后调试会话

与常规 Python 脚本类似,您也可以通过pdb.set_trace()方法以编程方式显式设置断点。 但是,建议使用由 IPython 核心模块提供的set_trace()增强版本,该版本提供语法着色:

Debugging on the Jupyter Notebook using pdb

显式断点

在下一个主题中,我们看一下 PixieDust 提供的 Python 调试器的增强版本。

将 PixieDebugger 用于视觉调试

使用标准的基于命令行的 Python pdb 调试代码是我们工具带中的一个不错的工具,但是它有两个主要限制:

  • 它是面向命令行的,这意味着必须手动输入命令并将结果顺序附加到单元格输出中,因此在进行高级调试时不切实际
  • 它不适用于 PixieApps

PixieDebugger 功能解决了这两个问题。 您可以将其与在 Jupyter 笔记本单元中运行的任何 Python 代码一起使用,以直观地调试代码。 要在单元格中调用 PixieDebugger ,只需在单元格顶部添加%%pixie_debugger单元魔术即可。

注意

注意:如果尚未这样做,请不要忘记在尝试使用%%pixie_debugger之前始终将pixiedust导入单独的单元格中。

例如,以下代码试图计算cars数据集中名称为chevrolet的汽车数量:

%%pixie_debugger
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)

def count_cars(name):
    count = 0
    for row in cars.itertuples():
        if name in row.name:
            count += 1
    return count

count_cars('chevrolet')

注意

您可以在此处找到代码文件

使用前面的代码运行单元将触发以下屏幕快照中所示的可视调试器。 用户界面允许您逐行进入代码,并具有检查局部变量,评估 Python 表达式和设置断点的能力。 代码执行工具栏提供了用于管理代码执行的按钮:恢复执行,单步执行当前行,单步执行特定功能的代码,运行至当前功能的末尾以及向上和向下显示栈帧 :

Visual debugging with PixieDebugger

运行中的 PixieDebugger

没有参数,pixie_debugger单元魔术将在代码中的第一个可执行语句处停止。 但是,您可以使用-b开关轻松地将其配置为在特定位置停止,然后是一个断点列表,该断点可以是行号或方法名。

从前面的示例代码开始,让我们在count_cars()方法和第11行添加断点:

%%pixie_debugger -b count_cars 11
import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)

def count_cars(name):
    count = 0
    for row in cars.itertuples():
        if name in row.name:
            count += 1
    return count

count_cars('chevrolet')

注意

您可以在此处找到代码文件

现在,运行前面的代码将触发 PixieDebugger 在count_cars()方法的第一个可执行语句处停止。 它还在第 11 行添加了一个断点,如果用户继续执行,将导致执行流在此处停止,如以下屏幕截图所示:

Visual debugging with PixieDebugger

具有预定义断点的 PixieDebugger

注意

注意:要运行到特定的代码行而不设置明确的断点,只需将鼠标悬停在左侧窗格的装订线中的行号上,然后单击出现的图标即可。

%debug魔法一样,您也可以使用%pixie_debugger魔法来调用 PixieDebugger 进行事后调试。

使用 PixieDebugger 调试 PixieApp 路由

PixieDebugger 已完全集成到 PixieApp 框架中。 每当触发路由时发生异常时,都会通过两个额外的按钮来增强产生的回溯:

  • 发布 Mortem:调用 PixieDebugger 启动事后故障排除会话,该会话可让您检查变量并分析栈帧
  • 调试路由:重播当前路由,停止在 PixieDebugger 中的第一个可执行语句处

例如,让我们考虑以下代码来实现 PixieApp,该代码使用户通过提供列名和搜索查询来搜索cars数据集:

from pixiedust.display.app import *

import pixiedust
cars = pixiedust.sampleData(1, forcePandas=True)

@PixieApp
class DisplayCars():
    @route()
    def main_screen(self):
        return """
        <div>
            <label>Column to search</label>
            <input id="column{{prefix}}" value="name">
            <label>Query</label>
            <input id="search{{prefix}}">
            <button type="submit" pd_options="col=$val(column{{prefix}});query=$val(search{{prefix}})"
                pd_target="target{{prefix}}">
                Search
            </button>
        </div>
        <div id="target{{prefix}}"></div>
        """
    @route(col="*", query="*")
    def display_screen(self, col, query):
        self.pdf = cars.loc[cars[col].str.contains(query)]
        return """
        <div pd_render_onload pd_entity="pdf">
            <pd_options>
            {
              "handlerId": "tableView",
              "table_noschema": "true",
              "table_nosearch": "true",
              "table_nocount": "true"
            }
            </pd_options>
        </div>
        """
app = DisplayCars()
app.run()

注意

您可以在此处找到代码文件

搜索列的缺省值为name,但是如果用户输入的列名不存在,则会按以下方式生成的回溯:

Debugging PixieApp routes with PixieDebugger

带有按钮的增强的追溯功能,用于调用 PixieDebugger

单击调试路由将自动启动 PixieDebugger 并在该路由的第一个可执行语句处停止,如以下屏幕截图所示:

Debugging PixieApp routes with PixieDebugger

调试 PixieApp 路由

通过使用run方法的debug_route关键字参数,您也可以故意让 PixieDebugger 停止在display_screen()路由上,而无需等待发生回溯:

...
app = DisplayCars()
app.run(debug_route="display_screen")

PixieDebugger 是 Jupyter 笔记本的第一个可视化 Python 调试器,提供了 Jupyter 用户社区长期以来要求的功能。 但是,使用实时调试并不是开发人员使用的唯一工具。 在下一节中,我们将通过检查日志记录消息来进行调试。

使用 PixieDust 日志记录对问题故障排除

始终将实践记录为带有日志消息的代码,PixieDust 框架提供了一种简单的方法来直接从 Jupyter 笔记本创建和读取日志消息。 首先,您需要通过调用getLogger()方法来创建一个记录器,如下所示:

import pixiedust
my_logger = pixiedust.getLogger(__name__)

注意

您可以在此处找到代码文件

您可以使用任何东西作为getLogger()方法的参数。 但是,为了更好地标识特定消息的来源,建议使用__name__变量,该变量返回当前模块的名称。 my_logger变量是一个标准的 Python 记录器对象,它提供各种级别的记录方法:

  • debug(msg, *args, **kwargs):以DEBUG级别记录消息。
  • info(msg, *args, **kwargs):以INFO级别记录消息。
  • warning(msg, *args, **kwargs):以WARNING级别记录消息。
  • error(msg, *args, **kwargs):以ERROR级别记录消息。
  • critical(msg, *args, **kwargs):以CRITICAL级别记录消息。
  • exception(msg, *args, **kwargs):以EXCEPTION级别记录消息。 此方法只能在异常处理器中调用。

注意

注意:您可以在此处找到有关 Python 日志记录框架的更多信息

然后,您可以使用%pixiedustLog单元魔术直接从 Jupyter 笔记本中查询日志消息,该魔术具有以下参数:

  • -l:按日志级别过滤,例如CRITICALFATALERRORWARNINGINFODEBUG
  • -f:过滤包含给定字符串的消息,例如Exception
  • -m:返回的日志消息的最大数量

在下面的示例中,我们使用%pixiedustLog魔术来显示所有调试消息,将它们限制为最后五个消息:

Troubleshooting issues using PixieDust logging

显示最后五条日志消息

为了方便起见,在使用 Python 类时,您还可以使用@Logger装饰器,该装饰器使用类名称作为其标识符自动创建记录器。

这是一个使用@Logger装饰器的代码示例:

from pixiedust.display.app import *
from pixiedust.utils import Logger

@PixieApp
@Logger()
class AppWithLogger():
    @route()
    def main_screen(self):
        self.info("Calling default route")
        return "<div>hello world</div>"

app = AppWithLogger()
app.run()

注意

您可以在此处找到代码文件

在单元格中运行前面的 PixieApp 之后,可以调用%pixiedustLog魔术来显示消息:

Troubleshooting issues using PixieDust logging

查询带有特定项目的日志

这样就完成了我们关于服务器端调试的讨论。 在下一节中,我们将介绍执行客户端调试的技术

客户端调试

PixieApp 编程模型的设计原则之一是最大程度地减少开发人员编写 JavaScript 的需求。 该框架将通过监听用户输入事件(例如单击或更改事件)来自动触发内核请求。 但是,在某些情况下,不可避免地需要编写一些 JavaScript。 这些 JavaScript 代码段通常是特定路由 HTML 片段的一部分,并动态注入到浏览器中,这使得调试非常困难。

一种流行的技术是在 JavaScript 代码中添加console.log调用,以便将消息打印到浏览器开发者控制台。

注意

注意:每种浏览器版本都有其自己的调用开发者控制台的方式。 例如,在 Google Chrome 浏览器中,您将使用视图 | 开发人员 | JavaScript 控制台,或Cmd + Alt + J快捷键。

我特别喜欢的另一种调试技术是使用debugger;语句以编程方式插入 JavaScript 代码的断点中。 除非打开浏览器开发人员工具并启用源调试,否则该语句无效;在这种情况下,执行将在debugger;语句处自动中断。

以下 PixieApp 示例使用 JavaScript 函数来解析$val()指令引用的动态值:

from pixiedust.display.app import *

@PixieApp
class TestJSDebugger():
    @route()
    def main_screen(self):
        return """
<script>
function FooJS(){
    debugger;
    return "value"
}
</script>
<button type="submit" pd_options="state=$val(FooJS)">Call route</button>
        """

    @route(state="*")
    def my_route(self, state):
        return "<div>Route called with state <b>{{state}}</b></div>"

app = TestJSDebugger()
app.run()

注意

您可以在此处找到代码文件

在前面的代码中,按钮使用包含调试器语句的FooJS JavaScript 函数动态设置状态的值。 在开发者工具打开的情况下执行该应用并单击按钮将自动在浏览器上启动调试会话:

Client-side debugging

使用调试器在客户端调试 JavaScript 代码

在 Python 笔记本中运行 Node.js

即使我在本书的开头就明确指出 Python 已成为数据科学领域的明确领导者,但它仍然仅被传统语言(例如 Node.js)的开发者社区少量使用。 ,仍然是首选。 认识到,对于某些开发人员而言,学习诸如 Python 之类的新语言是进入数据科学的代价,这可能太高了,我与 IBM 同事 Glynn Bird 合作,为 PixieDust 建立了名为pixiedust_node的扩展库,可以使开发人员在 Python 笔记本中的单元内运行 Node.js/JavaScript 代码。 该库的目的是通过允许开发人员重用自己喜欢的 Node.js 库(例如,从现有数据源加载和处理数据)来使他们轻松进入 Python 世界。

要安装pixiedust_node库,只需在其自己的单元格中运行以下命令:

!pip install pixiedust_node

注意

注意:安装完成后,不要忘记重新启动内核。

重要:您需要确保在 Jupyter 笔记本服务器所在的同一台计算机上安装了 Node.js 运行时版本 6 或更高版本。

内核重新​​启动后,我们将导入pixiedust_node模块:

import pixiedust_node

您应该在输出中看到有关 PixieDust 和pixiedust_node的信息,如下所示:

Run Node.js inside a Python Notebook

pixiedust_node欢迎输出

导入pixiedust_node时,会从 Python 端创建一个 Node 子进程,以及一个特殊线程,该线程读取该子进程的输出并将其传递给 Python 端,以在当前在其中执行的单元格中显示。 笔记本。 此子过程负责启动 REPL 会话(*读取-求值-打印-循环**),它将执行从笔记本发送的所有脚本,并使所有创建的类,函数和变量在所有执行中都可重用。

它还定义了一组旨在与笔记本和 PixieDust display() API 交互的功能:

  • print(data):输出当前在笔记本计算机中执行的单元格中的数据值。
  • display(data):使用从数据转换成的 Pandas DataFrame调用 PixieDust display() API。 如果数据无法转换为 Pandas DataFrame,则默认为print方法。
  • html(data):在笔记本计算机中当前正在执行的单元格中以 HTML 格式显示数据。
  • image(data):期望数据是图像的 URL,并将其显示在当前在笔记本计算机中执行的单元格中。
  • help():显示所有前述方法的列表。

此外,pixiedust_node使笔记本电脑中全局可用的两个变量分别称为npmnode,

  • node.cancel():停止 Node.js 子进程中的当前代码执行。
  • node.clear():重置 Node.js 会话; 所有现有变量将被删除。
  • npm.install(package):安装一个 NPM 包并使其可用于 Node.js 会话。 该包在各个会话之间均保持不变。
  • npm.uninstall(package):从系统和当前 Node.js 会话中删除 NPM 包。
  • npm.list():列出当前安装的所有 NPM 包。

pixiedust_node创建一个单元魔术,使您可以运行任意 JavaScript 代码。 只需在单元顶部上使用%%node魔术,然后照常运行即可。 然后将在 Node.js 子进程 REPL 会话中执行代码。

以下代码使用 JavaScript Date对象显示包含当前日期时间的字符串:

%%node
var date = new Date()
print("Today's date is " + date)

输出以下内容:

"Today's date is Sun May 27 2018 20:36:35 GMT-0400 (EDT)"

下图说明了先前单元的执行流程:

Run Node.js inside a Python Notebook

Node.js 脚本执行的生命周期

JavaScript 代码由pixiedust_node魔术处理,并发送到 Node 子进程以执行。 在执行代码时,其输出将由特殊线程读取并显示回当前在笔记本计算机中执行的单元格中。 请注意,JavaScript 代码可能会进行异步调用,在这种情况下,执行将在异步调用完成之前立即返回。 在这种情况下,即使以后可能会通过异步代码生成更多的输出,笔记本也会指示单元代码已完成。 没有方法来确定性地知道异步代码何时完成。 因此,开发人员有责任仔细管理此状态。

pixiedust_node还具有在 Python 端和 JavaScript 端之间共享变量的能力,反之亦然。 因此,您可以声明一个 Python 变量(例如整数数组),在 JavaScript 中应用转换(也许使用您喜欢的库),然后在 Python 中进行处理。

以下代码在两个单元格中运行,一个在纯 Python 中声明一个整数数组,一个在 JavaScript 中将每个元素乘以 2:

Run Node.js inside a Python Notebook

反向也相同。 以下代码首先在节点单元格中的 JavaScript 中创建一个 JSON 变量,然后在 Python 单元格中创建并显示 Pandas DataFrame

%%node
data = {
    "name": ["Bob","Alice","Joan","Christian"],
    "age": [20, 25, 19, 45]
}
print(data)

结果如下:

{"age": [20, 25, 19, 45], "name": ["Bob", "Alice", "Joan", "Christian"]}

然后,在 Python 单元中,我们使用 PixieDust display()

df = pandas.DataFrame(data)
display(df)

使用以下选项:

Run Node.js inside a Python Notebook

从节点单元创建的数据的display()选项

我们得到以下结果:

Run Node.js inside a Python Notebook

节点单元中创建的数据的条形图

通过使用pixiedust_node使可用的display()方法,我们也可以直接从 Node 单元获得相同的结果,如以下代码所示:

%%node
data = {
    "name": ["Bob","Alice","Joan","Christian"],
    "age": [20, 25, 19, 45]
}
display(data)

如果您对了解更多有关pixiedust_node感兴趣,我强烈推荐此博客文章。 与往常一样,我鼓励读者通过贡献代码或增强想法来参与改进这些工具。

总结

在本章中,我们探讨了各种高级概念,工具和最佳实践,这些新概念,工具和最佳做法为我们的工具箱添加了更多工具,范围包括用于 PixieApps 的高级技术(流式处理,如何通过将第三方库与@captureOutput集成,实现路由, PixieApp 事件,以及使用pd_app更好的模块性,以实现诸如 PixieDebugger 之类的基本开发人员工具。 我们还介绍了如何使用 PixieDust display() API 创建自己的自定义可视化效果的详细信息。 我们还讨论了pixiedust_node,,它是 PixieDust 框架的扩展,使对 JavaScript 更熟悉的开发人员可以使用自己喜欢的语言处理数据。

在本书的其余部分中,我们将通过构建行业用例数据管道来充分利用所有这些经验教训,首先从第 6 章中的“深度学习视觉识别应用”开始,“使用 TensorFlow 进行图像识别”。

本书结尾的附录,“PixieApp 快速参考”中提供了 PixieApp 编程模型的开发人员快速参考指南。

六、TensorFlow 图像识别

“人工智能,深度学习,机器学习————如果您不了解它,无论您在做什么————都要学习。否则,您将在 3 年内成为恐龙。”

————马克·库班

这是涵盖流行行业用例的一系列示例应用的第一章,并且我从与机器学习有关的用例开始,也不是巧合,尤其是通过图像识别示例应用进行深度学习。 在过去的几年中,我们看到在人工智能AI)领域加速发展,以至于许多实际应用正在成为现实,例如,自动驾驶汽车以及具有先进的自动语音识别功能的聊天机器人,在某些任务上完全可以代替人工操作,而从学术界到工业界的越来越多的人开始涉足其中。 但是,人们认为入门成本非常高,并且掌握机器学习的基本数学概念是先决条件。 在本章中,我们试图通过使用示例来证明事实并非如此。

我们将在本章开始时对机器学习进行快速介绍,并将其子集称为深度学习。 然后,我们将介绍一个非常流行的深度学习框架,称为 TensorFlow,我们将使用它来构建图像识别模型。 在本章的第二部分中,我们将展示如何通过实现示例 PixieApp 来操作已构建的模型,该示例使用户可以输入网站链接,抓取所有图像并将其用作对它们进行分类的模型的输入。

在本章的最后,您应该确信无需博士学位即可构建有意义的应用并使它们可操作。 在机器学习中。

什么是机器学习?

我认为一个很好的定义可以很好地抓住机器学习背后的直觉,这是斯坦福大学兼职教授 Andrew Ng 在 Coursera 的“机器学习”课程中

机器学习是一门无需明确编程即可让计算机学习的科学。

前面定义中的关键词是学习,在这种情况下,其含义与人类学习方式非常相似。 为了继续进行类似的工作,从很小的时候开始,我们就被教会了如何以身作则或通过反复试验来完成一项任务。 广义上讲,机器学习算法可以分为两种类型,分别对应于人类学习的两种方式:

  • 监督:算法从已正确标记的示例数据中学习。 该数据也称为训练数据,或有时称为基本事实
  • 无监督:算法能够自行从尚未标记的数据中学习。

对于此处描述的两种类别,下表概述了最常用的机器学习算法及其解决的问题类型:

What is machine learning?

机器学习算法列表

这些算法的输出称为模型,用于对以前从未见过的新输入数据进行预测。 构建和部署这些模型的整个端到端过程在不同类型的算法中非常一致。

下图显示了此过程的高级工作流程:

What is machine learning?

机器学习模型工作流程

与往常一样,工作流从数据开始。 在监督学习的情况下,数据将用作示例,因此必须正确标记正确答案。 然后处理输入数据以提取称为特征的内在属性,我们可以将其视为代表输入数据的数值。 随后,将这些特征输入构建模型的机器学习算法中。 在典型设置中,原始数据分为训练数据,测试数据和盲数据。 在模型构建阶段,将使用测试数据和盲数据来验证和优化模型,以确保其不会过拟合训练数据。 当模型参数过于紧随训练数据时会发生过拟合,从而在使用看不见的数据时导致错误。 当模型产生所需的精度水平时,然后将其部署到生产中,并根据主机应用的需要将其用于新数据。

在本节中,我们将通过简化的数据流水线工作流程对机器学习进行非常高级的介绍,仅足以直观地说明如何构建和部署模型。 再一次,如果您是初学者,我强烈推荐在 Coursera 上使用 Andrew Ng 的《机器学习》课程(我仍然会不时地复习)。 在下一节中,我们将介绍称为深度学习的机器学习分支,我们将使用它来构建图像识别样本应用。

什么是深度学习?

使计算机学习,推理和思考(做出决定)是一门被普遍称为认知计算的科学,其中机器学习和深度学习是其中的重要部分。 以下维恩图显示了这些字段与 AI 的总体字段的关系:

What is deep learning?

深度学习如何适应 AI

如图所示,深度学习是机器学习算法的一种。 也许尚未广为人知的是,深度学习领域已经存在了很长一段时间,但是直到最近才真正被广泛使用。 兴趣的重新点燃归因于过去几年中观察到的计算机,云和存储技术的非凡进步,随着许多新的深度学习算法的开发推动了 AI 的指数增长,每种算法都最适合解决特定问题。

正如我们将在本章稍后讨论一样,深度学习算法特别擅长学习复杂的非线性假设。 他们的设计实际上受到人脑工作方式的启发,例如,输入数据流经多层计算单元,以便将复杂的模型表示形式(例如图像)分解为更简单的模型表示形式,然后再将结果返回到下一层,依此类推,以此类推,直到到达负责输出结果的最后一层。 这些层的组装也称为神经网络,而组成一层的计算单元称为神经元。 本质上,神经元负责获取多个输入并将其转换为单个输出,然后可以将其输入到下一层的其他神经元中。

下图表示用于图像分类的多层神经网络:

What is deep learning?

用于图像分类的神经网络的高级表示

前面的神经网络也称为前馈,因为每个计算单元的输出都用作从输入层开始的下一层的输入。 中间层称为隐藏层,其中包含网络自动学习的中间功能。 在我们的图像示例中,某些神经元可能负责检测角,而其他可能关注边缘。 最终输出层负责为每个输出类分配一个置信度(得分)。

一个重要的问题是神经元的输出如何从其输入生成? 在不深入研究所涉及的数学的情况下,每个人工神经元对其输入的加权总和应用激活函数g(x)以决定是否应触发

以下公式计算加权和:

What is deep learning?

其中θ^iii + 1层之间的权重矩阵。 这些权重是在训练阶段计算出来的,稍后我们将简要讨论。

注意

注意:上式中的偏差表示偏差神经元的权重,它是添加到 x 值为 +1 的每一层的额外神经元。 偏向神经元是特殊的,因为它有助于下一层的输入,但它没有连接到上一层。 但是,通常仍然像其他神经元一样学习其权重。 偏向神经元背后的直觉是,它在线性回归方程中提供了常数项 b:

What is deep learning?

当然,在A上应用神经元激活函数g(x)不能简单地产生二进制(0 或 1)值,因为如果存在多个类的得分为 1,我们将无法正确地排列最终候选答案。相反,我们使用激活函数,该函数提供介于 0 和 1 之间的非离散得分,并设置阈值(例如 0.5)来决定是否激活神经元。

Sigmoid 函数是最受欢迎的激活函数之一:

What is deep learning?

下图显示了如何使用 Sigmoid 激活函数根据其输入及其权重来计算神经元输出:

What is deep learning?

使用 Sigmoid 函数计算神经元输出

其他流行的激活函数包括双曲正切tanh(x)整流线性单元ReLu):max(x, 0)。 ReLu 在有很多层时效果更好,因为可以激发神经元的稀疏性,从而降低噪音并加快学习速度。

在模型评分期间使用前馈传播,但是在训练神经网络的权重矩阵时,使用的一种流行方法称为反向传播

以下高级步骤描述了训练的工作方式:

  1. 随机初始化权重矩阵(最好使用较小的值,例如-ε, ε)。
  2. 使用所有训练示例上所述的前向传播,使用您选择的激活函数来计算每个神经元的输出。
  3. 为您的神经网络实现成本函数。 成本函数量化了有关训练示例的误差。 反向传播算法可以使用多种成本函数,例如均方误差交叉熵
  4. 使用反向传播可以最小化成本函数并计算权重矩阵。 反向传播背后的想法是从输出层的激活值开始,计算与训练数据有关的误差,然后将其误差传回隐藏层。 然后调整这些误差以最小化步骤 3 中实现的成本函数。

注意

:详细解释这些成本函数以及如何对其进行优化超出了本书的范围。 对于更深层次的探讨,我强烈建议您阅读 MIT 出版社(Ian Goodfellow,Yoshua Bengio 和 Aaron Courville)的《深度学习》书。

在本节中,我们已高层讨论了神经网络如何工作以及如何进行训练。 当然,我们只是触及了这项令人兴奋的技术的表面,但是希望您应该对它们的工作原理有所了解。 在下一部分中,我们将开始研究 TensorFlow,这是一个编程框架,可帮助抽象实现神经网络的底层复杂性。

TensorFlow 入门

除了 TensorFlow 之外,我还可以为该示例应用选择多个开源深度学习框架。

最受欢迎的一些框架如下:

TensorFlow API 有多种语言可用:Python,C++,Java,Go,以及最近的 JavaScript。 我们可以区分以下两类 API:高级别和低级别,如下图所示:

Getting started with TensorFlow

TensorFlow 高级 API 架构

为了使 TensorFlow API 成为,让我们构建一个简单的神经网络来学习 XOR 转换。

提醒一下,XOR 运算符只有四个训练示例:

X Y 结果
0 0 0
0 1 1
1 0 1
1 1 0

有趣的是,线性分类器无法学习 XOR 转换。 但是,我们可以通过一个简单的神经网络来解决此问题,该神经网络的输入层中包含两个神经元,一个隐藏层中包含两个神经元,而输出层中包含一个神经元(二分类),如下所示:

Getting started with TensorFlow

XOR 神经网络

注意

注意:您可以使用以下命令直接从笔记本计算机安装 TensorFlow:

!pip install tensorflow

与往常一样,在成功安装后不要忘记重新启动内核。

要创建输入和输出层张量,我们使用tf.placeholder API,如以下代码所示:

import tensorflow as tf
x_input = tf.placeholder(tf.float32)
y_output = tf.placeholder(tf.float32)

然后,我们使用tf.Variable API 初始化矩阵θ[1]的随机值,而θ[2]对应于隐藏层和输出层:

eps = 0.01
W1 = tf.Variable(tf.random_uniform([2,2], -eps, eps))
W2 = tf.Variable(tf.random_uniform([2,1], -eps, eps))

对于激活函数,我们使用 Sigmoid 函数:

注意

注意:为简单起见,我们忽略引入偏差。

layer1 = tf.sigmoid(tf.matmul(x_input, W1))
output_layer = tf.sigmoid(tf.matmul(layer1, W2))

对于成本函数,我们使用 MSE均方误差的缩写):

cost = tf.reduce_mean(tf.square(y_output - output_layer))

将所有张量放置在图中后,我们现在可以通过使用0.05的学习率使用tf.train.GradientDescentOptimizer来进行训练,以最小化我们的成本函数:

train = tf.train.GradientDescentOptimizer(0.05).minimize(cost)
training_data = ([[0,0],[0,1],[1,0],[1,1]], [[0],[1],[1],[0]])
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(5000):
        sess.run(train,
            feed_dict={x_input: training_data[0], y_output: training_data[1]})

注意

您可以在此处找到代码文件

前面的代码首次引入了 TensorFlow Session的概念,这是框架的基础部分。 本质上,任何 TensorFlow 操作都必须使用run方法在Session的上下文中执行。 会话还维护需要使用close方法显式释放的资源。 为了方便起见,Session类通过提供__enter____exit__方法来支持上下文管理协议。 这允许调用者使用with语句调用 TensorFlow 操作,并且自动释放资源。

以下伪代码显示了 TensorFlow 执行的典型结构:

with tf.Session() as sess:
    with-block statement with TensorFlow operations

在本部分中,我们快速探索了低级 TensorFlow API,以构建学习 XOR 转换的简单神经网络。 在下一节中,我们将探讨在低层 API 之上提供抽象层的高层估计器 API。

使用DNNClassifier的简单分类

注意

注意:本节讨论示例 PixieApp 的源代码。 如果您想继续学习,可以在以下位置下载完整的笔记本电脑

在使用低级 TensorFlow API 使用张量,图和会话之前,最好先熟悉Estimators包中提供的高级 API。 在本节中,我们将构建一个简单的 PixieApp,它将 Pandas DataFrame作为输入并使用分类输出训练分类模型。

注意

注意:分类输出本质上有两种类型:分类输出和连续输出。 在分类器模型中,只能从具有或不具有逻辑顺序的有限预定义值列表中选择输出。 我们通常将二分类称为只有两个类的分类模型。 另一方面,连续输出可以具有任何数值。

首先要求用户选择一个数值列进行预测,然后对DataFrame中存在的所有其他数值列进行分类模型训练。

注意

注意:此示例应用的某些代码改编自这个页面

在此示例中,我们将使用内置样本数据集 #7:波士顿犯罪数据,为期两周的样本,但是您可以使用任何其他数据集,只要它具有足够的数据和数字列即可。

提醒一下,您可以使用以下代码浏览 PixieDust 内置数据集:

import pixiedust
pixiedust.sampleData()

Simple classification with DNNClassifier

PixieDust 中的内置数据集列表

以下代码使用sampleData() API 加载波士顿犯罪数据集:

import pixiedust
crimes = pixiedust.sampleData(7, forcePandas=True)

与往常一样,我们首先使用display()命令探索数据。 目标是寻找合适的列进行预测:

display(crimes)

Simple classification with DNNClassifier

犯罪数据集的表格视图

看起来就像nonviolent是二分类的良好候选者。 现在让我们调出一个条形图,以确保我们在此列中具有良好的数据分布:

Simple classification with DNNClassifier

在选项对话框中选择非暴力列

单击下一步,然后单击确定将产生以下图表::

Simple classification with DNNClassifier

非暴力犯罪的分布

不幸的是,数据偏向于非暴力犯罪,但我们有近 2,000 个暴力犯罪数据点,就本示例应用而言,这应该是可以的。

现在,我们准备创建do_training方法,该方法将使用tf.estimator.DNNClassifier创建分类模型。

注意

注意:您可以在此处找到有关DNNClassifier和其他高级 TensorFlow 估计器的更多信息

DNNClassifier构造器带有很多可选参数。 在示例应用中,我们将仅使用其中的三个,但是我建议您看一下文档中的其他参数:

  • feature_columnsfeature_column._FeatureColumn模型输入的迭代。 在我们的例子中,我们可以使用 Python 理解从 Pandas DataFrame的数字列创建一个数组。
  • hidden_units:每个单元可迭代的多个隐藏层。 在这里,我们将仅使用两层,每层 10 个节点。
  • n_classes:标签类别的数量。 我们将预测变量列上的DataFrame分组并计算行数来推断此数字。

这是do_training方法的代码:

def do_training(train, train_labels, test, test_labels, num_classes):
    #set TensorFlow logging level to INFO
    tf.logging.set_verbosity(tf.logging.INFO)

    # Build 2 hidden layer DNN with 10, 10 units respectively.
    classifier = tf.estimator.DNNClassifier(
        # Compute feature_columns from dataframe keys using a list comprehension
        feature_columns =
            [tf.feature_column.numeric_column(key=key) for key in train.keys()],
        hidden_units=[10, 10],
        n_classes=num_classes)

    # Train the Model
    classifier.train(
        input_fn=lambda:train_input_fn(train, train_labels,100),
        steps=1000
    )

    # Evaluate the model
    eval_result = classifier.evaluate(
        input_fn=lambda:eval_input_fn(test, test_labels,100)
    )

    return (classifier, eval_result)

注意

您可以在此处找到代码文件

classifier.train方法使用train_input_fn方法,该方法负责提供训练输入数据(又称地面实况)作为小批量,返回tf.data.Dataset(features, labels)的元组。 我们的代码也正在使用classifier.evaluate执行模型评估,以通过根据测试数据集对模型评分并在给定标签中比较结果来验证准确率。 然后将结果作为函数输出的一部分返回。

此方法需要与train_input_fn相似的eval_input_fn方法,除了我们在评估期间不使数据集可重复。 由于这两种方法共享大多数相同的代码,因此我们使用一种称为input_fn的辅助方法,这两种方法都使用适当的标志来调用该方法:

def input_fn(features, labels, batch_size, train):
    # Convert the inputs to a Dataset and shuffle.
    dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels)).shuffle(1000)
    if train:
        #repeat only for training
 dataset = dataset.repeat()
    # Return the dataset in batch
    return dataset.batch(batch_size)

def train_input_fn(features, labels, batch_size):
    return input_fn(features, labels, batch_size, train=True)

def eval_input_fn(features, labels, batch_size):
    return input_fn(features, labels, batch_size, train=False)

注意

您可以在此处找到代码文件

下一步是构建 PixieApp,它将从作为输入传递给run方法的 Pandas DataFrame创建分类器。 主屏幕将所有数字列的列表构建到一个下拉控件中,并要求用户选择将用作分类器输出的列。 在下面的代码中,这是通过 Jinja2 {%for ...%}循环来实现的,该循环在使用pixieapp_entity变量引用的作为输入传递的DataFrame上进行迭代。

注意

注意:以下代码使用[[SimpleClassificationDNN]]表示法表示它不是指定类的完整代码。 在提供完整的实现之前,请勿尝试运行此代码。

[[SimpleClassificationDNN]]
from pixiedust.display.app import *
@PixieApp
class SimpleClassificationDNN():
    @route()
    def main_screen(self):
        return """
<h1 style="margin:40px">
    <center>The classificiation model will be trained on all the numeric columns of the dataset</center>
</h1>
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
          <select id="cols{{prefix}}" style="width:100%;height:30px" pd_options="predictor=$val(cols{{prefix}})">
              <option value="0">Select a predictor column</option>
              {%for col in this.pixieapp_entity.columns.values.tolist()%}
 <option value="{{col}}">{{col}}</option>
 {%endfor%}
          </select>
        </div>
    </div>
</div>     
        """

注意

您可以在此处找到代码文件

使用crimes数据集,我们使用以下代码运行 PixieApp:

app = SimpleClassificationDNN()
app.run(crimes)

注意

注意:PixieApp 代码目前不完整,但是我们仍然可以看到欢迎页面的结果,如以下屏幕截图所示:

Simple classification with DNNClassifier

主屏幕显示输入 Pandas DataFrame中的列列表

当用户选择预测列(例如nonviolent)时,新的prepare_training路由将由pd_options="predictor=$val(cols{{prefix}})"属性触发。 此路由将显示两个条形图,分别显示训练集和测试集的输出类别分布,它们是使用从原始数据集中以 80/20 分割随机选择的。

注意

注意:我们在训练集和测试集之间使用 80/20 的比例,根据我的经验,这很常见。 当然,这不是绝对的规则,可以根据用例进行调整

屏幕片段还包括一个开始训练分类器的按钮。

prepare_training路由的代码如下所示:

[[SimpleClassificationDNN]]
@route(predictor="*")
@templateArgs
def prepare_training(self, predictor):
        #select only numerical columns
        self.dataset = self.pixieapp_entity.dropna(axis=1).select_dtypes(
            include=['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        )
        #Compute the number of classed by counting the groups
        self.num_classes = self.dataset.groupby(predictor).size().shape[0]
        #Create the train and test feature and labels
        self.train_x=self.dataset.sample(frac=0.8)
        self.full_train = self.train_x.copy()
        self.train_y = self.train_x.pop(predictor)
        self.test_x=self.dataset.drop(self.train_x.index)
        self.full_test = self.test_x.copy()
        self.test_y=self.test_x.pop(predictor)

        bar_chart_options = {
          "rowCount": "100",
          "keyFields": predictor,
          "handlerId": "barChart",
          "noChartCache": "true"
        }

        return """
<div class="container" style="margin-top:20px">
    <div class="row">
        <div class="col-sm-5">
            <h3><center>Train set class distribution</center></h3>
            <div pd_entity="full_train" pd_render_onload>
                <pd_options>{{bar_chart_options|tojson}}</pd_options>
            </div>
        </div>
        <div class="col-sm-5">
            <h3><center>Test set class distribution</center></h3>
            <div pd_entity="full_test" pd_render_onload>
                <pd_options>{{bar_chart_options|tojson}}</pd_options>
            </div>
        </div>
    </div>
</div>

<div style="text-align:center">
 <button class="btn btn-default" type="submit" pd_options="do_training=true">
 Start Training
 </button>
</div>
"""

注意

您可以在此处找到代码文件

注意:由于我们计算一次bar_chart_options变量,然后在 Jinja2 模板中使用它,因此使用。

选择nonviolent预测列将为我们提供以下屏幕截图结果:

Simple classification with DNNClassifier

预训练屏幕

开始训练按钮使用属性pd_options="do_training=true",调用do_training路由,该属性调用我们之前创建的do_training方法。 请注意,我们使用@captureOutput装饰器,因为由于我们将 TensorFlow 日志级别设置为INFO,所以我们希望捕获日志消息并将其显示给用户。 这些日志消息使用模式发送回浏览器,PixieDust 将自动将它们显示为特殊创建的<div>元素,并将其添加到数据中。 训练完成后,该路由返回一个 HTML 片段,该片段会生成一个表,该表具有do_training方法返回的评估指标,如以下代码所示:

[[SimpleClassificationDNN]]
@route(do_training="*")
   @captureOutput
def do_training_screen(self):
 self.classifier, self.eval_results = \
 do_training(
self.train_x, self.train_y, self.test_x, self.test_y, self.num_classes
 )
        return """
<h2>Training completed successfully</h2>
<table>
    <thead>
        <th>Metric</th>
        <th>Value</th>
    </thead>
    <tbody>
{%for key,value in this.eval_results.items()%}
<tr>
    <td>{{key}}</td>
    <td>{{value}}</td>
</tr>
{%endfor%}
    </tbody>
</table>
        """

注意

您可以在此处找到代码文件

以下屏幕快照显示了成功创建模型后的结果,并包括分类模型的评估指标表,其准确率为 87%:

Simple classification with DNNClassifier

最终屏幕显示成功训练的结果

使用crimes数据集作为参数运行此 PixieApp ,如以下代码所示:

app = SimpleClassificationDNN()
app.run(crimes)

成功训练模型后,您可以通过调用app.classifier变量上的predict方法来访问它以对新数据进行分类。 与trainevaluate方法类似,predict也采用input_fn来构造输入特征。

注意

注意:此处提供有关predict方法的更多详细信息

该示例应用通过使用高级估计器 API 为熟悉 TensorFlow 框架提供了一个很好的起点。

注意

注意:可在此处找到此示例应用的完整笔记本

在下一节中,我们将开始使用低级 TensorFlow API(包括张量,图和会话)来构建图像识别示例应用。

图像识别示例应用

在涉及构建开放式应用的时,您首先需要定义包含足够的 MVP最低可行产品)版本的要求。 使其实用且对用户有价值的功能。 在为实现制定技术决策时,确保尽快获得可行的端到端实现而又不花费太多时间是非常重要的标准。 这个想法是您希望从小处着手,以便快速迭代并改善您的应用。

对于我们的图像识别示例应用的 MVP,我们将使用以下要求:

  • 不要从头开始构建模型; 相反,请重用已公开的预训练的通用卷积神经网络CNN)模型之一,例如 MobileNet。 我们以后总是可以使用传递学习使用自定义训练图像来重新训练这些模型。
  • 对于 MVP,虽然我们只关注得分而不是训练,但我们仍然应该使用户感兴趣。 因此,让我们构建一个 PixieApp,该应用允许用户输入网页的 URL 并显示从该页面抓取的所有图像,包括由我们的模型推断出的分类输出。
  • 由于我们正在学习深度学习神经网络和 TensorFlow,因此如果可以在 Jupyter 笔记本中直接显示 TensorBoard Graph Visualization 会很棒,而不会强迫用户使用其他工具。 这将提供更好的用户体验并增加他们对应用的参与度。

注意

注意:本节中应用的实现改编自教程

第 1 部分——加载预训练的 MobileNet 模型

注意

注意:您可以下载完整的笔记本,以按照此处的讨论进行操作

在使用 CNN 的中,有很多公开可用的图像分类模型,这些模型已经在大型图像数据库(例如 ImageNet 上进行了预训练。 ImageNet 已发起了多个公共挑战,例如 ImageNet 大规模视觉识别挑战ILSVRC)或 Kaggle 上的 ImageNet 对象本地化挑战,结果非常有趣。

这些挑战已经产生了多个模型,例如 ResNet,Inception,SqueezeNet,VGGNet 或 Xception,每个模型都使用不同的神经网络架构。 遍历所有这些架构都超出了本书的范围,但是即使您还不是机器学习专家(我绝对不是),我还是鼓励您在线阅读它们。 我为该示例应用选择的模型是 MobileNet,因为它小巧,快速且非常准确。 它提供了用于 1,000 个图像类别的图像分类模型,对于该示例应用已经足够了。

为了确保代码的稳定性,我在 GitHub 存储库中复制了该模型

在此目录中,您可以找到以下文件:

  • frozen_graph.pb:TensorFlow 图的序列化二进制版本
  • labels.txt:一个文本文件,其中包含对 1,000 个图像类别及其索引的描述
  • quantized_graph.pb:模型图的压缩形式,使用 8 位定点表示

加载模型包括构建tf.graph对象和相关标签。 由于将来可能要加载多个模型,因此我们首先定义一个字典,该字典提供有关模型的元数据:

models = {
    "mobilenet": {
        "base_url":"https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%206/Visual%20Recognition/mobilenet_v1_0.50_224",
        "model_file_url": "frozen_graph.pb",
        "label_file": "labels.txt",
        "output_layer": "MobilenetV1/Predictions/Softmax"
    }
}

注意

您可以在这里找到文件

前面的models字典中的中的每个键都代表特定模型的元数据:

  • base_url:指向文件存储的 URL
  • model_file_url:假定相对于base_url的模型文件的名称
  • label_file:假定相对于base_url的标签名称
  • output_layer:为每个类别提供最终评分的输出层的名称

我们实现了get_model_attribute帮助方法,以方便读取model元数据,这在整个应用中将非常有用:

# helper method for reading attributes from the model metadata
def get_model_attribute(model, key, default_value = None):
    if key not in model:
        if default_value is None:
            raise Exception("Require model attribute {} not found".format(key))
        return default_value
    return model[key]

注意

您可以在此处找到代码文件

要加载图,我们下载二进制文件,使用ParseFromString方法将其加载到tf.GraphDef对象中,然后使用图作为当前内容管理器来调用tf.import_graph_def方法:

import tensorflow as tf
import requests
# Helper method for resolving url relative to the selected model
def get_url(model, path):
    return model["base_url"] + "/" + path

# Download the serialized model and create a TensorFlow graph
def load_graph(model):
    graph = tf.Graph()
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(
        requests.get( get_url( model, model["model_file_url"] ) ).content
    )
    with graph.as_default():
        tf.import_graph_def(graph_def)
    return graph

注意

您可以在此处找到代码文件

加载标签的方法返回一个 JSON 对象或一个数组(我们将在后面看到两者)。 以下代码使用 Python 列表推导来迭代requests.get调用返回的行。 然后,它使用as_json标志来适当地格式化数据:

# Load the labels
def load_labels(model, as_json = False):
    labels = [line.rstrip() \
      for line in requests.get(get_url(model, model["label_file"]) ).text.split("\n") if line != ""]
    if as_json:
        return [{"index": item.split(":")[0],"label":item.split(":")[1]} for item in labels]
    return labels

注意

您可以在此处找到代码文件

下一步是调用模型对图像进行分类。 为了使其更简单并且可能更有价值,我们要求用户提供指向包含要分类图像的 HTML 页面的 URL。 我们将使用 BeautifulSoup4 库来帮助解析页面。 要安装 BeautifulSoup4,只需运行以下命令:

!pip install beautifulsoup4

注意

注意:与往常一样,安装完成后,不要忘记重新启动内核。

下面的get_image_urls方法将 URL 作为输入,下载 HTML,实例化 BeautifulSoup 解析器,并提取以任何<img>元素和background-image样式找到的所有图像。 BeautifulSoup 具有一个非常优雅且易于使用的 API,用于解析 HTML。 在这里,我们仅使用find_all方法查找所有<img>元素,然后使用select方法选择所有具有内联样式的元素。 读者会很快注意到还有许多其他方法可以使用我们没有发现的 HTML 来创建图像,例如,声明为 CSS 类的图像。 与往常一样,如果您有兴趣和时间来改进它,我强烈欢迎 GitHub 存储库中的请求请求(有关如何创建请求请求的说明,请参见此处)。

get_image_urls的代码如下所示:

from bs4 import BeautifulSoup as BS
import re

# return an array of all the images scraped from an html page
def get_image_urls(url):
    # Instantiate a BeautifulSoup parser
    soup = BS(requests.get(url).text, "html.parser")

    # Local helper method for extracting url
    def extract_url(val):
        m = re.match(r"url\((.*)\)", val)
        val = m.group(1) if m is not None else val
        return "http:" + val if val.startswith("//") else val

    # List comprehension that look for <img> elements and backgroud-image styles
    return [extract_url(imgtag['src']) for imgtag in soup.find_all('img')] + [ \
        extract_url(val.strip()) for key,val in \
        [tuple(selector.split(":")) for elt in soup.select("[style]") \
            for selector in elt["style"].strip(" ;").split(";")] \
            if key.strip().lower()=='background-image' \
        ]

注意

您可以在此处找到代码文件

对于发现的每个图像,我们还需要一个辅助函数来下载将作为输入传递到模型进行分类的图像。

以下download_image方法将图像下载到一个临时文件中:

import tempfile
def download_image(url):
   response = requests.get(url, stream=True)
   if response.status_code == 200:
      with tempfile.NamedTemporaryFile(delete=False) as f:
 for chunk in response.iter_content(2048):
 f.write(chunk)
         return f.name
   else:
      raise Exception("Unable to download image: {}".format(response.status_code))

注意

您可以在此处找到代码文件

给定图像的本地路径,我们现在需要通过从tf.image包(即.png文件的decode_png)调用正确的解码方法,将其解码为张量。

注意

:在数学中,张量是向量的概括,向量由方向和大小定义,以支持更高的维度。 向量是 1 阶张量,类似地,标量是 0 阶张量。直观地,我们可以将 2 阶张量视为一个二维数组,其值定义为两个向量相乘的结果。 在 TensorFlow 中,张量是 N 维数组。

在图像读取器张量上进行了一些转换(广播到右边的小数表示,调整大小和规格化)之后,我们在规格化张量上调用tf.Session.run以使执行之前定义的步骤,如以下代码所示:

# decode a given image into a tensor
def read_tensor_from_image_file(model, file_name):
    file_reader = tf.read_file(file_name, "file_reader")
    if file_name.endswith(".png"):
        image_reader = tf.image.decode_png(file_reader, channels = 3,name='png_reader')
    elif file_name.endswith(".gif"):
        image_reader = tf.squeeze(tf.image.decode_gif(file_reader,name='gif_reader'))
    elif file_name.endswith(".bmp"):
        image_reader = tf.image.decode_bmp(file_reader, name='bmp_reader')
    else:
        image_reader = tf.image.decode_jpeg(file_reader, channels = 3, name='jpeg_reader')
    float_caster = tf.cast(image_reader, tf.float32)
    dims_expander = tf.expand_dims(float_caster, 0);

    # Read some info from the model metadata, providing default values
    input_height = get_model_attribute(model, "input_height", 224)
    input_width = get_model_attribute(model, "input_width", 224)
    input_mean = get_model_attribute(model, "input_mean", 0)
    input_std = get_model_attribute(model, "input_std", 255)

    resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width])
    normalized = tf.divide(tf.subtract(resized, [input_mean]), [input_std])
    sess = tf.Session()
    result = sess.run(normalized)
    return result

注意

您可以在此处找到代码文件

一切准备就绪后,我们现在就可以实现score_image方法,该方法将tf.graph,模型元数据和图像的 URL 作为输入参数,并根据其置信度返回前五个候​​选类别分数,包括其标签:

import numpy as np

# classify an image given its url
def score_image(graph, model, url):
    # Get the input and output layer from the model
    input_layer = get_model_attribute(model, "input_layer", "input")
    output_layer = get_model_attribute(model, "output_layer")

    # Download the image and build a tensor from its data
    t = read_tensor_from_image_file(model, download_image(url))

    # Retrieve the tensors corresponding to the input and output layers
    input_tensor = graph.get_tensor_by_name("import/" + input_layer + ":0");
    output_tensor = graph.get_tensor_by_name("import/" + output_layer + ":0");

    with tf.Session(graph=graph) as sess:
        results = sess.run(output_tensor, {input_tensor: t})
    results = np.squeeze(results)
    # select the top 5 candidate and match them to the labels
    top_k = results.argsort()[-5:][::-1]
 labels = load_labels(model)
 return [(labels[i].split(":")[1], results[i]) for i in top_k]

注意

您可以在此处找到代码文件

现在,我们可以使用以下步骤测试代码:

  1. 选择mobilenet模型并加载相应的图
  2. 获取从 Flickr 网站抓取的图像 URL 的列表
  3. 为每个图像 URL 调用score_image方法并打印结果

代码如下所示:

model = models['mobilenet']
graph = load_graph(model)
image_urls = get_image_urls("https://www.flickr.com/search/?text=cats")
for url in image_urls:
    results = score_image(graph, model, url)
    print("Result for {}: \n\t{}".format(url, results))

注意

您可以在此处找到代码文件

结果非常准确(除了第一张图像是空白图像),如以下屏幕截图所示:

Part 1 – Load the pretrained MobileNet model

在 Flickr 页面上找到与猫有关的图像的分类

我们的图像识别示例应用的第 1 部分现已完成; 您可以在以下位置找到完整的笔记本

在下一部分中,我们将通过使用 PixieApp 构建用户界面来构建更加用户友好的体验。

第 2 部分——为我们的图像识别示例应用创建 PixieApp

注意

注意:您可以下载完整的笔记本,以按照此处的讨论进行操作

提醒一下,PixieApp 的setup方法(如果已定义)是在应用开始运行之前执行的。 我们使用选择模型并初始化图:

from pixiedust.display.app import *

@PixieApp
class ScoreImageApp():
    def setup(self):
        self.model = models["mobilenet"]
        self.graph = load_graph( self.model )
    ...

注意

您可以在此处找到代码文件

在 PixieApp 的主屏幕中,我们使用输入框让用户输入网页的 URL,如以下代码片段所示:

[[ScoreImageApp]]
@route()
def main_screen(self):
   return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
          <input id="url{{prefix}}" type="text" class="form-control"
              value="https://www.flickr.com/search/?text=cats"
              placeholder="Enter a url that contains images">
          <span class="input-group-btn">
            <button class="btn btn-default" type="button" pd_options="image_url=$val(url{{prefix}})">Go</button>
          </span>
        </div>
    </div>
</div>
"""

注意

您可以在此处找到代码文件

为了方便起见,我们使用默认值https://www.flickr.com/search/?text=cats初始化输入文本。

我们已经可以使用以下代码运行代码以测试主屏幕:

app = ScoreImageApp()
app.run()

主屏幕如下所示:

Part 2 – Create a PixieApp for our image recognition sample application

图像识别 PixieApp 的主屏幕

注意

注意:这对测试很有用,但是我们要记住,do_process_url路由尚未实现,因此,单击前进按钮将退回到再次使用默认路由。

现在让我们实现do_process_url路由,该路由在用户单击前进按钮时触发。 该路由首先调用get_image_urls方法以获取图像 URL 列表。 然后,使用 Jinja2,我们构建一个 HTML 片段来显示所有图像。 对于每个图像,我们异步调用运行模型并显示结果的do_score_url路由。

以下代码显示了do_process_url路由的实现:

[[ScoreImageApp]]
@route(image_url="*")
@templateArgs
def do_process_url(self, image_url):
    image_urls = get_image_urls(image_url)
    return """
<div>
{%for url in image_urls%}
<div style="float: left; font-size: 9pt; text-align: center; width: 30%; margin-right: 1%; margin-bottom: 0.5em;">
<img src="{{url}}" style="width: 100%">
  <div style="display:inline-block" pd_render_onload pd_options="score_url={{url}}">
  </div>
</div>
{%endfor%}
<p style="clear: both;">
</div>
        """

注意

您可以在此处找到代码文件

注意@templateArgs装饰器的使用,它允许 Jinja2 片段引用本地image_urls变量。

最后,在do_score_url路由中,我们调用score_image并将结果显示为列表:

[[ScoreImageApp]]
@route(score_url="*")
@templateArgs
def do_score_url(self, score_url):
    results = score_image(self.graph, self.model, score_url)
    return """
<ul style="text-align:left">
{%for label, confidence in results%}
<li><b>{{label}}</b>: {{confidence}}</li>
{%endfor%}
</ul>
"""

注意

您可以在此处找到代码文件

以下屏幕截图显示了包含猫图像的 Flickr 页面的结果:

Part 2 – Create a PixieApp for our image recognition sample application

猫的图像分类结果

注意

提醒一下,您可以在以下位置找到完整的笔记本

我们的 MVP 应用即将完成。 在下一节中,我们将在笔记本中直接集成 TensorBoard 图可视化。

第 3 部分——集成 TensorBoard 图可视化

注意

注意:本节中描述的部分代码改编自位于此处的deepdream笔记本

您可以在此处下载完整的笔记本以遵循本节的讨论

TensorFlow 带有非常强大的可视化套件,可帮助您调试和优化应用的性能。 请花一点时间在这里探索 TensorBoard 功能

这里的一个问题是,配置 TensorBoard 服务器以使其与笔记本电脑一起使用可能会很困难,尤其是如果您的笔记本电脑托管在云中,并且您几乎无法访问基础操作系统。 在这种情况下,配置和启动 TensorBoard 服务器可能证明是不可能的任务。 在本节中,我们将说明如何通过将模型图可视化直接集成到您的笔记本电脑中(零配置)来解决此问题。 为了提供更好的用户体验,我们想将 TensorBoard 可视化添加到我们的 PixieApp 中。 为此,我们将主布局更改为选项卡布局,然后将 TensorBoard 可视化效果分配给自己的选项卡。 方便地,PixieDust 提供了一个名为TemplateTabbedApp的基本 PixieApp,用于构建选项卡式布局。 使用TemplateTabbedApp作为基类时,我们需要按以下方式配置setup方法中的选项卡:

[[ImageRecoApp]]
from pixiedust.apps.template import TemplateTabbedApp
@PixieApp
class ImageRecoApp(TemplateTabbedApp):
    def setup(self):
        self.apps = [
            {"title": "Score", "app_class": "ScoreImageApp"},
            {"title": "Model", "app_class": "TensorGraphApp"},
            {"title": "Labels", "app_class": "LabelsApp"}
        ]
        self.model = models["mobilenet"]
        self.graph = self.load_graph(self.model)

app = ImageRecoApp()
app.run()

注意

您可以在此处找到代码文件

应该注意的是,在前面的代码中,我们尚未将LabelsApp子 PixieApp 添加到选项卡列表中,即使尚未实现。 因此,按预期,如果按原样运行代码,则Labels选项卡将失败。

self.apps包含定义选项卡的对象数组:

  • title:标签标题
  • app_class:选择选项卡时运行的 PixieApp

ImageRecoApp中,我们配置了与三个子 PixieApps 相关的三个选项卡:我们已经在第 2 部分“为我们的图像识别示例应用创建一个 PixieApp”中创建的ScoreImageApp,会显示模型图,LabelsApp会显示模型中使用的所有标签类别的表格。

结果显示在以下屏幕截图中:

Part 3 – Integrate the TensorBoard graph visualization

选项卡式布局,其中包括得分,模型和标签

使用TemplateTabbedApp超类的另一个好处是,分别定义了子 PixieApps,这使代码更易于维护和重用。

首先让我们看一下TensorGraphApp PixieApp。 它的主要路由返回一个 HTML 片段,该片段将tf-graph-basic.build.htmlhttps://tensorboard.appspot.com,加载到 IFrame 中,并使用 JavaScript 加载监听器应用通过tf.Graph.as_graph_def方法计算的序列化图定义。 为确保图定义保持合理的大小,并避免浏览器客户端上不必要的性能下降,我们调用strip_consts方法来移除具有大尺寸的恒定值的张量。

TensorGraphApp的代码如下所示:

@PixieApp
class TensorGraphApp():
    """Visualize TensorFlow graph."""
    def setup(self):
        self.graph = self.parent_pixieapp.graph

    @route()
    @templateArgs
    def main_screen(self):
        strip_def = self.strip_consts(self.graph.as_graph_def())
        code = """
            <script>
              function load() {{
                document.getElementById("{id}").pbtxt = {data};
              }}
            </script>
            <link rel="import" href="https://tensorboard.appspot.com/tf-graph-basic.build.html" onload=load()>
            <div style="height:600px">
              <tf-graph-basic id="{id}"></tf-graph-basic>
            </div>
        """.format(data=repr(str(strip_def)), id='graph'+ self.getPrefix()).replace('"', '&quot;')

        return """
<iframe seamless style="width:1200px;height:620px;border:0" srcdoc="{{code}}"></iframe>
"""

    def strip_consts(self, graph_def, max_const_size=32):
        """Strip large constant values from graph_def."""
        strip_def = tf.GraphDef()
        for n0 in graph_def.node:
            n = strip_def.node.add() 
            n.MergeFrom(n0)
            if n.op == 'Const':
                tensor = n.attr['value'].tensor
                size = len(tensor.tensor_content)
                if size > max_const_size:
                    tensor.tensor_content = "<stripped {} bytes>".format(size).encode("UTF-8")
        return strip_def

注意

您可以在此处找到代码文件

注意:子级 PixieApps 可以通过self.parent_pixieapp变量访问其父级 PixieApp。

以下屏幕快照显示了TensorGraphApp子 PixieApp 的结果屏幕。 它为选定的模型提供了 TensorFlow 图的交互式可视化,允许用户浏览不同的节点并深入研究模型。 但是,重要的是要注意,可视化完全在浏览器中运行,而没有 TensorBoard 服务器。 因此,整个 TensorBoard 中可用的某些功能(例如运行时统计信息)被禁用。

Part 3 – Integrate the TensorBoard graph visualization

显示 MobileNet V1 的模型图

LabelsApp PixieApp 中,我们简单地将标签加载为 JSON 格式,并使用handlerId=tableView选项在 PixieDust 表中显示它:

[[LabelsApp]]
@PixieApp
class LabelsApp():
    def setup(self):
        self.labels = self.parent_pixieapp.load_labels(
            self.parent_pixieapp.model, as_json=True
        )

    @route()
    def main_screen(self):
        return """
<div pd_render_onload pd_entity="labels">
    <pd_options>
    {
        "table_noschema": "true",
 "handlerId": "tableView",
        "rowCount": "10000"
    }
    </pd_options>
</div>
        """

注意

您可以在此处找到代码文件

注意:我们通过将table_noschema设置为true将表配置为不显示架构,但是为了方便起见,我们保留了搜索栏。

结果显示在以下屏幕截图中:

Part 3 – Integrate the TensorBoard graph visualization

可搜索的表中的模型类别

我们的 MVP 图像识别示例应用现已完成; 您可以在此处找到完整的笔记本

在下一个部分,我们将通过允许用户使用自定义图像重新训练模型来改进应用。

第 4 部分——使用自定义训练数据重新训练模型

注意

注意:您可以下载完整的笔记本,以按照此处的讨论进行操作

本节中的代码相当广泛,一些与该主题不直接相关的帮助器函数将被省略。 但是,与往常一样,请参阅 GitHub 上完整的笔记本以获取有关代码的更多信息。

在本节中,我们希望使用自定义训练数据对 MobileNet 模型进行再训练,并使用它来对在常规模型上得分较低的图像进行分类。

注意

注意:本节中的代码改编自《将 TensorFlow 用于作诗》教程

在大多数情况下,获得高质量的训练数据可能是最艰巨且耗时的任务之一。 在我们的示例中,我们要训练的每个类都需要大量的图像。 为了简单和可重复性,我们使用 ImageNet 数据库方便地提供用于获取 URL 和相关标签的 API。 我们还将下载的文件限制为.jpg文件。 当然,如有需要,随时获取您自己的训练数据。

我们首先下载了 2011 年秋季版本中所有图像 URL 的列表,可在此处下载,然后将文件解压缩到您选择的本地目录(例如,我选择/Users/dtaieb/Downloads/fall11_urls.txt)。我们还需要下载上所有synsets的 WordNet ID 和单词之间的映射,网址为 http://image-net.org/archive/ words.txt ,我们将使用它来查找包含我们需要下载的 URL 的 WordNet ID。

以下代码将两个文件分别加载到 Pandas DataFrame中:

import pandas
wnid_to_urls = pandas.read_csv('/Users/dtaieb/Downloads/fall11_urls.txt',
                sep='\t', names=["wnid", "url"],
                header=0, error_bad_lines=False,
                warn_bad_lines=False, encoding="ISO-8859-1")
wnid_to_urls['wnid'] = wnid_to_urls['wnid'].apply(lambda x: x.split("_")[0])
wnid_to_urls = wnid_to_urls.dropna()

wnid_to_words = pandas.read_csv('/Users/dtaieb/Downloads/words.txt',
                sep='\t', names=["wnid", "description"],
                header=0, error_bad_lines=False,
                warn_bad_lines=False, encoding="ISO-8859-1")
wnid_to_words = wnid_to_words.dropna()

注意

您可以在此处找到代码文件

注意,我们需要清除wnid_to_urls数据集中的wnid列,因为它包含与类别中图像索引相对应的后缀。

然后,我们可以定义方法get_url_for_keywords,该方法返回一个字典,其中包含作为关键字的类别和作为值的 URL 数组:

def get_url_for_keywords(keywords):
    results = {}
    for keyword in keywords:
        df = wnid_to_words.loc[wnid_to_words['description'] == keyword]
        row_list = df['wnid'].values.tolist()
        descriptions = df['description'].values.tolist()
        if len(row_list) > 0:
            results[descriptions[0]] = \
            wnid_to_urls.loc[wnid_to_urls['wnid'] == \
            row_list[0]]["url"].values.tolist()
    return results

注意

您可以在此处找到代码文件

通过使用 PixieDust display,我们可以轻松浏览一下数据分布。 与往常一样,随时可以自己进行更多探索:

Part 4 – Retrain the model with custom training data

按类别分配图像

我们现在可以构建代码,该代码将下载与我们选择的类别列表相对应的图像。 在本例中,我们选择了水果:["apple", "orange", "pear", "banana"]。 图像将被下载到 PixieDust 主目录的子目录中(使用pixiedust.utils包中的 PixieDust Environment助手类),从而将图像数量限制为500以提高速度:

注意

注意:以下代码使用笔记本中先前定义的方法和导入。 在尝试运行以下代码之前,请确保运行相应的单元格。

from pixiedust.utils.environment import Environment
root_dir = ensure_dir_exists(os.path.join(Environment.pixiedustHome, "imageRecoApp")
image_dir = root_dir
image_dict = get_url_for_keywords(["apple", "orange", "pear", "banana"])
with open(os.path.join(image_dir, "retrained_label.txt"), "w") as f_label:
    for key in image_dict:
        f_label.write(key + "\n")
        path = ensure_dir_exists(os.path.join(image_dir, key))
        count = 0
        for url in image_dict[key]:
            download_image_into_dir(url, path)
            count += 1
            if count > 500:
                break;

注意

您可以在此处找到代码文件

代码的下一部分使用以下步骤处理训练集中的每个图像:

注意

注意:如前所述,该代码相当广泛,并且省略了一部分; 这里仅说明重要部分。 请不要尝试按原样运行以下代码,并且请参阅完整的笔记本以获取完整的实现。

  1. 使用以下代码对.jpeg文件进行解码:

    def add_jpeg_decoding(model):
        input_height = get_model_attribute(model,
                       "input_height")
        input_width = get_model_attribute(model, "input_width")
        input_depth = get_model_attribute(model, "input_depth")
        input_mean = get_model_attribute(model, "input_mean",
                     0)
        input_std = get_model_attribute(model, "input_std",
                    255)
    
        jpeg_data = tf.placeholder(tf.string,
                    name='DecodeJPGInput')
        decoded_image = tf.image.decode_jpeg(jpeg_data,
                        channels=input_depth)
        decoded_image_as_float = tf.cast(decoded_image,
                                 dtype=tf.float32)
        decoded_image_4d =  tf.expand_dims(
                           decoded_image_as_float,
                           0)
        resize_shape = tf.stack([input_height, input_width])
        resize_shape_as_int = tf.cast(resize_shape,
                              dtype=tf.int32)
        resized_image = tf.image.resize_bilinear(
                        decoded_image_4d,
                        resize_shape_as_int)
        offset_image = tf.subtract(resized_image, input_mean)
        mul_image = tf.multiply(offset_image, 1.0 / input_std)
        return jpeg_data, mul_image
    

    注意

    您可以在此处找到代码文件

  2. 创建瓶颈值(适当地缓存它们),以通过调整大小和缩放比例来标准化图像。 这是通过以下代码完成的:

    def run_bottleneck_on_image(sess, image_data,
        image_data_tensor,decoded_image_tensor,
        resized_input_tensor,bottleneck_tensor):
        # First decode the JPEG image, resize it, and rescale the pixel values.
        resized_input_values = sess.run(decoded_image_tensor,
            {image_data_tensor: image_data})
        # Then run it through the recognition network.
        bottleneck_values = sess.run(
            bottleneck_tensor,
            {resized_input_tensor: resized_input_values})
        bottleneck_values = np.squeeze(bottleneck_values)
        return bottleneck_values
    

    注意

    您可以在此处找到代码文件

  3. 在公共名称空间下,使用add_final_training_ops方法添加最终的训练操作,以便在可视化图时更易于操作。 训练步骤如下:

    1. 使用tf.truncated_normal API 生成随机权重:

            initial_value = tf.truncated_normal(
                [bottleneck_tensor_size, class_count],
                stddev=0.001)
                layer_weights = tf.Variable(
                    initial_value, name='final_weights')
      
    2. 添加偏置,并初始化为零:

            layer_biases = tf.Variable(tf.zeros([class_count]),
                name='final_biases')
      
    3. 计算加权和:

            logits = tf.matmul(bottleneck_input, layer_weights) +
                layer_biases
      
    4. 添加cross_entropy成本函数:

            cross_entropy =
                tf.nn.softmax_cross_entropy_with_logits(
                labels=ground_truth_input, logits=logits)
            with tf.name_scope('total'):
                cross_entropy_mean = tf.reduce_mean(
                cross_entropy)
      

      ]

    5. 最小化成本函数:

            optimizer = tf.train.GradientDescentOptimizer(
                learning_rate)
            train_step = optimizer.minimize(cross_entropy_mean)
      

为了可视化重新训练的图,我们首先需要更新TensorGraphApp PixieApp,以让用户选择要可视化的模型:通用 MobileNet 或自定义。 这是通过在主路径中添加<select>下拉菜单并附加pd_script元素来更新状态来完成的:

[[TensorGraphApp]]
return """
{%if this.custom_graph%}
<div style="margin-top:10px" pd_refresh>
    <pd_script>
self.graph = self.custom_graph if self.graph is not self.custom_graph else self.parent_pixieapp.graph
    </pd_script>
    <span style="font-weight:bold">Select a model to display:</span>
    <select>
 <option {%if this.graph!=this.custom_graph%}selected{%endif%} value="main">MobileNet</option>
 <option {%if this.graph==this.custom_graph%}selected{%endif%} value="custom">Custom</options>
    </select>
{%endif%}
<iframe seamless style="width:1200px;height:620px;border:0" srcdoc="{{code}}"></iframe>
"""

注意

您可以在此处找到代码文件

重新运行我们的ImageReco PixieApp 会产生以下屏幕截图:

Part 4 – Retrain the model with custom training data

可视化再训练图

单击训练节点将显示运行反向传播算法的嵌套操作,以最小化前面的add_final_training_ops中指定的cross_entropy_mean成本函数:

with tf.name_scope('cross_entropy'):
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(
        labels=ground_truth_input, logits=logits)
    with tf.name_scope('total'):
        cross_entropy_mean = tf.reduce_mean(cross_entropy)

注意

您可以在此处找到代码文件

以下屏幕截图显示了训练命名空间的详细信息:

Part 4 – Retrain the model with custom training data

训练期间的反向传播

同样,我们可以在LabelsApp中添加下拉开关,以在通用 MobileNet 和自定义模型之间切换可视化效果:

[[LabelsApp]]
@PixieApp
class LabelsApp():
    def setup(self):
        ...

    @route()
    def main_screen(self):
        return """
{%if this.custom_labels%}
<div style="margin-top:10px" pd_refresh>
    <pd_script>
self.current_labels = self.custom_labels if self.current_labels is not self.custom_labels else self.labels
    </pd_script>
    <span style="font-weight:bold">
        Select a model to display:</span>
    <select>
        <option {%if this.current_labels!=this.labels%}selected{%endif%} value="main">MobileNet</option>
        <option {%if this.current_labels==this.custom_labels%}selected{%endif%} value="custom">Custom</options>
    </select>
{%endif%}
<div pd_render_onload pd_entity="current_labels">
    <pd_options>
    {
        "table_noschema": "true",
        "handlerId": "tableView",
        "rowCount": "10000",
        "noChartCache": "true"

    }
    </pd_options>
</div>
        """

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Part 4 – Retrain the model with custom training data

显示每个模型的标签信息

我们的第 4 部分 MVP 的最后一步是更新score_image方法,以使用两个模型对图像进行分类,并将结果添加到带有每个模型条目的字典中。 我们定义了一个本地方法do_score_image,该方法返回前 5 个候选答案。

每个模型都会调用此方法,结果将以模型名称为关键字填充字典:

# classify an image given its url
def score_image(graph, model, url):
    # Download the image and build a tensor from its data
    t = read_tensor_from_image_file(model, download_image(url))

    def do_score_image(graph, output_layer, labels):
        # Retrieve the tensors corresponding to the input and output layers
        input_tensor = graph.get_tensor_by_name("import/" +
            input_layer + ":0");
        output_tensor = graph.get_tensor_by_name( output_layer +
            ":0");

        with tf.Session(graph=graph) as sess:
            # Initialize the variables
            sess.run(tf.global_variables_initializer())
            results = sess.run(output_tensor, {input_tensor: t})
        results = np.squeeze(results)
        # select the top 5 candidates and match them to the labels
        top_k = results.argsort()[-5:][::-1]
        return [(labels[i].split(":")[1], results[i]) for i in top_k]

    results = {}
    input_layer = get_model_attribute(model, "input_layer",
        "input")
    labels = load_labels(model)
    results["mobilenet"] = do_score_image(graph, "import/" +
        get_model_attribute(model, "output_layer"), labels)
    if "custom_graph" in model and "custom_labels" in model:
        with open(model["custom_labels"]) as f:
            labels = [line.rstrip() for line in f.readlines() if line != ""]
            custom_labels = ["{}:{}".format(i, label) for i,label in zip(range(len(labels)), labels)]
        results["custom"] = do_score_image(model["custom_graph"],
            "final_result", custom_labels)
    return results

注意

您可以在此处找到代码文件

由于修改了score_image方法的返回值,因此我们需要调整ScoreImageApp中返回的 HTML 片段,以循环遍历results词典的所有模型条目:

@route(score_url="*")
@templateArgs
def do_score_url(self, score_url):
    scores_dict = score_image(self.graph, self.model, score_url)
    return """
{%for model, results in scores_dict.items()%}
<div style="font-weight:bold">{{model}}</div>
<ul style="text-align:left">
{%for label, confidence in results%}
<li><b>{{label}}</b>: {{confidence}}</li>
{%endfor%}
</ul>
{%endfor%}
    """

注意

您可以在此处找到代码文件

进行这些更改后,PixieApp 将自动调用自定义模型(如果可用),并且在这种情况下,将显示两个模型的结果。

以下屏幕截图显示了与香蕉相关的图像的结果:

Part 4 – Retrain the model with custom training data

使用通用 MobileNet 和定制训练模型进行评分

读者会注意到,定制模型的分数非常低。 一种可能的解释是,训练数据采集是完全自动化的,无需人工管理即可使用。 此示例应用的一项可能增强是将训练数据获取和再训练步骤移至其自己的标签 PixieApp 中。 我们还应该给用户机会验证图像​​并拒绝质量低劣的图像。 让用户重新标记被错误分类的图像也将是很棒的。

注意

完整的第四部分笔记本可以在以下位置找到

在本节中,我们讨论了使用 TensorFlow 在 Jupyter 笔记本中构建图像识别示例应用的增量方法,特别着重于使用 PixieApps 操作算法。 我们首先使用 TensorFlow DNNClassifier估计器从 Pandas DataFrame构建简单的分类模型。 然后我们分四个部分构建了图像识别示例应用的 MVP 版本:

  1. 我们加载了预训练的 MobileNet 模型
  2. 我们为图像识别示例应用创建了一个 PixieApp
  3. 我们将 TensorBoard 图可视化集成到 PixieApp 中
  4. 我们使用户能够使用来自 ImageNet 的自定义训练数据来重新训练模型

总结

机器学习是一个巨大的主题,无论在研发方面,它都得到了巨大的发展。 在本章中,我们仅结合机器学习算法探索了最新技术,即使用深度学习神经网络执行图像识别。 对于刚开始熟悉机器学习的一些读者来说,示例 PixieApps 和相关的算法代码可能太深,无法一次消化。 但是,其根本目的是演示如何迭代地构建利用机器学习模型的应用。 我们曾经尝试使用卷积神经网络模型进行图像识别,但是任何其他模型都可以。

希望您对 PixieDust 和 PixieApp 编程模型可以如何帮助您完成自己的项目有一个好主意,我强烈建议您使用此示例应用作为起点,使用您选择的机器学习来构建自己的自定义应用 。 我还建议您通过 PixieGateway 微服务将 PixieApp 部署为 Web 应用,并探索它是否是可行的解决方案。

在下一章中,我们将介绍与大数据和自然语言处理有关的另一个重要的行业用例。 我们将构建一个示例应用,使用自然语言理解服务来分析社交媒体趋势。

七、大数据和 Twitter 情感分析

“数据是新的石油。”

————未知

在本章中,我们将研究 AI 和数据科学的两个重要领域:自然语言处理NLP)和大数据分析。 对于支持的示例应用,我们重新实现了 Twitter 主题标签项目的情感分析,该项目在第 1 章“开发人员对数据科学的观点”中进行了介绍, 我们利用 Jupyter 笔记本和 PixieDust 构建实时的仪表板,以分析从相关的推文流到特定实体(例如公司提供的产品)的数据,以提供情感信息,以及有关从相同推文中提取的趋势实体的其他信息。 在本章的最后,读者将学习如何将基于云的 NLP 服务(例如,IBM Watson 自然语言理解)集成到其应用中,以及如何在(Twitter)规模上使用诸如 Apache Spark。

与往常一样,我们将展示如何通过将实时仪表板实现为直接在 Jupyter 笔记本中运行的 PixieApp 来实现分析的操作。

Apache Spark 入门

大数据一词可能会感到模糊不清。 考虑任何数据集大数据的截止点是什么? 是 10 GB,100 GB,1 TB 还是更多? 我喜欢的一个定义是:大数据是指数据无法容纳在单台计算机上可用的内存中。 多年以来,数据科学家一直被迫对大型数据集进行采样,因此它们可以放入一台机器中,但是随着并行计算框架能够将数据分布到一组机器中,并行处理框架开始发生变化,这种情况就开始发生变化。 整个数据集,当然前提是集群具有足够的计算机。 同时,随着云技术的进步,可以按需提供适应数据集大小的机器集群。

如今,有多个框架(大多数时候可以作为开源使用),可以提供强大,灵活的并行计算功能。 最受欢迎的包括 Apache HadoopApache SparkDask。 对于我们的 Twitter 情感分析应用,我们将使用 Apache Spark,它在可伸缩性,可编程性和速度方面提供出色的性能。 此外,许多云提供商提供了一些 Spark 即服务的功能,使您能够在几分钟内按需创建大小合适的 Spark 集群。

一些 Spark 即服务的云提供商包括:

注意

注意:Apache Spark 也可以很容易地安装在本地计算机上以进行测试,在这种情况下,群集节点是使用线程模拟的。

Apache Spark 架构

以下图显示了 Apache Spark 框架的主要组件:

Apache Spark architecture

Spark 高级架构

  • Spark SQL:此组件的核心数据结构是 Spark DataFrame,它使了解 SQL 语言的用户可以轻松处理结构化数据。
  • Spark Streaming:模块用于处理流数据。 稍后我们将看到,我们将在示例应用中使用此模块,尤其是使用结构化流(在 Spark 2.0 中引入)。
  • MLlib:模块,提供了功能丰富的机器学习库,可在 Spark 规模上工作。
  • GraphX:模块用于执行图并行计算。

主要有两种使用 Spark 集群的方法,如下图所示:

Apache Spark architecture

使用 Spark 集群的两种方法

  • spark-submit:用于在集群上启动 Spark 应用的 Shell 脚本
  • 笔记本:以交互方式针对 Spark 集群执行代码语句

关于spark-submit shell 脚本的内容不在本书的讨论范围内,但是可以在以下位置找到官方文档。 在本章的其余部分,我们将重点介绍通过 Jupyter 笔记本与 Spark 集群进行交互。

配置笔记本来配合 Spark

本节中的说明仅涉及在本地安装 Spark 以进行开发和测试。 在群集中手动安装 Spark 超出了本书的范围。 如果需要一个真正的集群,强烈建议使用基于云的服务。

默认情况下,本地 Jupyter 笔记本安装了纯 Python 内核。 要使用 Spark,用户必须使用以下步骤:

  1. 通过从这个页面下载二进制发行版在本地安装 Spark。

  2. 使用以下命令在临时目录中生成内核规范:

    ipython kernel install --prefix /tmp
    
    

    注意

    注意:只要声明以下消息,上述命令可能会生成警告消息,可以安全地忽略该警告消息:

    Installed kernelspec python3 in /tmp/share/jupyter/kernels/python3

  3. 转到/tmp/share/jupyter/kernels/python3,然后编辑kernel.json文件,将以下键添加到 JSON 对象(将<<spark_root_path>>替换为安装 Spark 的目录路径,将<<py4j_version>>替换为系统上安装的版本):

    "env": {
        "PYTHONPATH": "<<spark_root_path>>/python/:<<spark_root_path>>/python/lib/py4j-<<py4j_version>>-src.zip",
        "SPARK_HOME": "<<spark_root_path>>",
        "PYSPARK_SUBMIT_ARGS": "--master local[10] pyspark-shell",
        "SPARK_DRIVER_MEMORY": "10G",
        "SPARK_LOCAL_IP": "127.0.0.1",
        "PYTHONSTARTUP": "<<spark_root_path>>/python/pyspark/shell.py"
    }
    
  4. 您可能还需要自定义display_name键,使其具有唯一性,并且可以从 Juptyer UI 轻松识别。 如果需要了解现有内核的列表,可以使用以下命令:

    jupyter kernelspec list
    
    

    前面的命令将为您提供内核名称列表以及本地文件系统上的关联路径。 从路径中,您可以打开kernel.json文件来访问display_name值。 例如:

     Available kernels:
     pixiedustspark16
     /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark16
     pixiedustspark21
     /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark21
     pixiedustspark22
     /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark22
     pixiedustspark23
     /Users/dtaieb/Library/Jupyter/kernels/pixiedustspark23
    
    
  5. 使用以下命令将内核与已编辑的文件一起安装:

    jupyter kernelspec install /tmp/share/jupyter/kernels/python3
    
    

    注意

    注意:根据环境,运行前面的命令时,您可能会收到“权限被拒绝”错误。 在这种情况下,您可能希望使用sudo以管理员权限运行命令或使用--user开关,如下所示:

    jupyter kernelspec install --user /tmp/share/jupyter/kernels/python3

    有关安装选项的更多信息,可以使用-h开关。 例如:

     jupyter kernelspec install -h
    
    
  6. 重新启动笔记本服务器并开始使用新的 PySpark 内核。

幸运的是,PixieDust 提供了一个install脚本来自动执行上述手动步骤。

注意

您可以在此处找到有关此脚本的详细文档

简而言之,使用自动 PixieDust install脚本需要发出以下命令并遵循屏幕上的说明:

jupyter pixiedust install

我们将在本章稍后深入研究 Spark 编程模型,但是现在,让我们在下一部分中定义 Twitter 情感分析应用的 MVP 要求。

Twitter 情感分析应用

与往常一样,我们首先定义 MVP 版本的要求:

  • 连接到 Twitter 以获取由用户提供的查询字符串过滤的实时推文流
  • 丰富推文以添加情感信息和从文本中提取的相关实体
  • 使用实时图表显示仪表板,其中包含有关数据的各种统计信息,并按指定的时间间隔进行更新
  • 系统应该能够扩展到 Twitter 数据大小

以下图显示了我们的应用架构的第一个版本:

Twitter sentiment analysis application

Twitter 情感架构版本 1

对于版本 1,该应用将完全在单个 Python 笔记本中实现,并将调出 NLP 部分的外部服务。 为了进行扩展,我们当然必须将笔记本外部的某些处理外部化,但是为了进行开发和测试,我发现能够在单个笔记本中包含整个应用可以显着提高生产力。

至于库和框架,我们将使用 Tweepy 将连接到 Twitter,Apache Spark 结构化流,用于处理分布式集群和 Watson Developer Cloud Python SDK 来访问 IBM Watson Natural 语言理解服务。

第 1 部分——使用 Spark 结构化流获取数据

为了获取数据,我们使用 Tweepy,它提供了一个优雅的 Python 客户端库来访问 Twitter API。 Tweepy 涵盖的 API 非常广泛,详细介绍超出了本书的范围,但是您可以在 Tweepy 官方网站上找到完整的 API 参考

您可以使用pip install命令直接从 PyPi 安装 Tweepy 库。 以下命令显示如何使用!指令从笔记本计算机安装它:

!pip install tweepy

注意

注意:当前使用的 Tweepy 版本是 3.6.0。 安装库后,不要忘记重新启动内核。

数据管道的架构图

在我们开始深入研究数据管道的每个组件之前,最好先了解其总体架构并了解计算流程。

如下图所示,我们首先创建一个 Tweepy 流,该流将原始数据写入 CSV 文件。 然后,我们创建一个 Spark Streaming DataFrame,该框架读取 CSV 文件,并定期使用新数据进行更新。 从 Spark StreamingDataFrame中,我们使用 SQL 创建一个 Spark 结构化查询并将其结果存储在 Parquet 数据库中:

Architecture diagram for the data pipeline

流计算流程

通过 Twitter 执行认证

在使用任何 Twitter API 之前,建议先通过系统进行认证。 OAuth 2.0 协议是最常用的认证机制之一,它使第三方应用能够访问网络上的服务。 您需要做的第一件事是获取 OAuth 协议用来验证您身份的一组密钥字符串:

  • 用户密钥:唯一标识客户端应用的字符串(也称为 API 密钥)。
  • 使用者密码:仅应用和 Twitter OAuth 服务器知道的密码字符串。 可以认为它就像一个密码。
  • 访问令牌:使用字符串来验证您的请求。 在授权阶段还可以使用此令牌来确定应用的访问级别。
  • 访问令牌密钥:类似于用户密钥,这是与访问令牌一起发送的秘密字符串,用作密码。

要生成上述密钥字符串,您需要转到这个页面,使用常规的 Twitter 用户 ID 和密码进行认证,然后按照以下步骤操作:

  1. 使用创建新应用按钮创建一个新的 Twitter 应用。

  2. 填写应用详细信息,同意开发者协议,然后单击创建您的 Twitter 应用按钮。

    提示

    注意:请确保您的手机号码已添加到个人资料中,否则在创建 Twitter 应用时会出现错误。

    您可以为 必填网站 的强制输入提供随机 URL,并将 URL 的输入保留为空白,因为这是可选的回调 URL。

  3. 单击密钥和访问令牌选项卡以获取使用者和访问令牌。 您可以随时使用此页面上的按钮重新生成这些令牌。 如果这样做,则还需要更新您的应用代码中的值。

为了更轻松地维护代码,让我们将这些标记放在笔记本顶部的自己的变量中,并创建tweepy.OAuthHandler类,稍后我们将使用它:

from tweepy import OAuthHandler
# Go to http://apps.twitter.com and create an app.
# The consumer key and secret will be generated for you after
consumer_key="XXXX"
consumer_secret="XXXX"

# After the step above, you will be redirected to your app's page.
# Create an access token under the "Your access token" section
access_token="XXXX"
access_token_secret="XXXX"

auth = OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)

创建 Twitter 流

为了实现我们的应用,我们只需要使用此处记录的 Twitter 流 API。 在此步骤中,我们创建一个 Twitter 流,将输入的数据存储到本地文件系统上的 CSV 文件中。 这是使用从tweepy.streaming.StreamListener继承的自定义RawTweetsListener类完成的。 通过覆盖on_data方法,可以完成对传入数据的自定义处理。

在我们的例子中,我们想使用来自标准 Python csv模块的DictWriter将传入的数据从 JSON 转换为 CSV。 由于 Spark Streaming 文件输入源仅在输入目录中创建新文件时才触发,因此我们不能简单地将数据追加到现有文件中。 取而代之的是,我们将数据缓冲到一个数组中,并在缓冲区达到容量后将其写入磁盘。

注意

为简单起见,该实现不包括在处理完文件后清理文件。 此实现的另一个次要限制是,我们当前要等到缓冲区被填满后才能写入文件,如果没有新的推文出现,理论上可能会花费很长时间。

RawTweetsListener的代码如下所示:

from six import iteritems
import json
import csv
from tweepy.streaming import StreamListener
class RawTweetsListener(StreamListener):
    def __init__(self):
        self.buffered_data = []
        self.counter = 0

    def flush_buffer_if_needed(self):
        "Check the buffer capacity and write to a new file if needed"
        length = len(self.buffered_data)
        if length > 0 and length % 10 == 0:
            with open(os.path.join( output_dir,
                "tweets{}.csv".format(self.counter)), "w") as fs:
                self.counter += 1
                csv_writer = csv.DictWriter( fs,
                    fieldnames = fieldnames)
                for data in self.buffered_data:
 csv_writer.writerow(data)
            self.buffered_data = []

    def on_data(self, data):
        def transform(key, value):
            return transforms[key](value) if key in transforms else value

        self.buffered_data.append(
            {key:transform(key,value) \
                 for key,value in iteritems(json.loads(data)) \
                 if key in fieldnames}
        )
        self.flush_buffer_if_needed()
        return True

    def on_error(self, status):
        print("An error occured while receiving streaming data: {}".format(status))
        return False

注意

您可以在此处找到代码文件

从前面的代码中需要注意的重要事项是:

  • 来自 Twitter API 的每条推文都包含大量数据,我们选择继续使用field_metadata变量的字段。 我们还定义了一个全局变量fieldnames,该全局变量保存要从流中捕获的字段列表,而一个transforms变量则包含一个字典,该字典的所有字段名称均具有转换函数作为键,而转换函数本身作为值:

    from pyspark.sql.types import StringType, DateType
    from bs4 import BeautifulSoup as BS
    fieldnames = [f["name"] for f in field_metadata]
    transforms = {
        item['name']:item['transform'] for item in field_metadata if "transform" in item
    }
    field_metadata = [
        {"name": "created_at","type": DateType()},
        {"name": "text", "type": StringType()},
        {"name": "source", "type": StringType(),
             "transform": lambda s: BS(s, "html.parser").text.strip()
        }
    ]
    

    注意

    您可以在此处找到的文件文件

  • CSV 文件写在output_dir中,该文件在其自己的变量中定义。 在开始时,我们首先删除目录及其内容:

    import shutil
    def ensure_dir(dir, delete_tree = False):
        if not os.path.exists(dir):
            os.makedirs(dir)
        elif delete_tree:
            shutil.rmtree(dir)
            os.makedirs(dir)
        return os.path.abspath(dir)
    
    root_dir = ensure_dir("output", delete_tree = True)
    output_dir = ensure_dir(os.path.join(root_dir, "raw"))
    
    

    注意

    您可以在此处找到代码文件

  • field_metadata包含 Spark 数据类型,我们稍后将在创建 Spark 流查询时使用它来构建架构。

  • field_metadata还包含一个可选的变换lambda函数,用于在将值写入磁盘之前清除该值。 作为参考,Python 中的 Lambda 函数是内联定义的匿名函数(请参见这个页面)。 我们将其用于通常作为 HTML 片段返回的源字段。 在此 Lambda 函数中,我们使用 BeautifulSoup 库(在上一章中也使用过)仅提取文本,如以下代码片段所示:

    lambda s: BS(s, "html.parser").text.strip()
    

现在RawTweetsListener已创建,我们定义了start_stream函数,稍后将在 PixieApp 中使用。 此函数将搜索词数组作为输入,并使用filter方法开始新的流:

from tweepy import Stream
def start_stream(queries):
    "Asynchronously start a new Twitter stream"
    stream = Stream(auth, RawTweetsListener())
 stream.filter(track=queries, async=True)
    return stream

注意

注意async=True参数传递给stream.filter。 需要确保该函数不会被阻止,这将阻止我们在笔记本中运行任何其他代码。

您可以在此处找到代码文件

以下代码启动流,该流将接收其中包含单词baseball的推文:

stream = start_stream(["baseball"])

当运行前面的代码时,笔记本计算机中不生成任何输出。 但是,您可以从运行笔记本的路径中看到在输出目录(即../output/raw)中生成的文件(即tweets0.csvtweets1.csv等)。

要停止流,我们只需调用disconnect方法,如下所示:

stream.disconnect()

创建 Spark 流数据框架

参考架构图,下一步是创建一个将output_dir用作源文件输入的 Spark Streaming DataFrame tweets_sdf。 我们可以将 StreamingDataFrame视为一个无界表,其中随着新数据从流到达而不断添加新行。

注意

:Spark 结构化流支持多种类型的输入源,包括文件,Kafka,套接字和速率。 (Socket 和 Rate 均仅用于测试。)

下图取自 Spark 网站,并很好地解释了如何将新数据附加到 Streaming DataFrame

Creating a Spark Streaming DataFrame

流式DataFrame

来源

Spark Streaming Python API 提供了一种优雅的方法,可以使用spark.readStream属性创建 Streaming DataFrame,该属性创建一个新的pyspark.sql.streamingreamReader对象,该对象方便地使您链接方法调用,并具有创建更清晰代码的额外好处(请参见这个页面,以获取有关此模式的更多详细信息)。

例如,要创建 CSV 文件流,我们使用csv调用format方法,链接适用的选项,然后使用目录路径调用load方法:

schema = StructType(
[StructField(f["name"], f["type"], True) for f in field_metadata]
)
csv_sdf = spark.readStream\
    .format("csv")\
 .option("schema", schema)\
    .option("multiline", True)\
 .option("dateFormat", 'EEE MMM dd kk:mm:ss Z y')\
    .option("ignoreTrailingWhiteSpace", True)\
    .option("ignoreLeadingWhiteSpace", True)\
    .load(output_dir)

注意

您可以在此处找到代码文件

spark.readStream还提供了一种方便的高级csv方法,该方法将路径作为选项的第一个参数和关键字参数:

csv_sdf = spark.readStream \
    .csv(
        output_dir,
        schema=schema,
        multiLine = True,
        dateFormat = 'EEE MMM dd kk:mm:ss Z y',
        ignoreTrailingWhiteSpace = True,
        ignoreLeadingWhiteSpace = True
    )

注意

您可以在此处找到代码文件

您可以通过调用应返回trueisStreaming方法来验证csv_sdf``DataFrame确实是流数据帧。 以下代码还向printSchema添加了一个调用,以验证模式是否按预期遵循field_metadata配置:

print(csv_sdf.isStreaming)
csv_sdf.printSchema()

返回值:

root
 |-- created_at: date (nullable = true)
 |-- text: string (nullable = true)
 |-- source: string (nullable = true)

在继续下一步之前,重要的是要了解csv_sdf StreamingDataFrame如何适合结构化流编程模型,以及它具有哪些限制。 Spark 底层 API 定义了弹性分布式数据集RDD)数据结构,该数据结构封装了管理分布式数据的所有底层复杂性。 诸如容错之类的功能(由于任何原因而崩溃的群集节点将在无需开发人员干预的情况下透明地重新启动)由框架自动处理。 RDD 操作有两种类型:转换和操作。 转换是对现有 RDD 的逻辑操作,除非调用某个操作(延迟执行),否则它们不会立即在集群上执行。 转换的输出是新的 RDD。 在内部,Spark 维护一个 RDD 非循环有向图,该图可跟踪导致创建 RDD 的所有沿袭,这在从服务器故障中恢复时非常有用。 示例转换包括mapflatMapfiltersampledistinct。 在DataFrame(内部由 RDD 支持)上进行转换的情况也是如此,这些转换具有包括 SQL 查询的优点。 另一方面,操作不产生其他 RDD,而是对实际的分布式数据执行操作以返回非 RDD 值。 动作的示例包括reducecollectcounttake

如前所述,csv_sdf是一个 Streaming DataFrame,这意味着数据不断被添加到其中,因此我们只能对其进行转换,而不能对其进行操作。 为了解决这个问题,我们必须首先使用作为pyspark.sql.streaming.DataStreamWriter对象的csv_sdf.writeStream创建一个流查询。 流查询负责将结果发送到输出接收器。 然后我们可以使用start()方法运行流式查询。

Spark Streaming 支持多种输出接收器类型:

  • 文件:支持所有经典文件格式,包括 JSON,CSV 和 Parquet
  • Kafka:直接写一个或多个 Kafka 主题
  • Foreach:对集合中的每个元素运行任意计算
  • 控制台:将输出打印到系统控制台(主要用于调试)
  • 存储器:输出存储在存储器中

在的下一部分中,我们将在csv_sdf上创建并运行结构化查询,该查询带有一个输出接收器,该输出接收器以 Parquet 格式存储输出。

创建并运行结构化查询

使用tweets_sdf流式数据帧,我们创建了一个流式查询tweet_streaming_query,该查询使用附加输出模式将数据写入 Parquet 格式。

注意

注意:Spark 流查询支持三种输出模式:完整,其中在每个触发器中写入整个表,附加,其中仅包含自上一个触发器以来的增量行写入,更新,其中仅写入已修改的行。

Parquet 是列式数据库格式,可为分布式分析提供高效,可扩展的存储。 您可以在以下位置找到有关 Parquet 格式的更多信息

以下代码创建并启动tweet_streaming_query流查询:

tweet_streaming_query = csv_sdf \
    .writeStream \
    .format("parquet") \
 .option("path", os.path.join(root_dir, "output_parquet")) \
 .trigger(processingTime="2 seconds") \
 .option("checkpointLocation", os.path.join(root_dir, "output_chkpt")) \
    .start()

注意

您可以在此处找到代码文件

同样,可以使用stop()方法停止流查询,如下所示:

tweet_streaming_query.stop()

在前面的代码中,我们使用path选项指定 Parquet 文件的位置,并使用checkpointLocation指定在服务器故障的情况下将使用的恢复数据的位置。 我们还为从流中读取新数据和要添加到 Parquet 数据库中的新行指定触发间隔。

为了测试,每次output_dir目录中生成新的原始 CSV 文件时,也可以使用console接收器查看正在读取的新行:

tweet_streaming_query = csv_sdf.writeStream\
    .outputMode("append")\
    .format("console")\
    .trigger(processingTime='2 seconds')\
    .start()

注意

您可以在此处找到代码文件

您可以在 Spark 集群的主节点的系统输出中看到结果(您将需要物理访问主节点机器并查看日志文件,因为不幸的是,输出未打印到笔记本本身中,因为日志文件的位置取决于群集管理软件;有关更多信息,请参阅特定的文档)。

以下是显示特定批次的示例结果(标识符已被屏蔽):

-------------------------------------------
Batch: 17
-------------------------------------------
+----------+--------------------+-------------------+
|created_at|                text|             source|
+----------+--------------------+-------------------+
|2018-04-12|RT @XXXXXXXXXXXXX...|Twitter for Android|
|2018-04-12|RT @XXXXXXX: Base...| Twitter for iPhone|
|2018-04-12|That's my roommat...| Twitter for iPhone|
|2018-04-12|He's come a long ...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXX: U s...| Twitter for iPhone|
|2018-04-12|Baseball: Enid 10...|   PushScoreUpdates|
|2018-04-12|Cubs and Sox aren...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXXXX: T...|          RoundTeam|
|2018-04-12|@XXXXXXXX that ri...| Twitter for iPhone|
|2018-04-12|RT @XXXXXXXXXX: S...| Twitter for iPhone|
+----------+--------------------+-------------------+

监视活动的流查询

启动流查询时,Spark 会分配群集资源。 因此,重要的是管理和监视这些查询,以确保不会耗尽群集资源。 您可以随时获取所有正在运行的查询的列表,如以下代码所示:

print(spark.streams.active)

结果:

[<pyspark.sql.streaming.StreamingQuery object at 0x12d7db6a0>, <pyspark.sql.streaming.StreamingQuery object at 0x12d269c18>]

然后,您可以使用以下查询监视属性来深入了解每个查询的详细信息:

  • id:返回查询的唯一标识符,该标识符在从检查点数据重新启动后持续存在
  • runId:返回为当前会话生成的唯一 ID
  • explain():打印查询的详细说明
  • recentProgress:返回最新进度更新的数组
  • lastProgress:返回最新进度

以下代码显示每个活动查询的最新进度:

import json
for query in spark.streams.active:
    print("-----------")
    print("id: {}".format(query.id))
    print(json.dumps(query.lastProgress, indent=2, sort_keys=True))

注意

您可以在此处找到代码文件

第一个查询的结果如下所示:

-----------
id: b621e268-f21d-4eef-b6cd-cb0bc66e53c4
{
  "batchId": 18,
  "durationMs": {
    "getOffset": 4,
    "triggerExecution": 4
  },
  "id": "b621e268-f21d-4eef-b6cd-cb0bc66e53c4",
  "inputRowsPerSecond": 0.0,
  "name": null,
  "numInputRows": 0,
  "processedRowsPerSecond": 0.0,
  "runId": "d2459446-bfad-4648-ae3b-b30c1f21be04",
  "sink": {
    "description": "org.apache.spark.sql.execution.streaming.ConsoleSinkProvider@586d2ad5"
  },
  "sources": [
    {
      "description": "FileStreamSource[file:/Users/dtaieb/cdsdev/notebookdev/Pixiedust/book/Chapter7/output/raw]",
      "endOffset": {
        "logOffset": 17
      },
      "inputRowsPerSecond": 0.0,
      "numInputRows": 0,
      "processedRowsPerSecond": 0.0,
      "startOffset": {
        "logOffset": 17
      }
    }
  ],
  "stateOperators": [],
  "timestamp": "2018-04-12T21:40:10.004Z"
}

作为读者的练习,构建一个 PixieApp 很有用,该 PixieApp 提供一个实时仪表板,其中包含有关每个活动流查询的更新详细信息。

注意

注意:我们将在“第 3 部分——创建实时仪表板 PixieApp”中展示如何构建此 PixieApp。

从 Parquet 文件创建批量DataFrame

注意

注意:对于本章的其余部分,我们将批量 SparkDataFrame定义为经典的 Spark DataFrame,它是非流式的。

此流计算流程的最后一步是创建一个或多个批量DataFrame,我们可以将其用于构建分析和数据可视化。 我们可以认为这最后一步是对数据进行快照以进行更深入的分析。

有两种方法可以通过编程方式从 Parquet 文件中加载批量DataFrame

  • 使用spark.read(注意,我们不像以前那样使用spark.readStream):

    parquet_batch_df = spark.read.parquet(os.path.join(root_dir, "output_parquet"))
    
  • 使用spark.sql

    parquet_batch_df = spark.sql(
    "select * from parquet.'{}'".format(
    os.path.join(root_dir, "output_parquet")
    )
    )
    

    注意

    您可以在此处找到代码文件

这种方法的好处是,我们可以使用任何 ANSI SQL 查询来加载数据,而不是使用在第一种方法中必须使用的等效低级DataFrameAPI。

然后,我们可以通过重新运行前面的代码并重新创建DataFrame来定期刷新数据。 现在,我们准备对数据进行进一步的分析,例如,通过对数据运行 PixieDust display()方法来创建可视化效果:

import pixiedust
display(parquet_batch_df)

我们选择条形图菜单并在字段区域中拖放source字段。 由于我们只想显示前 10 条推文,因此我们在要显示的行数字段中设置此值。 以下屏幕截图显示了 PixieDust 选项对话框:

Creating a batch DataFrame from the Parquet files

用于显示前 10 条推文来源的“选项”对话框

在单击 OK 之后,我们看到以下结果:

Creating a batch DataFrame from the Parquet files

该图表按来源显示与棒球相关的推文数量

在本节中,我们已经看到了如何使用 Tweepy 库创建 Twitter 流,清理原始的数据并将其存储在 CSV 文件中,创建 Spark Streaming DataFrame,对其运行流查询并将其存储在一个 Parquet 数据库中,从 Parquet 文件创建一个批量DataFrame,并使用 PixieDust display()可视化数据。

注意

可在以下位置找到“第 1 部分-使用 Spark 结构化流获取数据”的完整笔记本

在下一部分中,我们将研究使用 IBM Watson Natural Language 了解服务通过情感和实体提取来丰富数据。

第 2 部分——使用情感和最相关的提取实体丰富数据

在这一部分中,我们使用情感信息来丰富 Twitter 数据,例如中性。 我们还希望从推文中提取最相关的实体,例如运动,组织和位置。 这些额外的信息将通过实时仪表板进行分析和可视化,我们将在下一部分中构建。 用于从非结构化文本中提取情感和实体的算法属于计算机科学和人工智能领域,称为自然语言处理NLP)。 网络上有很多教程,其中提供了有关如何提取情感的算法示例。 例如,可以在这个页面

但是,对于此示例应用,我们将不会构建自己的 NLP 算法。 相反,我们将选择一种基于云的服务,该服务提供文本分析,例如情感和实体提取。 当您有通用要求(例如不需要训练定制模型)时,这种方法非常有效,但是即使那样,大多数服务提供商现在都提供了实现此目的的工具。 与创建您自己的模型相比,使用基于云的供应商具有主要优势,例如节省了开发时间以及更好的准确率和性能。 通过一个简单的 REST 调用,我们将能够生成所需的数据并将其集成到我们的应用流中。 而且,如果需要,更改提供者将非常容易,因为负责与服务接口的代码已被很好地隔离了。

对于此示例应用,我们将使用 IBM Watson 自然语言理解NLU)服务,该服务是 IBM Watson 认知服务家族的一部分,可在 IBM Cloud 上使用。

IBM Watson Natural 语言理解服务入门

对于每个云提供商,提供新服务的过程通常是相同的。 登录后,您将转到服务目录页面,可在其中搜索特定服务。

要登录到 IBM Cloud,只需转到这个页面并创建一个免费的 IBM 帐户(如果您还没有)。 进入仪表板后,可以通过多种方式搜索 IBM Watson NLU 服务:

  • 单击左上方的菜单,然后选择 Watson,选择浏览服务,然后在服务列表中找到自然语言理解条目。

  • 单击右上角的“创建资源”按钮进入目录。 进入目录后,您可以在搜索栏中搜索“自然语言理解”,如以下屏幕截图所示:

    Getting started with the IBM Watson Natural Language Understanding service

    在服务目录中搜索 Watson NLU

然后,您可以单击自然语言理解来设置新实例。 云提供商通常会为某些服务提供免费或基于试用的计划,这并不稀奇,幸运的是 Watson NLU 提供了其中一种服务,其局限性在于您只能训练一个自定义模型,每个模型最多可处理 30,000 个 NLU 项目。 月(足够用于我们的示例应用)。 选择 Lite(免费)计划并单击创建按钮后,新配置的实例将出现在仪表板上并准备接受请求。

注意

注意:创建服务后,您可能会重定向到 NLU 服务入门文档。 如果是这样,只需导航回到仪表板,您将在其中看到列出的新服务实例。

下一步是通过进行 REST 调用来测试我们笔记本的服务。 每个服务都提供有关如何使用它的详细文档,包括 API 参考。 在笔记本中,我们可以根据 API 参考使用 request 包进行 GET,POST,PUT 或 DELETE 调用,但是强烈建议检查该服务是否为 SDK 提供了对 API 的高级编程访问。

幸运的是,IBM Watson 提供了watson_developer_cloud开源库,其中包括多个开源 SDK,这些 SDK 支持某些最受欢迎的语言,包括 Java,Python 和 Node.js。 对于本项目,我们将使用 Python SDK 和位于此处的源代码和代码示例

以下pip命令直接从 Jupyter 笔记本安装watson_developer_cloud包:

!pip install Watson_developer_cloud

注意

注意命令前面的!表示它是一个 shell 命令。

注意:安装完成后,不要忘记重新启动内核。

大多数云服务提供商都使用一种通用模式来让消费者通过该服务进行认证,该模式包括从将嵌入在客户端应用中的服务控制台仪表板生成一组凭据。 要生成凭证,只需单击 Watson NLU 实例的服务凭证选项卡,然后单击新凭证按钮。

这将以 JSON 格式生成一组新的凭据,如以下屏幕截图所示:

Getting started with the IBM Watson Natural Language Understanding service

为 Watson NLU 服务生成新的凭证

现在我们有了服务的凭据,我们可以创建一个NaturalLanguageUnderstandingV1对象,该对象将提供对 REST API 的编程访问,如以下代码所示:

from watson_developer_cloud import NaturalLanguageUnderstandingV1
from watson_developer_cloud.natural_language_understanding_v1 import Features, SentimentOptions, EntitiesOptions

nlu = NaturalLanguageUnderstandingV1(
    version='2017-02-27',
    username='XXXX',
    password='XXXX'
)

注意

您可以在此处找到代码文件

注意:在前面的代码中,用服务凭据中的相应用户名和密码替换XXXX文本。

version参数是指 API 的特定版本。 要了解最新版本,请转到位于此处的官方文档页面

在继续构建应用之前,让我们花一点时间来了解 Watson Natural Language 服务提供的文本分析功能,其中包括:

  • 情感
  • 实体
  • 概念
  • 类别目录
  • 感情
  • 关键词
  • 关系
  • 语义角色

在我们的应用中,丰富 Twitter 数据发生在RawTweetsListener中,我们在其中创建了一个enrich方法,该方法将从on_data处理器方法中调用。 在此方法中,我们使用 Twitter 数据和仅包含情感和实体的特征列表调用nlu.analyze方法,如以下代码所示:

注意

注意[[RawTweetsListener]]表示这意味着以下代码是名为RawTweetsListener的类的一部分,并且用户不应尝试在没有完整类的情况下照常运行代码。 始终可以始终参考完整的笔记本作为参考。

[[RawTweetsListener]]
def enrich(self, data):
    try:
        response = nlu.analyze(
 text = data['text'],
 features = Features(
 sentiment=SentimentOptions(),
 entities=EntitiesOptions()
 )
 )
        data["sentiment"] = response["sentiment"]["document"]["label"]
        top_entity = response["entities"][0] if len(response["entities"]) > 0 else None
        data["entity"] = top_entity["text"] if top_entity is not None else ""
        data["entity_type"] = top_entity["type"] if top_entity is not None else ""
        return data
    except Exception as e:
 self.warn("Error from Watson service while enriching data: {}".format(e))

注意

您可以在此处找到代码文件

然后将结果存储在data对象中,该对象将被写入 CSV 文件。 我们还防止意外的异常跳过当前的推文并记录警告消息,而不是让异常冒泡,这会阻止 Twitter 流。

注意

注意:当推文数据使用服务不支持的语言时,会发生最常见的异常。

我们使用第 5 章,“最佳实践和高级 PixieDust 概念”中描述的@Logger装饰器将消息记录到 PixieDust 日志记录框架中。 提醒一下,您可以使用另一个单元格中的%pixiedustLog魔术来查看日志消息。

我们仍然需要更改架构元数据以包括以下新字段:

field_metadata = [
    {"name": "created_at", "type": DateType()},
    {"name": "text", "type": StringType()},
    {"name": "source", "type": StringType(),
         "transform": lambda s: BS(s, "html.parser").text.strip()
    },
 {"name": "sentiment", "type": StringType()},
 {"name": "entity", "type": StringType()},
 {"name": "entity_type", "type": StringType()}
]

注意

您可以在此处找到代码文件

最后,我们更新on_data处理器以调用enrich方法,如下所示:

def on_data(self, data):
    def transform(key, value):
        return transforms[key](value) if key in transforms else value
    data = self.enrich(json.loads(data))
 if data is not None:
        self.buffered_data.append(
            {key:transform(key,value) \
                for key,value in iteritems(data) \
                if key in fieldnames}
        )
        self.flush_buffer_if_needed()
    return True

注意

您可以在此处找到代码文件

当我们重新启动 Twitter 流并创建 Spark StreamingDataFrame时,我们可以使用以下代码来验证我们是否具有正确的架构:

schema = StructType(
    [StructField(f["name"], f["type"], True) for f in field_metadata]
)
csv_sdf = spark.readStream \
    .csv(
        output_dir,
        schema=schema,
        multiLine = True,
        dateFormat = 'EEE MMM dd kk:mm:ss Z y',
        ignoreTrailingWhiteSpace = True,
        ignoreLeadingWhiteSpace = True
    )
csv_sdf.printSchema()

注意

您可以在此处找到代码文件

哪个显示了以下预期结果:

root
 |-- created_at: date (nullable = true)
 |-- text: string (nullable = true)
 |-- source: string (nullable = true)
 |-- sentiment: string (nullable = true)
 |-- entity: string (nullable = true)
 |-- entity_type: string (nullable = true)

同样,当我们使用console接收器运行结构化查询时,数据将在 Spark 主节点的控制台中分批显示,如下所示:

-------------------------------------------
Batch: 2
-------------------------------------------
+----------+---------------+---------------+---------+------------+-------------+
|created_at|           text|         source|sentiment|      entity|  entity_type|
+----------+---------------+---------------+---------+------------+-------------+
|2018-04-14|Some little ...| Twitter iPhone| positive|        Drew|       Person|d
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone|  neutral| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone|  neutral|    baseball|        Sport|
|2018-04-14|RT @XXXXXXXX...| Twitter Client|  neutral| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXXXXX...| Twitter Client| positive| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXX: I...|Twitter Android| positive| Greg XXXXXX|       Person|
|2018-04-14|RT @XXXXXXXX...| Twitter iPhone| positive| @XXXXXXXXXX|TwitterHandle|
|2018-04-14|RT @XXXXX: I...|Twitter Android| positive| Greg XXXXXX|       Person|
|2018-04-14|Congrats to ...|Twitter Android| positive|    softball|        Sport|
|2018-04-14|translation:...| Twitter iPhone|  neutral|        null|         null|
+----------+---------------+---------------+---------+------------+-------------+

最后,我们使用 Parquet output接收器运行结构化查询,创建一个批量DataFrame,并使用 PixieDust display()浏览数据以显示例如按情感分类的推文数(positivenegativeneutral)由实体聚类,如下图所示:

Getting started with the IBM Watson Natural Language Understanding service

条形图显示按实体分组的按情感分类的推文数量

注意

“第 2 部分——使用情感丰富数据”的完整笔记本

如果您正在运行它,建议您通过向架构中添加更多字段,运行不同的 SQL 查询并使用 PixieDust display()可视化数据来进行试验。

在下一部分中,我们将构建一个仪表板,以显示有关 Twitter 数据的多个指标。

第 3 部分——创建实时仪表板 PixieApp

与往常一样,我们首先需要定义仪表板的 MVP 版本的要求。 这次,我们将从敏捷方法中借用一个称为用户故事的工具,其中从用户的角度描述了我们要从构建的功能。 敏捷方法论还规定,通过将不同用户分类为角色,可以充分理解将与软件交互的不同用户的上下文。 在我们的案例中,我们只会使用一种角色:市场总监 Frank,他希望从消费者在社交媒体上谈论的内容中获得实时见解。

用户的故事是这样的:

  • 弗兰克(Frank)输入搜索查询,例如产品名称
  • 然后显示一个仪表板,该仪表板显示一组图表,这些图表显示有关用户情感(积极,消极,中立)的指标
  • 仪表板还包含推文中要说出的所有实体的词云
  • 此外,仪表板具有一个选项,可以显示当前活动的所有 Spark Streaming 查询的实时进度

注意

注意:Frank 确实不需要最后一个功能,但是无论如何我们在此展示它作为前面给出的练习的示例实现。

将分析重构为自己的方法

在开始之前,我们需要重构启动 Twitter 流的代码并将 Spark StreamingDataFrame创建为将在 PixieApp 中调用的自己的方法。

start_stream, start_streaming_dataframestart_parquet_streaming_query方法如下:

def start_stream(queries):
    "Asynchronously start a new Twitter stream"
    stream = Stream(auth, RawTweetsListener())
    stream.filter(track=queries, languages=["en"], async=True)
    return stream

注意

您可以在此处找到代码文件

def start_streaming_dataframe(output_dir):
    "Start a Spark Streaming DataFrame from a file source"
    schema = StructType(
        [StructField(f["name"], f["type"], True) for f in field_metadata]
    )
    return spark.readStream \
        .csv(
            output_dir,
            schema=schema,
            multiLine = True,
            timestampFormat = 'EEE MMM dd kk:mm:ss Z yyyy',
            ignoreTrailingWhiteSpace = True,
            ignoreLeadingWhiteSpace = True
        )

注意

您可以在此处找到代码文件

def start_parquet_streaming_query(csv_sdf):
    """
    Create and run a streaming query from a Structured DataFrame
    outputing the results into a parquet database
    """
    streaming_query = csv_sdf \
      .writeStream \
      .format("parquet") \
      .option("path", os.path.join(root_dir, "output_parquet")) \
      .trigger(processingTime="2 seconds") \
      .option("checkpointLocation", os.path.join(root_dir, "output_chkpt")) \
      .start()
    return streaming_query

注意

您可以在此处找到代码文件

在准备工作的中,我们还需要管理 PixieApp 将创建的不同流的生命周期,并确保在用户重新启动仪表板时正确​​停止了基础资源。 为了解决这个问题,我们创建了一个StreamsManager类,该类封装了 Tweepy twitter_stream和 CSV Streaming DataFrame。 此类具有reset方法,该方法将停止twitter_stream,停止所有活动的流查询,删除从先前查询创建的所有输出文件,并使用新的查询字符串开始一个新的输出文件。 如果在没有查询字符串的情况下调用reset方法,则我们不会启动新的流。

我们还创建了一个全局streams_manager实例,即使重新启动仪表板,该实例也将跟踪当前状态。 由于用户可以重新运行包含全局streams_manager的单元格,因此我们需要确保在删除当前全局实例的时自动调用reset方法。 为此,我们覆盖了对象的__del__方法,这是 Python 实现析构器并调用reset的方法。

StreamsManager的代码如下所示:

class StreamsManager():
    def __init__(self):
        self.twitter_stream = None
        self.csv_sdf = None

    def reset(self, search_query = None):
        if self.twitter_stream is not None:
            self.twitter_stream.disconnect()
        #stop all the active streaming queries and re_initialize the directories
        for query in spark.streams.active:
            query.stop()
        # initialize the directories
        self.root_dir, self.output_dir = init_output_dirs()
        # start the tweepy stream
        self.twitter_stream = start_stream([search_query]) if search_query is not None else None
        # start the spark streaming stream
        self.csv_sdf = start_streaming_dataframe(output_dir) if search_query is not None else None

 def __del__(self):
 # Automatically called when the class is garbage collected
 self.reset()

streams_manager = StreamsManager()

注意

您可以在此处找到代码文件

创建 PixieApp

就像第 6 章,“使用 TensorFlow 的图像识别”一样,我们将再次使用TemplateTabbedApp类创建带有两个 PixieApps 的标签布局:

  • TweetInsightApp:让用户指定查询字符串并显示与之关联的实时仪表板
  • StreamingQueriesApp:监视活动的结构化查询的进度

TweetInsightApp的默认路由中,我们返回一个片段,该片段向用户询问查询字符串,如下所示:

from pixiedust.display.app import *
@PixieApp
class TweetInsightApp():
    @route()
    def main_screen(self):
        return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
          <input id="query{{prefix}}" type="text" class="form-control"
              value=""
              placeholder="Enter a search query (e.g. baseball)">
          <span class="input-group-btn">
            <button class="btn btn-default" type="button"
 pd_options="search_query=$val(query{{prefix}})">
                Go
            </button>
          </span>
        </div>
    </div>
</div>
        """

TweetInsightApp().run()

注意

您可以在此处找到代码文件

以下屏幕截图显示了运行上述代码的结果:

注意

注意:我们将创建具有选项卡式布局的主TwitterSentimentApp PixieApp,并在本节稍后部分中包含此类。 目前,我们仅显示TweetInsightApp子应用。

Creating the PixieApp

Twitter 情感仪表板的欢迎屏幕

Go按钮中,我们使用用户提供的查询字符串调用search_query路由。 在此路由中,我们首先启动各种流,并从 Parquet 数据库所在的输出目录中创建一个存储在名为parquet_df的类变量中的批量DataFrame。 然后我们返回由三个小部件组成的 HTML 片段,其中显示了以下指标:

  • 实体聚集的三种情感中的每一种的条形图
  • 折线子图按情感分布显示了的推文
  • 实体的词云

每个小部件都使用第 5 章,“最佳做法和高级 PixieDust 概念”中记录的pd_refresh_rate属性,定期调用特定的路由。 我们还确保重新加载parquet_df变量以获取自上次以来到达的新数据。 然后在pd_entity属性中引用此变量以显示图表。

以下代码显示了search_query路由的实现:

import time
[[TweetInsightApp]]
@route(search_query="*")
    def do_search_query(self, search_query):
        streams_manager.reset(search_query)
        start_parquet_streaming_query(streams_manager.csv_sdf)
 while True:
 try:
 parquet_dir = os.path.join(root_dir,
 "output_parquet")
 self.parquet_df = spark.sql("select * from parquet.'{}'".format(parquet_dir))
 break
 except:
 time.sleep(5)
        return """
<div class="container">
 <div id="header{{prefix}}" class="row no_loading_msg"
 pd_refresh_rate="5000" pd_target="header{{prefix}}">
 <pd_script>
print("Number of tweets received: {}".format(streams_manager.twitter_stream.listener.tweet_count))
 </pd_script>
 </div>
    <div class="row" style="min-height:300px">
        <div class="col-sm-5">
            <div id="metric1{{prefix}}" pd_refresh_rate="10000"
                class="no_loading_msg"
                pd_options="display_metric1=true"
                pd_target="metric1{{prefix}}">
            </div>
        </div>
        <div class="col-sm-5">
            <div id="metric2{{prefix}}" pd_refresh_rate="12000"
                class="no_loading_msg"
                pd_options="display_metric2=true"
                pd_target="metric2{{prefix}}">
            </div>
        </div>
    </div>

    <div class="row" style="min-height:400px">
        <div class="col-sm-offset-1 col-sm-10">
            <div id="word_cloud{{prefix}}" pd_refresh_rate="20000"
                class="no_loading_msg"
                pd_options="display_wc=true"
                pd_target="word_cloud{{prefix}}">
            </div>
        </div>
    </div>
        """

注意

您可以在此处找到代码文件

从前面的代码中有多个注意事项:

  • 当我们尝试加载parquet_df批量DataFrame时,Parquet 文件的输出目录可能未准备好,这会导致异常。 为了解决此时序问题,我们将代码包装到try...except语句中,并使用time.sleep(5)等待 5 秒钟。

  • 我们还将在标题中显示当前的推文计数。 为此,我们添加一个<div>元素,该元素每 5 秒刷新一次,并添加一个<pd_script>,该元素使用streams_manager.twitter_stream.listener.tweet_count来打印当前的推文计数,该变量是我们添加到 RawTweetsListener类。 我们还更新了on_data()方法,以在每次有新的推文到达时增加tweet_count变量,如以下代码所示:

    [[TweetInsightApp]]
    def on_data(self, data):
            def transform(key, value):
                return transforms[key](value) if key in transforms else value
            data = self.enrich(json.loads(data))
            if data is not None:
     self.tweet_count += 1
                self.buffered_data.append(
                    {key:transform(key,value) \
                         for key,value in iteritems(data) \
                         if key in fieldnames}
                )
                self.flush_buffer_if_needed()
            return True
    

    另外,为避免闪烁,我们防止在<div>元素中使用class="no_loading_msg"显示加载微调器图像。

  • 我们调用三个不同的路由(display_metric1display_metric2display_wc),分别显示三个小部件。

    display_metric1display_metric2路由非常相似。 他们返回一个带有parquet_df作为pd_entity的 DIV 和一个自定义<pd_options>子元素,该子元素包含传递给 PixieDust display()层的 JSON 配置。

以下代码显示了display_metric1路由的实现:

[[TweetInsightApp]]
@route(display_metric1="*")
    def do_display_metric1(self, display_metric1):
        parquet_dir = os.path.join(root_dir, "output_parquet")
        self.parquet_df = spark.sql("select * from parquet.'{}'".format(parquet_dir))
        return """
<div class="no_loading_msg" pd_render_onload pd_entity="parquet_df">
    <pd_options>
    {
      "legend": "true",
      "keyFields": "sentiment",
      "clusterby": "entity_type",
      "handlerId": "barChart",
      "rendererId": "bokeh",
      "rowCount": "10",
      "sortby": "Values DESC",
      "noChartCache": "true"
    }
    </pd_options>
</div>
        """

注意

您可以在此处找到代码文件

display_metric2路由遵循相似的模式,但具有pd_options属性不同的集合。

最后一条路由是display_wc,负责为实体显示单词 cloud。 此路由使用wordcloud Python 库,您可以通过以下命令进行安装:

!pip install wordcloud

注意

注意:与往常一样,安装完成后,不要忘记重新启动内核。

我们使用第 5 章,“最佳做法和高级 PixieDust 概念中记录的@captureOutput装饰器,如下所示:

import matplotlib.pyplot as plt
from wordcloud import WordCloud

[[TweetInsightApp]]
@route(display_wc="*")
@captureOutput
def do_display_wc(self):
    text = "\n".join(
 [r['entity'] for r in self.parquet_df.select("entity").collect() if r['entity'] is not None]
 )
    plt.figure( figsize=(13,7) )
    plt.axis("off")
    plt.imshow(
        WordCloud(width=750, height=350).generate(text),
        interpolation='bilinear'
    )

注意

您可以在此处找到代码文件

传递给WordCloud类的文本是通过收集parquet_df批量DataFrame中的所有实体而生成的。

以下屏幕快照显示了让通过搜索查询baseball创建的 Twitter 流运行一段时间后的仪表板:

Creating the PixieApp

Twitter 情感仪表板的搜索查询“棒球”

第二个 PixieApp 用于监视正在运行的流查询。 主路由返回一个 HTML 片段,该片段具有一个<div>元素,该元素以固定间隔(5000 ms)调用show_progress路由,如以下代码所示:

@PixieApp
class StreamingQueriesApp():
    @route()
    def main_screen(self):
        return """
<div class="no_loading_msg" pd_refresh_rate="5000" pd_options="show_progress=true">
</div>
        """

注意

您可以在此处找到代码文件

show_progress路由中,我们使用本章前面介绍的query.lastProgress监视 API,使用 Jinja2 {%for%}循环遍历 JSON 对象,并在表中显示结果,如以下代码所示:

@route(show_progress="true")
    def do_show_progress(self):
        return """
{%for query in this.spark.streams.active%}
    <div>
    <div class="page-header">
        <h1>Progress Report for Spark Stream: {{query.id}}</h1>
    <div>
    <table>
        <thead>
          <tr>
             <th>metric</th>
             <th>value</th>
          </tr>
        </thead>
        <tbody>
 {%for key, value in query.lastProgress.items()%}
 <tr>
 <td>{{key}}</td>
 <td>{{value}}</td>
 </tr>
 {%endfor%}
        </tbody>
    </table>
{%endfor%}
        """

注意

您可以在此处找到代码文件

以下屏幕快照显示了监视 PixieApp 的流查询:

Creating the PixieApp

实时监控活动的 Spark 流查询

的最后一步是使用TemplateTabbedApp类将整个应用组合在一起,如以下代码所示:

from pixiedust.display.app import *
from pixiedust.apps.template import TemplateTabbedApp

@PixieApp
class TwitterSentimentApp(TemplateTabbedApp):
    def setup(self):
 self.apps = [
 {"title": "Tweets Insights", "app_class": "TweetInsightApp"},
 {"title": "Streaming Queries", "app_class": "StreamingQueriesApp"}
 ]

app = TwitterSentimentApp()
app.run()

注意

您可以在此处找到代码文件

示例应用的第 3 部分现已完成; 您可以在这里找到完整的笔记本:

### 注意

在下一部分中,我们将讨论通过使用 Apache Kafka 进行事件流传输和使用 IBM Streams Designer 进行流数据的数据丰富化,从而使应用的数据管道更具可伸缩性的方法。

第 4 部分——使用 Apache Kafka 和 IBM Streams Designer 添加可伸缩性

注意

注意:此部分是可选的。 它演示了如何使用基于云的流服务重新实现部分数据管道,以实现更大的可扩展性

在单个笔记本电脑中实现整个数据管道可在开发和测试过程中提高生产率。 我们可以使用代码对进行试验,并以非常小的占位面积非常快速地测试更改。 此外,由于我们一直在处理相对少量的数据,因此性能合理。 但是,很明显,我们不会在生产中使用这种架构,接下来我们要问自己的一个问题是,随着来自 Twitter 的流数据数量的急剧增加,瓶颈将阻止应用扩展。

在本节中,我们确定了两个需要改进的地方:

  • 在 Tweepy 流中,传入的数据被发送到RawTweetsListener实例,以使用on_data方法进行处理。 我们需要确保在这种方法中花费尽可能少的时间,否则随着传入数据量的增加,系统将落在后面。 在当前的实现中,通过对 Watson NLU 服务进行外部调用来同步丰富数据。 然后将其缓冲并最终写入磁盘。 为了解决此问题,我们将数据发送到 Kafka 服务,该服务是一种高度可扩展的容错流平台,使用发布/订阅模式来处理大量数据。 我们还使用 Streaming Analytics 服务,该服务将使用来自 Kafka 的数据并通过调用 Watson NLU 服务来丰富数据。 两种服务都可以在 IBM Cloud 上使用。

    注意

    注意:我们可以使用其他开源代码框架来处理流数据,例如 Apache FlinkApache Storm

  • 在当前实现中,数据存储为 CSV 文件,然后我们以输出目录作为源创建一个 Spark Streaming DataFrame。 此步骤会消耗笔记本电脑和本地环境上的时间和资源。 取而代之的是,我们可以让 Streaming Analytics 写回另一个主题中的丰富事件,并创建一个以 Message Hub 服务作为 Kafka 输入源的 Spark Streaming DataFrame

以下图显示了示例应用的更新架构:

Part 4 – Adding scalability with Apache Kafka and IBM Streams Designer

使用 Kafka 和 Streams Designer 扩展架构

在接下来的几节中,我们将实现更新的架构,首先是将推文流式传输到 Kafka。

将原始推文流式传输到 Kafka

在 IBM Cloud 上配置 Kafka/Message Hub 服务实例的方式与相同,与我们用于配置 Watson NLU 服务的步骤相同。 我们首先在目录中找到并选择服务,选择价格计划,然后单击创建。 然后,我们打开服务仪表板并选择服务凭据选项卡以创建新的凭据,如以下屏幕截图所示:

Streaming the raw tweets to Kafka

为 Message Hub 服务创建新的凭据

与 IBM Cloud 上所有可用服务的情况一样,凭证以 JSON 对象的形式出现,我们需要将其存储在笔记本中其自己的变量中,如以下代码所示(同样,请不要忘记将XXXX文本替换为服务凭据中的用户名和密码):

message_hub_creds = {
  "instance_id": "XXXXX",
  "mqlight_lookup_url": "https://mqlight-lookup-prod02.messagehub.services.us-south.bluemix.net/Lookup?serviceId=XXXX",
  "api_key": "XXXX",
  "kafka_admin_url": "https://kafka-admin-prod02.messagehub.services.us-south.bluemix.net:443",
  "kafka_rest_url": "https://kafka-rest-prod02.messagehub.services.us-south.bluemix.net:443",
  "kafka_brokers_sasl": [
    "kafka03-prod02.messagehub.services.us-south.bluemix.net:9093",
    "kafka01-prod02.messagehub.services.us-south.bluemix.net:9093",
    "kafka02-prod02.messagehub.services.us-south.bluemix.net:9093",
    "kafka05-prod02.messagehub.services.us-south.bluemix.net:9093",
    "kafka04-prod02.messagehub.services.us-south.bluemix.net:9093"
  ],
  "user": "XXXX",
  "password": "XXXX"
}

注意

您可以在此处找到代码文件

至于与 Kafka 的接口,我们可以在多个优质客户端库之间进行选择。 我已经尝试了很多,但是我最常使用的是kafka-python,它的优点是可以使用纯 Python 实现,因此更易于安装。

要从笔记本计算机安装它,请使用以下命令:

!pip install kafka-python

注意

注意:与往常一样,在安装任何库之后,请不要忘记重启内核。

kafka-python库提供了一个KafkaProducer类,用于将数据作为消息写入服务中,我们需要使用我们先前创建的凭据对其进行配置。 有多个可用的 Kafka 配置选项,所有这些配置选项都超出了本书的范围。 必需的选项与认证,主机服务器和 API 版本有关。

以下代码在RawTweetsListener类的__init__构造器中实现。 它创建一个KafkaProducer实例并将其存储为类变量:

[[RawTweetsListener]]
context = ssl.create_default_context()
context.options &= ssl.OP_NO_TLSv1
context.options &= ssl.OP_NO_TLSv1_1
kafka_conf = {
    'sasl_mechanism': 'PLAIN',
    'security_protocol': 'SASL_SSL',
    'ssl_context': context,
    "bootstrap_servers": message_hub_creds["kafka_brokers_sasl"],
    "sasl_plain_username": message_hub_creds["user"],
    "sasl_plain_password": message_hub_creds["password"],
    "api_version":(0, 10, 1),
    "value_serializer" : lambda v: json.dumps(v).encode('utf-8')
}
self.producer = KafkaProducer(**kafka_conf)

注意

您可以在此处找到代码文件

我们为value_serializer键配置了一个 lambda 函数,该函数将序列化 JSON 对象,这是我们将用于数据的格式。

注意

注意:我们需要指定api_version键,因为否则,该库将尝试自动发现其值,这会导致kafka-python库中的错误可重现,从而引发NoBrokerAvailable异常。 仅在 Mac 上。 在编写本书时,尚未提供针对此错误的修复程序。

现在我们需要更新on_data方法,以使用tweets主题将推文数据发送到 Kafka。 Kafka 主题就像应用可以发布或订阅的渠道。 重要的是在尝试写入主题之前已经创建了该主题,否则将引发异常。 这是通过以下ensure_topic_exists方法完成的:

import requests
import json

def ensure_topic_exists(topic_name):
    response = requests.post(
 message_hub_creds["kafka_rest_url"] +
 "/admin/topics",
 data = json.dumps({"name": topic_name}),
 headers={"X-Auth-Token": message_hub_creds["api_key"]}
 )
    if response.status_code != 200 and \
       response.status_code != 202 and \
       response.status_code != 422 and \
       response.status_code != 403:
        raise Exception(response.json())

注意

您可以在此处找到代码文件

在前面的代码中,我们使用包含要创建的主题名称的 JSON 有效负载向路径/admin/topic发出 POST 请求。 必须使用凭据和X-Auth-Token标头中提供的 API 密钥对请求进行认证。 我们也确保忽略表示该主题已存在的 HTTP 错误代码 422 和 403。

现在,on_data方法的代码看起来更加简单,如下所示:

[[RawTweetsListener]]
def on_data(self, data):
    self.tweet_count += 1
 self.producer.send(
 self.topic,
 {key:transform(key,value) \
 for key,value in iteritems(json.loads(data)) \
 if key in fieldnames}
 )
    return True

注意

您可以在此处找到代码文件

如我们所见,使用此新代码,我们在on_data方法上花费了尽可能少的时间,这是我们想要实现的目标。 这些推文数据现在正在流入 Kafka tweets主题,准备通过 Streaming Analytics 服务进行充实,我们将在下一部分中进行讨论。

通过 Streaming Analytics 服务丰富推文数据

对于这一步,我们将需要使用 Watson Studio,它是一个基于云的集成 IDE,可提供各种用于处理数据的工具,包括机器学习/深度学习模型,Jupyter 笔记本,数据流等。 Watson Studio 是 IBM Cloud 的配套工具,可通过这个页面访问,因此无需额外注册。

登录到 Watson Studio 后,我们将创建一个新项目,我们将其称为Thoughtful Data Science

注意

注意:创建项目时可以选择默认选项。

然后,我们转到设置选项卡以创建 Streaming Analytics 服务,该服务将成为驱动我们丰富过程并将其与项目关联的引擎。 请注意,我们也可以像本章中使用的其他服务一样在 IBM Cloud 目录中创建服务,但是由于我们仍然必须将其与项目关联,因此我们也可以在 Watson Studio 中进行创建。

设置选项卡中,滚动到关联服务部分,然后单击添加服务下拉列表以选择流式分析。 在下一页中,可以在现有新建之间进行选择。 选择新建,然后按照以下步骤创建服务。 完成后,新创建的服务应与项目关联,如以下屏幕截图所示:

注意

注意:如果有多个可用选项,则可以选择其中任何一个。

Enriching the tweets data with the Streaming Analytics service

将 Streaming Analytics 服务与项目相关联

现在我们准备创建定义我们的推文数据的丰富处理的流。

我们转到资产选项卡,向下滚动到流式流部分,然后单击新流式流按钮。 在下一页中,我们提供一个名称,选择 Streaming Analytics 服务,手动选择,然后单击创建按钮。

我们现在是 Streams Designer 中的,它由左侧的一组运算符和一个画布组成,我们可以在其中以图形方式构建流。 对于我们的示例应用,我们需要从面板中选择三个运算符并将其拖放到画布中:

  • 面板的“源”部分中的消息中心:数据的输入源。 进入画布后,我们将其重命名为Source Message Hub(通过双击它进入编辑模式)。
  • 处理和分析部分的代码:它将包含调用 Watson NLU 服务的数据丰富 Python 代码。 我们将运算符重命名为Enrichment
  • 面板的目标部分中的 消息中心:丰富数据的输出源。 我们将其重命名为Target Message Hub

接下来,我们在源消息中心扩展之间以及扩展目标消息中心之间创建连接。 要在两个运算符之间建立连接,只需抓住第一个运算符末端的输出端口并将其拖到另一个运算符的输入端口即可。 请注意,源运算符在框的右侧仅具有一个输出端口,以表示它仅支持传出连接,而目标运算符在左侧仅具有一个输入端口,以表示仅支持传入连接。 处理和分析部分中的任何运算符在左侧和右侧都有两个端口,因为它们都接受传入和传出连接。

以下屏幕截图显示了完整完成的画布:

Enriching the tweets data with the Streaming Analytics service

推特浓缩流

现在,让我们看一下这三个运算符的配置。

注意

注意:要完成本节,请确保运行为上一节中讨论的 Message Hub 实例生成主题的代码。 否则,Message Hub 实例将为空,并且不会检测到任何架构。

单击源消息中心。 随即出现右侧的动画窗格,其中包含用于选择包含推文的 Message Hub 实例的选项。 第一次,您需要创建与 Message Hub 实例的连接。 选择tweets作为主题。 单击编辑输出模式,然后单击检测模式,以从数据中自动填充该模式。 您还可以使用展示预览按钮预览实时流数据,如以下屏幕截图所示:

Enriching the tweets data with the Streaming Analytics service

设置架构并预览实时流数据

现在,选择代码运算符,以实现调用 Watson NLU 的代码。 动画的上下文右侧窗格包含一个 Python 代码编辑器,该代码编辑器带有样板代码,其中包含要实现的必需函数,即init(state)process(event, state)

init方法中,我们实例化NaturalLanguageUnderstandingV1实例,如以下代码所示:

import sys
from watson_developer_cloud import NaturalLanguageUnderstandingV1
from watson_developer_cloud.natural_language_understanding_v1 import Features, SentimentOptions, EntitiesOptions

# init() function will be called once on pipeline initialization
# @state a Python dictionary object for keeping state. The state object is passed to the process function
def init(state):
    # do something once on pipeline initialization and save in the state object
 state["nlu"] = NaturalLanguageUnderstandingV1(
 version='2017-02-27',
 username='XXXX',
 password='XXXX'
 )

注意

您可以在此处找到代码文件

注意:我们需要通过位于右侧上下文窗格中 Python 编辑器窗口上方的 Python 包链接安装Watson_developer_cloud库,如以下屏幕截图所示:

Enriching the tweets data with the Streaming Analytics service

watson_cloud_developer包添加到流中

在每个事件数据上调用过程方法。 我们使用它来调用 Watson NLU 并将额外的信息添加到事件对象中,如以下代码所示:

# @event a Python dictionary object representing the input event tuple as defined by the input schema
# @state a Python dictionary object for keeping state over subsequent function calls
# return must be a Python dictionary object. It will be the output of this operator.
# Returning None results in not submitting an output tuple for this invocation.
# You must declare all output attributes in the Edit Schema window.
def process(event, state):
    # Enrich the event, such as by:
    # event['wordCount'] = len(event['phrase'].split())
    try:
        event['text'] = event['text'].replace('"', "'")
 response = state["nlu"].analyze(
 text = event['text'],
 features=Features(sentiment=SentimentOptions(), entities=EntitiesOptions())
 )
        event["sentiment"] = response["sentiment"]["document"]["label"]
        top_entity = response["entities"][0] if len(response["entities"]) > 0 else None
        event["entity"] = top_entity["text"] if top_entity is not None else ""
        event["entity_type"] = top_entity["type"] if top_entity is not None else ""
    except Exception as e:
        return None
 return event

注意

您可以在此处找到代码文件

注意:我们还必须使用编辑输出模式链接声明所有输出变量,如以下屏幕截图所示:

Enriching the tweets data with the Streaming Analytics service

为代码运算符声明所有输出变量

最后,我们将目标消息中心配置为使用enriched_tweets主题。 请注意,您将需要第一次手动创建主题,方法是进入 IBM Cloud 上的 Message Hub 实例的仪表板,然后单击添加主题按钮。

然后,我们使用主工具栏中的保存按钮保存流。 流程中的任何错误(无论是代码中的编译错误,服务配置错误还是任何其他错误)都将显示在通知窗格中。 确定没有错误后,可以使用运行按钮运行流,该按钮将带我们进入流实时监视屏幕。 该屏幕由多个窗格组成。 主窗格显示了不同的运算符,数据表示为在运算符之间的虚拟管道中流动的小球。 我们可以单击管道以在右侧窗格中显示事件有效负载。 这对于调试非常有用,因为我们可以可视化如何通过每个运算符转换数据。

注意

注意:Streams Designer 还支持在代码运算符中添加 Python 日志消息,然后可以将其下载到本地计算机上进行分析。 您可以在此处了解有关此功能的更多信息

以下屏幕截图显示了流实时监控屏幕:

Enriching the tweets data with the Streaming Analytics service

实时监控屏幕,用于 Twitter 情感分析流

现在,我们使用enriched_tweets主题在消息中心实例中传递丰富的推文。 在下一部分中,我们将展示如何使用 Message Hub 实例作为输入源来创建 Spark Streaming DataFrame

使用 Kafka 输入源创建 Spark Streaming DataFrame

在最后的步骤中,我们创建一个 Spark Streaming DataFrame,它使用来自消息中心服务的enriched_tweets Kafka 主题的丰富推文。 为此,我们使用内置的 Spark Kafka 连接器在subscribe选项中指定我们要订阅的主题。 我们还需要通过从我们之前创建的全局message_hub_creds变量中读取kafka.bootstrap.servers选项来指定 Kafka 服务器列表。

注意

注意:您可能已经注意到,不同的系统对此选项使用不同的名称,这使其更易于出错。 幸运的是,如果拼写错误,将显示带有明确根本原因消息的异常。

前面的选项用于 Spark Streaming,我们仍然需要配置 Kafka 凭据,以便可以使用 Message Hub 服务对较低级别的 Kafka 使用者进行正确的认证。 为了将这些消费者属性正确地传递给 Kafka,我们不使用.option方法,而是创建一个kafka_options字典,并将其传递给load方法,如以下代码所示:

def start_streaming_dataframe():
    "Start a Spark Streaming DataFrame from a Kafka Input source"
    schema = StructType(
        [StructField(f["name"], f["type"], True) for f in field_metadata]
    )
 kafka_options = {
 "kafka.ssl.protocol":"TLSv1.2",
 "kafka.ssl.enabled.protocols":"TLSv1.2",
 "kafka.ssl.endpoint.identification.algorithm":"HTTPS",
 'kafka.sasl.mechanism': 'PLAIN',
 'kafka.security.protocol': 'SASL_SSL'
 }
    return spark.readStream \
        .format("kafka") \
 .option("kafka.bootstrap.servers", ",".join(message_hub_creds["kafka_brokers_sasl"])) \
 .option("subscribe", "enriched_tweets") \
 .load(**kafka_options)

注意

您可以在此处找到代码文件

您可能会认为代码已完成,因为笔记本的其余部分应与“第 3 部分——创建实时仪表板 PixieApp”保持不变。 这将是正确的,直到我们运行笔记本并开始看到 Spark 出现异常,抱怨找不到 Kafka 连接器。 这是因为 Kafka 连接器未包含在 Spark 的核心发行版中,因此必须单独安装。

不幸的是,这些类型的问题本质上是基础设施的,并且与手头的任务没有直接关系,而且一直在发生,我们最终花费大量时间来解决这些问题。 在 Stack Overflow 或任何其他技术站点上进行搜索通常可以迅速找到解决方案,但是在某些情况下,答案并不明显。 在这种情况下,由于我们是在笔记本中而不是在spark-submit脚本中运行,因此没有太多可用的帮助,因此我们必须进行实验直到找到解决方案。 要安装spark-sql-kafka,我们需要编辑本章前面讨论的kernel.json文件,并在"PYSPARK_SUBMIT_ARGS"条目中添加以下选项:

--packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.0

内核重新​​启动时,此配置将自动下载依赖项并将其本地缓存。

现在应该正常工作了吗? 好吧,还没有。 我们仍然必须配置 Kafka 安全性以使用我们的消息中心服务的凭据,该消息中心服务使用 SASL 作为安全协议。 为此,我们需要提供一个 JAASJava 认证和授权服务的缩写)配置文件,其中应包含该服务的用户名和密码。 Kafka 的最新版本提供了一种灵活的机制,可使用名为sasl.jaas.config的使用者属性以编程方式配置安全性。 不幸的是,最新版本的 Spark(在撰写本文时为 2.3.0)尚未更新为最新版本的 Kafka。 因此,我们不得不回到另一种配置 JAAS 的方式,即设置一个名为java.security.auth.login.config的 JVM 系统属性以及指向jaas.conf配置文件的路径。

我们首先在选择的目录中创建jaas.conf,然后在其中添加以下内容:

KafkaClient {
    org.apache.kafka.common.security.plain.PlainLoginModule required
 username="XXXX"
 password="XXXX";
};

在前面的内容中,将XXXX文本替换为从消息中心服务凭据获得的用户名和密码。

然后,将以下配置添加到kernel.json"PYSPARK_SUBMIT_ARGS"条目中:

--driver-java-options=-Djava.security.auth.login.config=<<jaas.conf path>>

作为参考,下面是包含以下配置的示例kernel.json

{
 "language": "python",
 "env": {
  "SCALA_HOME": "/Users/dtaieb/pixiedust/bin/scala/scala-2.11.8",
  "PYTHONPATH": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/:/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/lib/py4j-0.10.6-src.zip",
  "SPARK_HOME": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7",
  "PYSPARK_SUBMIT_ARGS": "--driver-java-options=-Djava.security.auth.login.config=/Users/dtaieb/pixiedust/jaas.conf --jars /Users/dtaieb/pixiedust/bin/cloudant-spark-v2.0.0-185.jar --driver-class-path /Users/dtaieb/pixiedust/data/libs/* --master local[10] --packages org.apache.spark:spark-sql-kafka-0-10_2.11:2.3.0 pyspark-shell",
  "PIXIEDUST_HOME": "/Users/dtaieb/pixiedust",
  "SPARK_DRIVER_MEMORY": "10G",
  "SPARK_LOCAL_IP": "127.0.0.1",
  "PYTHONSTARTUP": "/Users/dtaieb/pixiedust/bin/spark/spark-2.3.0-bin-hadoop2.7/python/pyspark/shell.py"
 },
 "display_name": "Python with Pixiedust (Spark 2.3)",
 "argv": [
  "python",
  "-m",
  "ipykernel",
  "-f",
  "{connection_file}"
 ]
}

注意

您可以在此处找到代码文件

注意:修改kernel.json时,应始终重新启动笔记本服务器,以确保正确加载所有新配置。

笔记本代码的其余不变,并且 PixieApp 仪表板应该可以正常工作。

注意

现在,我们已经完成了示例应用的第 4 部分。 您可以在此处找到完整的笔记本

我们必须在本节末尾编写的额外代码提醒我们,处理数据的过程绝非直线。 我们必须准备应对本质上可能不同的障碍:依赖库中的错误或外部服务中的限制。 克服这些障碍并不需要长时间停止该项目。 由于我们主要使用开源代码组件,因此我们可以在 Stack Overflow 等社交网站上利用志趣相投的开发人员组成的庞大社区,获得新的想法和代码示例,并在 Jupyter 笔记本上快速进行实验。

总结

在本章中,我们建立了一个数据管道,该管道可以分析包含非结构化文本的大量流数据,并应用来自外部云服务的 NLP 算法提取文本中的情感和其他重要实体。 我们还构建了一个 PixieApp 仪表板,该仪表板显示实时指标以及从推文中提取的见解。 我们还讨论了各种用于大规模分析数据的技术,包括 Apache Spark 结构化流技术,Apache Kafka 和 IBM Streaming Analytics。 与往常一样,这些示例应用的目标是展示建立数据管道的可能性,并特别关注利用现有框架,库和云服务。

在下一章中,我们将讨论时间序列分析,这是另一个具有许多行业应用的伟大的数据科学主题,我们将通过构建金融资产组合分析应用来进行说明。

八、金融时间序列分析和预测

“在做出重要决定时,可以相信自己的直觉,但总要用数据进行验证”

David Taieb

时间序列的研究是数据科学的一个非常重要的领域,它在工业中有多种应用,包括天气,医学,销售,当然还有金融。 它是一门广泛而复杂的主题,而对其进行详细介绍将不在本书的讨论范围之内,但是我们将尝试涉及本章中的一些重要概念,保持足够高的水平以至于不需要任何特定的特定内容。 读者的知识。 我们还展示了 Python 如何特别适合于使用诸如 pandas 之类的库进行数据分析和 NumPy 用于科学计算,并使用 MatplotlibBokeh

本章首先介绍 NumPy 库及其最重要的 API,这些 API 将在构建描述性分析以分析表示股票历史财务数据的时间序列时得到很好的利用。 使用statsmodels之类的 Python 库,我们将展示如何进行统计探索并查找平稳性,自相关函数ACF)和部分自相关函数PACF)。 有助于发现数据趋势和创建预测模型。 然后,我们将通过构建一个 PixieApp 来对这些分析进行操作,该 PixieApp 可以总结有关股票历史财务数据的所有重要统计信息和可视化效果。

在第二部分中,我们将尝试建立一个预测股票未来趋势的时间序列预测模型。 我们将使用称为 ARIMA 的集成移动平均的自回归模型,其中我们使用时间序列中的先前值来预测下一个值。 ARIMA 是当前使用最流行的模型之一,尽管基于循环神经网络的新模型开始流行起来。

与往常一样,我们将通过在StockExplorer PixieApp 中合并 ARIMA 时间序列预测模型的构建来结束本章。

NumPy 入门

NumPy 库是 Python 在数据科学家社区中获得如此高吸引力的主要原因之一。 它是一个基础库,上面有很多最受欢迎的库,例如 pandasMatplotlibSciPyScikit-learn 构建。

NumPy 提供的关键功能为:

  • 一个非常强大的多维 NumPy 数组,称为ndarray,具有非常高性能的数学运算(至少与常规的 Python 列表和数组相比)
  • 通用函数也简称为ufunc,用于在一个或多个ndarray上提供非常高效且易于使用的逐元素操作
  • 强大的ndarray切片和选择功能
  • 广播功能,使得只要遵守某些规则,就可以对不同形状的ndarray进行算术运算

在我们开始探索 NumPy API 之前,有一个绝对要了解的 API:lookfor()。 使用此方法,您可以使用查询字符串查找函数,考虑到 NumPy 提供的数百种功能强大的 API,这非常有用。

例如,我可以寻找一个计算数组平均值的函数:

import numpy as np
np.lookfor("average")

结果如下:

Search results for 'average'
----------------------------
numpy.average
    Compute the weighted average along the specified axis.
numpy.irr
    Return the Internal Rate of Return (IRR).
numpy.mean
    Compute the arithmetic mean along the specified axis.
numpy.nanmean
    Compute the arithmetic mean along the specified axis, ignoring NaNs.
numpy.ma.average
    Return the weighted average of array over the given axis.
numpy.ma.mean
    Returns the average of the array elements along given axis.
numpy.matrix.mean
    Returns the average of the matrix elements along the given axis.
numpy.chararray.mean
    Returns the average of the array elements along given axis.
numpy.ma.MaskedArray.mean
    Returns the average of the array elements along given axis.
numpy.cov
    Estimate a covariance matrix, given data and weights.
numpy.std
    Compute the standard deviation along the specified axis.
numpy.sum
    Sum of array elements over a given axis.
numpy.var
    Compute the variance along the specified axis.
numpy.sort
    Return a sorted copy of an array.
numpy.median
    Compute the median along the specified axis.
numpy.nanstd
    Compute the standard deviation along the specified axis, while
numpy.nanvar
    Compute the variance along the specified axis, while ignoring NaNs.
numpy.nanmedian
    Compute the median along the specified axis, while ignoring NaNs.
numpy.partition
    Return a partitioned copy of an array.
numpy.ma.var
    Compute the variance along the specified axis.
numpy.apply_along_axis
    Apply a function to 1-D slices along the given axis.
numpy.ma.apply_along_axis
    Apply a function to 1-D slices along the given axis.
numpy.ma.MaskedArray.var
    Compute the variance along the specified axis.

在几秒钟内,我可以找到一些候选函数,而不必离开我的笔记本电脑来查阅文档。 在前面的例子中,我可以发现一些有趣的函数-np.averagenp.mean-我仍然需要了解它们的参数。 再一次,我没有使用费时的文档来破坏我的工作流程,而是使用 Jupyter 笔记本鲜为人知的功能,该功能为我提供了内联函数的签名和文档字符串。 要调用函数的内联帮助,只需将光标置于函数的末尾并使用Shift + Tab组合即可。 第二次调用Shift + Tab将展开弹出窗口,以显示更多文本,如以下屏幕快照所示:

注意

注意Shift + Tab仅适用于函数。

Getting started with NumPy

Jupyter 笔记本中的内联帮助。

使用这种方法,我可以快速迭代候选函数,直到找到适合我需求的函数为止。

重要的是要注意,np.lookfor()不限于查询 NumPy 模块; 您也可以在其他模块中搜索。 例如,以下代码在statsmodels包中搜索与[​​HTG1](自相关函数)相关的方法:

import statsmodels
np.lookfor("acf", module = statsmodels)

注意

您可以在此处找到代码文件

这将产生以下结果:

Search results for 'acf'
------------------------
statsmodels.tsa.vector_ar.var_model.var_acf
    Compute autocovariance function ACF_y(h) up to nlags of stable VAR(p)
statsmodels.tsa.vector_ar.var_model._var_acf
    Compute autocovariance function ACF_y(h) for h=1,...,p
statsmodels.tsa.tests.test_stattools.TestPACF
    Set up for ACF, PACF tests.
statsmodels.sandbox.tsa.fftarma.ArmaFft.acf2spdfreq
    not really a method
statsmodels.tsa.stattools.acf
    Autocorrelation function for 1d arrays.
statsmodels.tsa.tests.test_stattools.TestACF_FFT
    Set up for ACF, PACF tests.
...

创建一个 NumPy 数组

创建 NumPy 数组的方法有很多。 以下是最常用的方法:

  • 从使用np.array()的 Python 列表或元组中,例如np.array([1, 2, 3, 4])

  • 从 NumPy 工厂函数之一:

    • np.random: 提供大量用于随机生成值的函数的模块。 此模块由以下类别组成:

      简单随机数据:randrandnrandint

      排列:shufflepermutation

      分布:geometriclogistic

      注意

    您可以在np.random模块上找到更多信息

    • np.arange: 返回在给定间隔内具有均匀间隔值的ndarray

      签名:numpy.arange([start, ]stop, [step, ]dtype=None)

      例如:np.arange(1, 100, 10)

      结果:array([ 1, 11, 21, 31, 41, 51, 61, 71, 81, 91])

    • np.linspace: 与np.arange类似,它会返回一个ndarray,该·在给定的间隔内具有均匀间隔的值,不同之处在于,使用linspace可以指定所需的样本数,而不是步骤数。

      例如:np.linspace(1,100,8, dtype=int)

      结果:array([ 1, 15, 29, 43, 57, 71, 85, 100])

    • np.full, np.full_like, np.ones, np.ones_like, np.zeros, np.zeros_like: 创建一个用常量值初始化的ndarray

      例如:np.ones( (2,2), dtype=int)

      结果:array([[1, 1], [1, 1]])

    • np.eye, np.identity, np.diag: 创建一个对角线常量值的ndarray

      例如:np.eye(3,3)

      结果:array([[1, 0, 0],[0, 1, 0],[0, 0, 1]])

    注意

    注意:未提供dtype参数时,NumPy 尝试从输入参数中推断出它。 但是,返回的类型可能不正确; 例如,浮点数应为整数时返回。 在这种情况下,您应该使用dtype参数来强制类型。 例如:

    np.arange(1, 100, 10, dtype=np.integer)
    

    为什么 NumPy 数组比它们的 Python 列表和数组要快得多?

    如前所述,NumPy 数组上的操作比 Python 上的数组运行快得多。 这是因为 Python 是一种动态语言,它先验地不知道它要处理的类型,因此必须不断查询与其关联的元数据,才能将其分配给正确的方法。 另一方面,通过将 CPU 密集型例程的执行委派给已预先编译的外部高度优化的 C 库,NumPy 进行了高度优化以处理大型多维数据数组。

    为了做到这一点,NumPy 对ndarray设置了两个重要的约束:

  • ndarray是不可变的:因此,如果要更改ndarray的形状或大小,或者要添加/删除元素,则必须始终创建一个新数组。 例如,以下代码使用arange()函数创建一个ndarray,该ndarray返回一维数组,该数组具有均匀间隔的值,然后对其进行整形以适合4×5矩阵:

    ar = np.arange(20)
    print(ar)
    print(ar.reshape(4,5))
    

    注意

    您可以在此处找到代码文件

    结果如下:

    before:
       [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19]
    after: 
       [[ 0  1  2  3  4]
       [ 5  6  7  8  9]
       [10 11 12 13 14]
       [15 16 17 18 19]]
    
  • ndarray中的元素必须是同一类型:ndarraydtype成员中带有元素类型。 使用nd.array()函数创建新的ndarray时,NumPy 将自动推断出适合所有元素的类型。

    例如:np.array([1,2,3]).dtype将是dtype('int64')

    np.array([1,2,'3']).dtype将是dtype('<U21'),其中<表示小尾数(请参见这个页面),而U21则是 21 个字符的 Unicode 字符串。

注意

注意:您可以在此处找到有关所有受支持数据类型的详细信息

ndarray的操作

大多数情况下,我们需要汇总ndarray上的数据。 幸运的是,NumPy 提供了非常丰富的函数集(也称为归约函数),可以在ndarrayndarray的轴上提供框外汇总。

作为参考,NumPy 轴对应于数组的尺寸。 例如,一个二维ndarray有两个轴:一个跨行运行,称为轴 0,另一个跨列运行,称为轴 1。

下图说明了二维数组中的轴:

Operations on ndarray

二维数组中的轴

接下来我们将讨论的大多数归约函数都将轴作为参数。 它们分为以下几类:

  • 数学函数

    • 三角:np.sinnp.cos,以及其它
    • 双曲:np.sinhnp.cosh,以及其它
    • 取整:np.aroundnp.floor,以及其它
    • 求和,乘积,差异:np.sumnp.prodnp.cumsum,以及其它
    • 指数和对数:np.expnp.log,以及其它
    • 算术:np.addnp.multiply,以及其它
    • 杂项:np.sqrtnp.absolute,以及其它

    注意

    注意:所有这些一元函数(仅带有一个参数的函数)都直接在ndarray级别上工作。 例如,我们可以使用np.square立即对数组中的所有值求平方:

    代号: np.square(np.arange(10))

    结果: array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81])

    您可以在此处找到有关 NumPy 数学函数的更多信息

  • 统计函数

    • 顺序统计:np.aminnp.amaxnp.percentile等,
    • 平均值和方差:np.mediannp.varnp.std
    • 相关:np.corrcoefnp.correlatenp.cov
    • 直方图:np.histogramnp.bincount,依此类推

注意

注意:Pandas 与 NumPy 紧密集成,可让您将这些 NumPy 操作应用于 Pandas DataFrame。 在本章其余部分中,当分析时间序列时,我们将大量使用此功能。

以下代码示例创建一个 Pandas DataFrame并计算所有列的平方:

Operations on ndarray

将 NumPy 操作应用于 Pandas DataFrame

NumPy 数组上的选择

NumPy 数组支持与 Python 数组和列表类似的切片操作。 因此,使用通过np.arrange()方法创建的 ndarray,我们可以执行以下操作:

sample = np.arange(10)
print("Sample:", sample)
print("Access by index: ", sample[2])
print("First 5 elements: ", sample[:5])
print("From 8 to the end: ", sample[8:])
print("Last 3 elements: ", sample[-3:])
print("Every 2 elements: ", sample[::2])

注意

您可以在此处找到代码文件

产生以下结果:

Sample: [0 1 2 3 4 5 6 7 8 9]
Access by index:  2
First 5 elements:  [0 1 2 3 4]
From index 8 to the end:  [8 9]
Last 3 elements:  [7 8 9]
Every 2 elements:  [0 2 4 6 8]

使用切片的选择也可用于具有多个维度的 NumPy 数组。 我们可以对数组中的每个维度使用切片。 对于仅允许使用切片整数进行索引的 Python 数组和列表,情况并非如此。

注意

注意:Python 中的切片具有以下语法供参考:

start:end:step

例如,我们创建一个形状为(3,4)的 NumPy 数组,即 3 行乘 4 列:

my_nparray = np.arange(12).reshape(3,4)
print(my_nparray)

返回值:

array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

假设我只选择矩阵的中间,即[5, 6]。 我可以简单地在行和列上应用切片,例如,[1:2]选择第二行,[1:3]选择第二行的第二和第三值:

print(my_nparray[1:2, 1:3])

返回值:

array([[5, 6]])

NumPy 的另一个有趣功能是,我们还可以使用谓词对具有布尔值的ndarray进行索引。

例如:

print(sample > 5 )

返回值:

[False False False False False False  True  True  True  True]

然后,我们可以使用布尔值ndarray通过简单而优雅的语法选择数据子集。

例如:

print( sample[sample > 5] )

返回值:

[6 7 8 9]

注意

这只是 NumPy 所有选择功能的一小部分预览。 有关 NumPy 选择的更多信息,您可以访问

广播

广播是 NumPy 的非常方便的功能。 它使您可以对具有不同形状的ndarray执行算术运算。 广播一词来自,事实是较小的数组会自动复制以适合较大的数组,因此它们具有兼容的形状。 但是,有一组规则可以控制广播的工作方式。

注意

您可以在此处找到有关广播的更多信息

NumPy 广播的最简单形式是标量广播,它使您可以在ndarray和标量(即数字)之间执行逐元素的算术运算。

例如:

my_nparray * 2

返回值:

array([[ 0,  2,  4,  6],
       [ 8, 10, 12, 14],
       [16, 18, 20, 22]])

注意

注意:在下面的讨论中,我们假设我们要对两个不具有相同维数的ndarray进行操作。

使用较小的数组进行广播仅需遵循一个规则:其中一个数组的尺寸至少必须等于 1。该想法是沿不匹配的尺寸复制较小的数组,直到它们匹配为止。

下图取自这个页面网站,很好地说明了添加两个数组的不同情况:

Broadcasting

广播流程说明

来源

上图中演示的三个用例是:

  • 数组的尺寸匹配:像往常一样对元素求和。

  • 较小的数组只有 1 行:复制行,直到尺寸适合第一个数组。 如果较小的数组只有 1 列,则将使用相同的算法。

  • 第一个数组只有 1 列,第二个数组只有 1 行

    • 复制第一个数组中的列,直到我们与第二个数组拥有相同的列数
    • 复制第二个数组中的行,直到与第一个数组具有相同的行数

以下代码示例显示了正在运行的 NumPy 广播:

my_nparray + np.array([1,2,3,4])

结果:

array([[ 1,  3,  5,  7],
       [ 5,  7,  9, 11],
       [ 9, 11, 13, 15]])

在本节中,我们提供了对 NumPy 的基本介绍,至少足以使我们入门并遵循本章其余部分将介绍的代码示例。 在下一节中,我们将通过统计数据探索开始关于时间序列的讨论,以发现模式,这将有助于我们识别数据中的基础结构。

时间序列统计探索

对于示例应用,我们将使用 Quandl 数据平台金融 API 提供的股票历史财务数据。 quandl Python 库

首先,我们需要通过在其自己的单元中运行以下命令来安装quandl库:

!pip install quandl

注意

注意:与往常一样,不要忘记在安装完成后重新启动内核。

免费访问 Quandl 数据,但每天最多可调用 50 次,但您可以通过创建免费帐户并获取 API 密钥来绕过此限制:

  1. 转到这个页面并通过单击右上角的注册按钮创建一个新帐户。

  2. 在注册向导的三个步骤中填写表格。 (我选择了个人,但是根据您的情况,您可能要选择商业学术。)

  3. 流程结束时,您应该会收到一封带有链接的电子邮件确认,以激活该帐户。

  4. 激活帐户后,登录 Quandl 平台网站并单击右上方菜单中的帐户设置,然后转到 API KEY 选项卡。

  5. 复制此页面中提供的 API 密钥。 该值将用于以编程方式在quandl Python 库中设置密钥,如以下代码所示:

    import quandl
    quandl.ApiConfig.api_key = "YOUR_KEY_HERE"
    

quandl库主要由两个 API 组成:

  • quandl.get(dataset, **kwargs): 这将为请求的数据集返回 Pandas DataFrame或 NumPy 数组。 dataset参数可以是字符串(单个数据集)或字符串列表(多个数据集)。 当database_code是数据发布者,而dataset_code与资源有关时,每个数据集都遵循语法database_code/dataset_code。 (请参阅下面的如何获取所有database_codedataset_code的完整列表)。

    关键字参数使您能够优化查询。 您可以在 GitHub 上的quandl代码中找到受支持参数的完整列表

    一个有趣的关键字参数returns控制该方法返回的数据结构,并且可以采用以下两个值:

    • pandas:返回 Pandas DataFrame
    • numpy:返回一个 NumPy 数组
  • quandl.get_table(datatable_code, **kwargs):返回有关资源的非时间序列数据集(称为datatable)。 我们不会在本章中使用此方法,但是您可以通过查看以下代码找到更多有关此方法

要获取database_code的列表,我们使用 Quandl REST API:https://www.quandl.com/api/v3/databases?api_key=YOUR_API_KEY&page=n,它使用分页。

注意

注意:在前面的 URL 中,将YOUR_API_KEY值替换为您的实际 API 密钥。

返回的有效负载采用以下 JSON 格式:

{
  "databases": [{
         "id": 231,
         "name": "Deutsche Bundesbank Data Repository",
         "database_code": "BUNDESBANK",
         "description": "Data on the German economy, ...",
         "datasets_count": 49358,
         "downloads": 43209922,
         "premium": false,
         "image": "https://quandl--upload.s3.amazonaws/...thumb_bundesbank.png",
         "favorite": false,
         "url_name": "Deutsche-Bundesbank-Data-Repository"
       },...
],
  "meta": {
    "query": "",
    "per_page": 100,
    "current_page": 1,
    "prev_page": null,
    "total_pages": 3,
    "total_count": 274,
    "next_page": 2,
    "current_first_item": 1,
    "current_last_item": 100
  }
}

注意

您可以在此处找到代码文件

我们使用while循环将依赖于payload['meta']['next_page']值的所有可用页面加载到,知道何时停止。 在每次迭代中,我们将database_code信息列表附加到称为databases的数组中,如以下代码所示:

import requests
databases = []
page = 1
while(page is not None):
    payload = requests.get("https://www.quandl.com/api/v3/databases?api_key={}&page={}"\
                    .format(quandl.ApiConfig.api_key, page)).json()
 databases += payload['databases']
 page = payload['meta']['next_page']

注意

您可以在此处找到代码文件

databases变量现在包含一个 JSON 对象数组,其中包含有关每个database_code的元数据。 我们使用 PixieDust display() API 在一个漂亮的可搜索表中查看数据:

import pixiedust
display(databases)

在以下 PixieDust 表的屏幕截图中,我们使用第 2 章,“使用 Jupyter 笔记本和 PixieDust 的数据科学”中描述的过滤器按钮来访问每个数据库中可用的数据集计数的统计信息,例如,最小值,最大值和平均值:

Statistical exploration of time series

Quandl 数据库代码列表

在从纽约证券交易所NYSE)搜索包含股票信息的数据库之后,我找到了XNYS数据库,如下所示:

注意

注意:确保将图表选项对话框中显示的值的数量增加到300,所以所有结果都显示在表中。

Statistical exploration of time series

寻找包含纽约证券交易所股票数据的数据库

不幸的是,XNYS数据库不是公开的,需要付费订阅。 我最终使用了WIKI数据库代码,由于某种原因,它不是前面的 API 请求返回的列表的一部分,但我在某些代码示例中找到了。

然后,我使用https://www.quandl.com/api/v3/databases/{database_code}/codes REST API 来获取数据集列表。 幸运的是,此 API 返回压缩为 ZIP 文件的 CSV,PixieDust sampleData()方法可以轻松处理该 CSV 文件,如以下代码所示:

codes = pixiedust.sampleData( "https://www.quandl.com/api/v3/databases/WIKI/codes?api_key=" + quandl.ApiConfig.api_key)
display(codes)

注意

您可以在此处找到代码文件

在 PixieDust 表界面中,我们单击选项对话框以将显示的值数量增加到4000,以便我们可以拟合整个数据集(即 3,198),并使用搜索栏查找下列屏幕快照中显示的特定股票:

注意

注意:搜索栏仅搜索浏览器中显示的行,当数据集太大时,该行可以较小。 由于在这种情况下,数据集太大,因此增加要显示的行数是不切实际的; 建议改用过滤器,这样可以保证查询整个数据集。

quandl API 返回的 CSV 文件没有标题,但是PixieDust.sampleData()希望有一个标题。 当前这是一个将来会解决的限制。

Statistical exploration of time series

WIKI 数据库的数据集列表

在本节的其余部分中,我们将加载过去几年中的 Microsoft 股票(股票代码 MSFT)的历史时间序列数据,并开始探索其统计属性。 在下面的代码中,我们将quandl.get()WIKI/MSFT数据集结合使用。 我们添加了一个称为daily_spread的列,该列通过调用 Pandas diff()方法来计算每日收益/损失,该方法返回当前和先前调整后的收盘价之间的差额。 请注意,返回的 Pandas DataFrame使用日期作为索引,但是 PixieDust 目前不支持通过索引绘制时间序列。 因此,在以下代码中,我们调用reset_index()DateTime索引转换为名为Date的新列,其中包含日期信息:

msft = quandl.get('WIKI/MSFT')
msft['daily_spread'] = msft['Adj. Close'].diff()
msft = msft.reset_index()

注意

您可以在此处找到代码文件

对于我们的第一个数据探索,我们使用display()使用 Bokeh 渲染器创建股票经调整的收盘价随时间变化的折线图。

以下屏幕截图显示了选项配置以及生成的折线图:

Statistical exploration of time series

MSFT 随时间变化的价格,根据股息分配,股票分割和其他公司行为进行了调整

我们还可以生成一个图表,该图表显示该期间每一天的每日点差,如以下屏幕截图所示:

Statistical exploration of time series

MSFT 股票的每日价差

模拟投资

作为练习,让我们尝试创建一个图表,该图表显示在选定股票(MSFT)中进行 10,000 美元的假设投资会如何随着时间变化。 为此,我们必须计算一个DataFrame,其中包含该期间每一天的总投资价值,并考虑到我们在上一段中计算的每日价差,并使用 PixieDust display() API 可视化数据。

我们使用 Pandas 功能使用基于日期的谓词来选择行,以首先过滤DataFrame以仅选择我们感兴趣的时间段内的数据点。然后,通过将 10,000 美元的初始投资,除以周期第一天的收盘价,并加上初始投资价值,来计算购买的股票数量。 多亏了 Pandas 的高效序列计算和底层的 NumPy 基础库,所有这些计算都变得非常容易。 我们使用np.cumsum()方法计算所有每日收益加上 10,000 美元的初始投资价值。

最后,我们使用resample()方法使图表更易于阅读,该方法将频率从每日转换为每月,并使用当月平均值计算新值。

以下代码使用从 2016 年 5 月开始的时间段来计算增长数据帧:

import pandas as pd
tail = msft[msft['Date'] > '2016-05-16']
investment = np.cumsum((10000 / tail['Adj. Close'].values[0]) * tail['daily_spread']) + 10000
investment = investment.astype(int)
investment.index = tail['Date']
investment = investment.resample('M').mean()
investment = pd.DataFrame(investment).reset_index()
display(investment)

注意

您可以在此处找到代码文件

以下屏幕截图显示了display() API 生成的图形,包括配置选项:

Hypothetical investment

假设的投资组合增长

自相关函数(ACF)和部分自相关函数(PACF)

在尝试生成预测模型之前,必须了解时间序列是否具有可识别的模式,例如季节性或趋势。 一种流行的技术是根据指定的时滞来查看数据点与先前的数据点之间的关系。 直觉是自相关将揭示内部结构,例如,确定发生高相关(正或负)时的时间段。 您可以尝试不同的滞后值(即,对于每个数据点,要考虑多少个先前的点),以找到正确的周期。

计算 ACF 通常需要为数据点集计算 Pearson R 相关系数。 好消息是statsmodels Python 库具有tsa包(时间序列分析),该包提供了辅助方法来计算 ACF, 与 Pandas 序列紧密集成。

注意

注意:如果尚未完成,我们将使用以下命令安装statsmodels包,并在完成后重新启动内核:

!pip install statsmodels

以下代码使用tsa.api.graphics包中的plot_acf()计算并可视化 MSFT 股票时间序列调整后的收盘价的 ACF:

import statsmodels.tsa.api as smt
import matplotlib.pyplot as plt
smt.graphics.plot_acf(msft['Adj. Close'], lags=100)
plt.show()

结果如下:

Autocorrelation function (ACF) and partial autocorrelation function (PACF)

滞后为 100 的 MSFT 的 ACF

上图显示了x横坐标给出的多个先前数据点(滞后)处数据的自相关。 因此,在滞后0处,您始终具有1.0的自相关(您始终与自己完美相关),滞后1显示与先前数据点的自相关,滞后2显示与落后两个步骤的数据点的自相关。 我们可以清楚地看到,随着滞后的增加,自相关减小。 在上一张图表中,我们仅使用了 100 个滞后,并且我们看到自相关仍然在 0.9 左右具有统计显着性,这告诉我们长时间分隔的数据是不相关的。 这表明数据具有趋势,当浏览整体价格图表时,这一趋势非常明显。

为了确认这个假设,我们用更大的lags参数绘制 ACF 图表,例如1000(鉴于我们的序列有 10,000 个以上的数据点这一事实,这是不合理的),如以下屏幕快照所示:

Autocorrelation function (ACF) and partial autocorrelation function (PACF)

滞后为 1000 的 MSFT 的 ACF

现在我们清楚地看到,自相关在600滞后附近降至显着性水平以下。

为了更好地说明 ACF 的工作原理,让我们生成一个不带趋势的周期性时间序列,并了解可以学习的内容。 例如,我们可以对np.linspace()生成的一系列均匀间隔的点使用np.cos()

smt.graphics.plot_acf(np.cos(np.linspace(0, 1000, 100)), lags=50)
plt.show()

注意

您可以在此处找到代码文件

结果如下:

Autocorrelation function (ACF) and partial autocorrelation function (PACF)

没有趋势的周期性序列的 ACF

在上一张图表中,我们可以看到自相关以固定间隔(每隔 5 个滞后)再次尖峰,清楚地显示了周期性(在处理实际数据时也称为季节性)。

使用 ACF 检测时间序列中的结构有时会导致问题,尤其是当具有很强的周期性时。 在这种情况下,无论您尝试自动关联数据的时间有多远,您总是会在一段时间内看到自相关峰值,这可能会导致错误的解释。 要解决此问题,我们使用的 PACF 使用的延迟更短,并且与 ACF 不同,它不重用以前在较短时间段内发现的相关性。 ACF 和 PACF 的数学相当复杂,但是读者只需要了解其背后的直觉,并乐于使用statsmodels之类的库来进行繁重的计算。 我用来获取有关 ACF 和 PACF 的更多信息的一种资源可以在这里找到

回到我们的 MSFT 股票时间序列,以下代码显示了如何使用smt.graphics包绘制其 PACF:

import statsmodels.tsa.api as smt
smt.graphics.plot_pacf(msft['Adj. Close'], lags=50)
plt.show()

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Autocorrelation function (ACF) and partial autocorrelation function (PACF)

MSFT 股票时间序列的部分自相关

我们将在本章稍后讨论 ARIMA 模型的时间序列预测时,回到 ACF 和 PACF。

在本节中,我们讨论了探索数据的多种方法。 当然,这绝不是详尽无遗的,但是让我们想到了 Jupyter,Pandas,NumPy 和 PixieDust 等工具如何使实验更容易,并且在必要时会快速失败。 在下一节中,我们将构建一个 PixieApp,将所有这些图表组合在一起。

将其与StockExplorer PixieApp 整合在一起

对于StockExplorer PixieApp 的第一个版本,我们希望对用户选择的股票数据时间序列进行数据探索。 与我们构建的其他 PixieApps 相似,第一个屏幕的布局简单,带有一个输入框,用户可以在其中输入以逗号分隔的股票代码列表,以及一个浏览按钮以启动数据探索。 主屏幕由垂直导航栏组成,该导航栏带有每种数据浏览类型的菜单。 为了使 PixieApp 代码更具模块化并易于维护和扩展,我们在垂直导航栏触发的子 PixieApp 中实现每个数据浏览屏幕。 同样,每个子 PixieApp 都继承自称为BaseSubApp的基类,该基类提供了对所有子类有用的通用功能。 下图显示了所有子 PixieApps 的总体 UI 布局以及类图:

Putting it all together with the StockExplorer PixieApp

StockExplorer PixieApp 的 UI 布局

首先让我们看一下欢迎屏幕的实现。 它是StockExplorer PixieApp 类的默认路由中实现的。 以下代码显示StockExplorer类的部分实现,以仅包括默认路由。

注意

注意:在提供完整实现之前,请不要尝试运行此代码。

@PixieApp
class StockExplorer():
    @route()
    def main_screen(self):
        return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-3"></div>
        <div class="input-group col-sm-6">
          <input id="stocks{{prefix}}" type="text"
              class="form-control"
              value="MSFT,AMZN,IBM"
              placeholder="Enter a list of stocks separated by comma e.g MSFT,AMZN,IBM">
          <span class="input-group-btn">
 <button class="btn btn-default" type="button" pd_options="explore=true">
                <pd_script>
self.select_tickers('$val(stocks{{prefix}})'.split(','))
                </pd_script>
                Explore
            </button>
          </span>
        </div>
    </div>
</div>
"""

注意

您可以在此处找到代码文件

前面的代码与到目前为止我们看到的其他示例 PixieApps 非常相似。 浏览按钮包含以下两个 PixieApp 属性:

  • pd_script子元素,该元素调用 Python 代码段来设置股票行情收录器。 我们还使用$val指令来检索用户输入的股票报价器的值:

    <pd_script>
       self.select_tickers('$val(stocks{{prefix}})'.split(','))
    </pd_script>
    
  • pd_options属性,它指向explore路由:

    pd_options="explore=true"
    

select_tickers帮助程序方法将代码行列表存储在字典成员变量中,并选择第一个作为活动代码行。 出于性能方面的考虑,我们仅在需要时加载数据,即,首次设置活动代码或用户在 UI 中单击特定代码时。

注意

注意:与前面的章节一样,[[StockExplorer]]表示该代码是StockExplorer类的一部分。

[[StockExplorer]]
def select_tickers(self, tickers):
        self.tickers = {ticker.strip():{} for ticker in tickers}
        self.set_active_ticker(tickers[0].strip())

def set_active_ticker(self, ticker):
    self.active_ticker = ticker
 if 'df' not in self.tickers[ticker]:
        self.tickers[ticker]['df'] = quandl.get('WIKI/{}'.format(ticker))
        self.tickers[ticker]['df']['daily_spread'] = self.tickers[ticker]['df']['Adj. Close'] - self.tickers[ticker]['df']['Adj. Open']
        self.tickers[ticker]['df'] = self.tickers[ticker]['df'].reset_index()

注意

您可以在此处找到代码文件

set_active_ticker()中将特定股票代码的股票数据延迟加载到 Pandas DataFrame中。 我们首先通过查看df键是否存在来检查是否已加载DataFrame,如果不存在,我们用dataset_code'WIKI/{ticker}'调用quandl API。 我们还添加了一个列,该列计算了将在基本探索屏幕中显示的股票的每日价差。 最后,我们需要在DataFrame上调用reset_index()以转换索引, 是DateTimeIndex进入其自己的列,称为Date。 原因是 PixieDust display()尚不支持使用DateTimeIndex可视化DataFrame

explore路由中,我们返回一个 HTML 片段,该片段构建了整个屏幕的布局。 如前面的模型所示,我们使用btn-group-verticalbtn-group-toggle引导程序类创建垂直导航栏。 菜单列表和关联的子 PixieApp 在tabs Python 变量中定义,我们使用 Jinja2 {%for loop%}构建内容。 我们还将在id ="analytic_screen{{prefix}}"中添加一个占位符<div>元素,该元素将成为子 PixieApp 屏幕的接收者。

explore路由实现如下所示:

[[StockExplorer]] 
@route(explore="*")
 @templateArgs
    def stock_explore_screen(self):
 tabs = [("Explore","StockExploreSubApp"),
 ("Moving Average", "MovingAverageSubApp"),
 ("ACF and PACF", "AutoCorrelationSubApp")]
        return """
<style>
    .btn:active, .btn.active {
        background-color:aliceblue;
    }
</style>
<div class="page-header">
    <h1>Stock Explorer PixieApp</h1>
</div>
<div class="container-fluid">
    <div class="row">
        <div class="btn-group-vertical btn-group-toggle col-sm-2"
             data-toggle="buttons">
 {%for title, subapp in tabs%}
            <label class="btn btn-secondary {%if loop.first%}active{%endif%}"
                pd_options="show_analytic={{subapp}}"
                pd_target="analytic_screen{{prefix}}">
                <input type="radio" {%if loop.first%}checked{%endif%}>
                    {{title}}
            </label>
 {%endfor%}
        </div>
        <div id="analytic_screen{{prefix}}" class="col-sm-10">
    </div>
</div>
"""

注意

您可以在此处找到代码文件

在前面的代码中,请注意我们使用@templateArgs装饰器,因为我们要使用 Jinja2 模板中在方法实现本地创建的tabs变量。

垂直导航栏中的每个菜单都指向相同的analytic_screen{{prefix}}目标,并使用{{subapp}}引用的选定子 PixieApp 类名称调用show_analytic路由。

反过来,show_anatytic路由仅返回带有<div>元素的 HTML 片段,该元素具有pd_app属性,该属性引用子 PixieApp 类名称。 我们还使用pd_render_onload属性要求 PixieApp 将<div>元素的内容加载到浏览器 DOM 中后立即呈现。

以下代码用于show_analytic路由:

    @route(show_analytic="*")
    def show_analytic_screen(self, show_analytic):
        return """
<div pd_app="{{show_analytic}}" pd_render_onload></div>
"""

注意

您可以在此处找到代码文件

BaseSubApp——所有子 PixieApps 的基类

现在让我们看一下每个子 PixieApps 的实现以及基类BaseSubApp用于如何提供常见的功能。 对于每个子 PixieApp,我们希望用户能够通过选项卡式界面选择股票行情收录器,如以下屏幕截图所示:

BaseSubApp – base class for all the child PixieApps

MSFT,IBM,AMZN 代码的选项卡小部件

我们没有为每个子 PixieApp 重复 HTML 片段,而是使用了我特别喜欢的技术,该技术包括创建一个称为add_ticker_selection_markup的 Python 装饰器,该装饰器可动态更改函数的行为(有关 Python 装饰器的更多信息,请参见这个页面。 此修饰器在BaseSubApp类类中创建,并将自动为该路由添加选项卡选择小部件 HTML 标记,如以下代码所示:

[[BaseSubApp]]
def add_ticker_selection_markup(refresh_ids):
    def deco(fn):
        def wrap(self, *args, **kwargs):
            return """
<div class="row" style="text-align:center">
 <div class="btn-group btn-group-toggle"
 style="border-bottom:2px solid #eeeeee"
 data-toggle="buttons">
 {%for ticker, state in this.parent_pixieapp.tickers.items()%}
 <label class="btn btn-secondary {%if this.parent_pixieapp.active_ticker == ticker%}active{%endif%}"
 pd_refresh=\"""" + ",".join(refresh_ids) + """\" pd_script="self.parent_pixieapp.set_active_ticker('{{ticker}}')">
 <input type="radio" {%if this.parent_pixieapp.active_ticker == ticker%}checked{%endif%}> 
 {{ticker}}
 </label>
 {%endfor%}
 </div>
</div>
            """ + fn(self, *args, **kwargs)
        return wrap
    return deco

注意

您可以在此处找到代码文件

乍一看,前面的代码可能很难阅读,因为add_ticker_selection_markup装饰器方法包含两个级匿名嵌套方法。 让我们尝试说明每种方法的目的,包括主要的add_ticker_selection_markup装饰器方法:

  • add_ticker_selection_markup:这是主要的装饰器方法,采用一个称为refresh_ids的参数,该参数将在生成的标记中使用。 此方法返回一个匿名函数deco,该函数带有一个函数参数。

  • deco:这是一个包装器方法,采用一个称为fn的参数,该参数是指向应用装饰器的原始函数的指针。 此方法返回一个名为wrap的匿名函数,当在用户代码中调用该函数时,该函数将在原始函数的环境中被调用。

  • wrap: 这是采用三个参数的最终包装方法:

    • self:指向该函数的主类的指针
    • *args:原始方法定义的任何变量参数(可以为空)
    • **kwargs:原始方法定义的任何关键字参数(可以为空)

    wrap方法可以通过 Python 闭包机制访问超出其范围的变量。 在这种情况下,它使用refresh_ids生成选项卡小部件标记,然后使用selfargskwargs参数调用fn函数。

注意

注意:即使多次阅读后,上述解释仍然令人困惑,请不要担心。 您现在只能使用装饰器,它不会影响您理解本章其余部分的能力。

StockExploreSubApp——第一个子 PixieApp

现在,我们可以实现名为StockExploreSubApp的第一个子 PixieApp。 在主屏幕中,我们创建两个<div>元素,每个元素均具有pd_options属性,该属性以Adj. Closedaily_spread作为值调用show_chart路由。 反过来,show_chart路由返回一个<div>元素,该元素的pd_entity属性指向parent_pixieapp.get_active_df()方法,该属性带有一个<pd_options>元素,该元素包含一个 JSON 有效载荷,用于显示带有Date的散景折线图作为x横坐标,并且将任何值作为参数传递为y纵坐标的列。 我们还使用BaseSubApp.add_ticker_selection_markup装饰器装饰路由,并使用前两个<div>元素的 ID 作为refresh_ids参数。

以下代码显示了StockExplorerSubApp子 PixieApp 的实现:

@PixieApp
class StockExploreSubApp(BaseSubApp):
    @route()
 @BaseSubApp.add_ticker_selection_markup(['chart{{prefix}}', 'daily_spread{{prefix}}'])
    def main_screen(self):
        return """
<div class="row" style="min-height:300px">
    <div class="col-xs-6" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
    </div>
    <div class="col-xs-6" id="daily_spread{{prefix}}" pd_render_onload pd_options="show_chart=daily_spread">
    </div>
</div>
"""

    @route(show_chart="*")
    def show_chart_screen(self, show_chart):
        return """
<div pd_entity="parent_pixieapp.get_active_df()" pd_render_onload>
    <pd_options>
    {
      "handlerId": "lineChart",
      "valueFields": "{{show_chart}}",
      "rendererId": "bokeh",
      "keyFields": "Date",
      "noChartCache": "true",
      "rowCount": "10000"
    }
    </pd_options>
</div>
        """

注意

您可以在此处找到代码文件

show_chart路由之前的中,pd_entity使用来自parent_pixieapp的和get_active_df()方法,该方法在StockExplorer主类中定义如下:

[[StockExplorer]]
def get_active_df(self):
    return self.tickers[self.active_ticker]['df']

注意

您可以在此处找到代码文件

提醒一下,StockExploreSubApp通过菜单中的StockExplorer路由的Explore路由声明的tabs数组变量中的元组与菜单关联:

tabs = [("Explore","StockExploreSubApp"), ("Moving Average", "MovingAverageSubApp"),("ACF and PACF", "AutoCorrelationSubApp")]

注意

您可以在此处找到代码文件

以下屏幕截图显示了StockExploreSubApp

StockExploreSubApp – first child PixieApp

StockExploreSubApp主屏幕

MovingAverageSubApp——第二个子 PixieApp

第二个子 PixieApp 是MovingAverageSubApp,它显示选定股票行情指示器的移动平均线的折线图,其滞后可通过滑块控件进行配置。 与代码选择器选项卡类似,另一个子 PixieApp 中将需要滞后滑块。 我们可以使用与股票行情选择标签控件相同的装饰器技术,但在这里我们希望能够将滞后滑块放置在页面上的任何位置。 因此,我们将使用在BaseSubApp类中定义的名为lag_sliderpd_widget控件,并为滑块控件返回 HTML 片段。 它还添加了<script>元素,该元素使用 jQuery UI 模块中可用的 jQuery slider方法(有关更多信息,请参见这个页面)。 我们还添加了change处理函数,当用户选择新值时会调用该函数。 在此处理器中,我们调用pixiedust.sendEvent函数来发布lagSlider类型的事件和包含滞后新值的有效负载。 调用者有责任添加<pd_event_handler>元素以监听该事件并处理有效负载。

以下代码显示了lag_slider pd_widget的实现:

[[BaseSubApp]]
@route(widget="lag_slider")
def slider_screen(self):
    return """
<div>
    <label class="field">Lag:<span id="slideval{{prefix}}">50</span></label>
    <i class="fa fa-info-circle" style="color:orange"
       data-toggle="pd-tooltip"
       title="Selected lag used to compute moving average, ACF or PACF"></i>
    <div id="slider{{prefix}}" name="slider" data-min=30 
         data-max=300
         data-default=50 style="margin: 0 0.6em;">
    </div>
</div>
<script>
$("[id^=slider][id$={{prefix}}]").each(function() {
    var sliderElt = $(this)
    var min = sliderElt.data("min")
    var max = sliderElt.data("max")
    var val = sliderElt.data("default")
 sliderElt.slider({
        min: isNaN(min) ? 0 : min,
        max: isNaN(max) ? 100 : max,
        value: isNaN(val) ? 50 : val,
        change: function(evt, ui) {
            $("[id=slideval{{prefix}}]").text(ui.value);
            pixiedust.sendEvent({type:'lagSlider',value:ui.value})
        },
        slide: function(evt, ui) {
            $("[id=slideval{{prefix}}]").text(ui.value);
        }
    });
})
</script>
        """

注意

您可以在此处找到代码文件

MovingAverageSubApp中,我们使用add_ticker_selection_markup装饰器并将chart{{prefix}}作为默认路由的参数,以添加股票选择器选项卡,并添加一个名为lag_slider<div>元素,其中包含<pd_event_handler>设置self.lag变量并刷新chart DIV。 chart DIV 将pd_entity属性与调用rolling方法的get_moving_average_df()方法一起使用,从选定的 Pandas DataFrame返回滚动均值,并在其上调用mean()方法。 由于 PixieDust display()尚不支持 Pandas 序列,因此我们将序列索引用作名为x的列来构建 Pandas DataFrame,并以get_moving_average_df()方法返回它。

以下代码显示了MovingAverageSubApp子 PixieApp 的实现

@PixieApp
class MovingAverageSubApp(BaseSubApp):
    @route()
 @BaseSubApp.add_ticker_selection_markup(['chart{{prefix}}'])
    def main_screen(self):
        return """
<div class="row" style="min-height:300px">
    <div class="page-header text-center">
        <h1>Moving Average for {{this.parent_pixieapp.active_ticker}}</h1>
    </div>
    <div class="col-sm-12" id="chart{{prefix}}" pd_render_onload pd_entity="get_moving_average_df()">
        <pd_options>
        {
          "valueFields": "Adj. Close",
          "keyFields": "x",
          "rendererId": "bokeh",
          "handlerId": "lineChart",
          "rowCount": "10000"
        }
        </pd_options>
    </div>
</div>
<div class="row">
    <div pd_widget="lag_slider">
        <pd_event_handler 
            pd_source="lagSlider"
 pd_script="self.lag = eventInfo['value']"
 pd_refresh="chart{{prefix}}">
        </pd_event_handler>
    </div>
</div>
"""
    def get_moving_average_df(self):
        ma = self.parent_pixieapp.get_active_df()['Adj. Close'].rolling(window=self.lag).mean()
        ma_df = pd.DataFrame(ma)
        ma_df["x"] = ma_df.index
        return ma_df

注意

您可以在此处找到代码文件

以下屏幕截图显示了MovingAverageSubApp显示的图表:

MovingAverageSubApp – second child PixieApp

MovingAverageSubApp屏幕截图

AutoCorrelationSubApp——第三个子 PixieApp

对于第三个子项,PixieApp 调用了AutoCorrelationSubApp; 我们显示所选股票DataFrame的 ACF 和 PACF,这些数据是使用statsmodels包计算的。

以下代码显示了AutoCorrelationSubApp的实现,该实现也使用了add_ticker_selection_markup装饰器和名为lag_sliderpd_widget

import statsmodels.tsa.api as smt
@PixieApp
class AutoCorrelationSubApp(BaseSubApp):
    @route()
    @BaseSubApp.add_ticker_selection_markup(['chart_acf{{prefix}}', 'chart_pacf{{prefix}}'])
    def main_screen(self):
        return """
<div class="row" style="min-height:300px">
    <div class="col-sm-6">
        <div class="page-header text-center">
            <h1>Auto-correlation Function</h1>
        </div>
        <div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
        </div>
    </div>
    <div class="col-sm-6">
        <div class="page-header text-center">
            <h1>Partial Auto-correlation Function</h1>
        </div>
        <div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
        </div>
    </div>
</div> 

<div class="row">
    <div pd_widget="lag_slider">
        <pd_event_handler 
            pd_source="lagSlider"
            pd_script="self.lag = eventInfo['value']"
            pd_refresh="chart_acf{{prefix}},chart_pacf{{prefix}}">
        </pd_event_handler>
    </div>
</div>
"""
 @route(show_acf='*')
 @captureOutput
    def show_acf_screen(self):
        smt.graphics.plot_acf(self.parent_pixieapp.get_active_df()['Adj. Close'], lags=self.lag)

 @route(show_pacf='*')
 @captureOutput
    def show_pacf_screen(self):
        smt.graphics.plot_pacf(self.parent_pixieapp.get_active_df()['Adj. Close'], lags=self.lag)

注意

您可以在此处找到代码文件

在前面的代码中,我们定义了两个路由:show_acfshow_pacf,它们分别调用smt.graphics包的plot_acfplot_pacf方法。 我们还使用@captureOutput装饰器向 PixieApp 框架发出信号,以捕获plot_acfplot_pacf生成的输出。

以下屏幕截图显示了AutoCorrelationSubApp显示的图表:

AutoCorrelationSubApp – third child PixieApp

AutoCorrelationSubApp屏幕截图

在本部分中,我们显示了如何将样本 PixieApp 放在一起,该样本对时间序列进行基本数据探索并显示各种统计图。 完整的笔记本可以在这里找到

在下一节中,我们将尝试使用称为自回归综合移动平均值ARIMA)的非常流行的模型来构建时间序列预测模型。

使用 ARIMA 模型的时间序列预测

ARIMA 是最受欢迎的时间序列预测模型之一,顾名思义,它由三个项组成:

  • AR: 代表自回归,无非就是使用一种线性回归算法,该算法使用一个观测值和自己的滞后观测值作为训练数据。

    AR 模型使用以下公式:

    Time series forecasting using the ARIMA model

    其中φ[i]是从先前观察值中获知的模型的权重,ε[t]是观察值t的残差。

    我们也将p称为自回归模型的阶,该阶数定义为上式中包含的滞后观测次数。

    例如:

    AR(2)定义为:

    Time series forecasting using the ARIMA model

    AR(1)定义为:

    Time series forecasting using the ARIMA model

  • I: 代表集成。 为了使 ARIMA 模型正常工作,假设时间序列是固定的或可以使其固定。 如果其均值和方差不随时间变化,据说一个序列是固定的

    注意

    注意:还有严格的平稳性的概念,它要求随时间推移时观察子集的联合概率分布不发生变化。

    使用数学符号,严格平稳性可以转换为:

    F = (y[t], ..., y[t + k]F = (y[t + m], ..., y[t + m + k]对于任何tmk都是相同的,其中F是联合概率分布。

    实际上,此条件太强了,最好使用前面提供的较弱的定义。

    我们可以通过使用观察值与之前的观察值之间的对数差的转换来使时间序列平稳,如以下等式所示:

    Time series forecasting using the ARIMA model

    在序列实际变为固定时间之前,可能需要多次对数差分转换。 我们称d为使用对数差分对序列进行变换的次数。

    例如:

    I(0)被定义为不需要对数差(该模型称为 ARMA)。

    I(1)定义为需要 1 个对数差。

    I(2)定义为需要 2 个对数差。

    注意

    注意:重要的是要记住在预测一个值之后对进行的所有积分进行反向转换。

  • MA: 代表“移动平均线”。 MA 模型使用来自当前观测值均值的残差和滞后观测值的加权残差。 我们可以使用以下公式定义模型:

    Time series forecasting using the ARIMA model

    在哪里

    Time series forecasting using the ARIMA model

    是时间序列的平均值,ε[t]是序列中的残差,θ[q]是滞后残差的权重。

    我们将q称为移动平均窗口的大小。

    例如:

    MA(0)定义为不需要移动平均线(该模型称为 AR)。

    MA(1)被定义为使用 1 的移动平均窗口。公式变为:

    Time series forecasting using the ARIMA model

根据先前的定义,我们使用符号ARIMA(p, d, q)定义 ARIMA 模型,其阶数为p的自回归模型,阶为[d和大小为q的移动平均窗口。

实现所有代码以建立 ARIMA 模型可能非常耗时。 幸运的是,statsmodels库在statsmodels.tsa.arima_model包中实现了ARIMA类,该类提供了使用fit()方法训练模型并使用predict()方法预测值所需的所有计算。 它还照顾日志差异以使时间序列固定。 诀窍在于找到用于构建最佳 ARIMA 模型的参数pdq。 为此,我们使用 ACF 和 PACF 图表,如下所示:

  • p值对应于滞后数(在x横坐标上),ACF 图表首次超过统计显着性阈值。
  • 类似地,q值对应于 PACF 图表首次超过统计显着性阈值的滞后次数(在x横坐标上)。

为 MSFT 股票时间序列建立 ARIMA 模型

提醒一下,MSFT 股票时间序列的价格图如下所示:

Build an ARIMA model for the MSFT stock time series

MSFT 股票序列图

在开始建立模型之前,让我们首先保留数据的最后 14 天以进行测试,然后将其余部分用于训练。

以下代码定义了两个新变量:train_settest_set

train_set, test_set = msft[:-14], msft[-14:]

注意

注意:如果您仍然不熟悉前面的切片符号,请参阅本章开头有关 NumPy 的部分

从上图可以清楚地看到从 2012 年开始的增长趋势,但没有明显的季节性。 因此,我们可以放心地假设没有平稳性。 让我们首先尝试一次应用对数差分转换,并绘制相应的 ACF 和 PACF 图表。

在以下代码中,我们通过在Adj. Close列上使用np.log()来构建logmsftPandas 序列,然后使用logmsft与 1 的滞后时间之间的差值来构建logmsft_diff pandas DataFrameshift()方法)。 像以前做过一样,我们也调用reset_index()Date索引转换为一列,以便 PixieDust display()可以处理它:

logmsft = np.log(train_set['Adj. Close'])
logmsft.index = train_set['Date']
logmsft_diff = pd.DataFrame(logmsft - logmsft.shift()).reset_index()
logmsft_diff.dropna(inplace=True)
display(logmsft_diff)

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Build an ARIMA model for the MSFT stock time series

应用对数差异后的 MSFT 股票序列

通过查看前面的图,我们可以合理地认为我们已经成功地使时间序列固定为 0。 我们还可以使用更严格的方法通过使用 Dickey-Fuller 测试,它测试AR(1)模型中存在单位根的原假设。

注意

注意:在统计中,统计假设检验包括通过抽取样本并确定主张是否成立来质疑所提出的假设是否成立。 我们看一下 p 值,它有助于确定结果的重要性。 有关统计假设检验的更多详细信息,请参见此处

以下代码使用statsmodels.tsa.stattools包中的adfuller方法:

from statsmodels.tsa.stattools import adfuller
import pprint

ad_fuller_results = adfuller(
logmsft_diff['Adj. Close'], autolag = 'AIC', regression = 'c'
)
labels = ['Test Statistic','p-value','#Lags Used','Number of Observations Used']
pp = pprint.PrettyPrinter(indent=4)
pp.pprint({labels[i]: ad_fuller_results[i] for i in range(4)})

注意

您可以在此处找到代码文件

我们使用了pprint包,该包用于精美打印任何 Python 数据结构。 有关pprint的更多信息,请参见这个页面

结果(在这个页面中进行了详细说明)显示在此处:

{
    'Number of lags used': 3,
    'Number of Observations Used': 8057,
    'Test statistic': -48.071592138591136,
    'MacKinnon's approximate p-value': 0.0
}

注意

您可以在此处找到代码文件

p 值低于显着性水平; 因此,我们可以否定AR(1)模型中存在单位根的零假设,这使我们确信时间序列是固定的。

然后我们绘制 ACF 和 PACF 图表,这将为我们提供 ARIMA 模型的pq参数:

以下代码构建了 ACF 图表:

import statsmodels.tsa.api as smt
smt.graphics.plot_acf(logmsft_diff['Adj. Close'], lags=100)
plt.show()

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Build an ARIMA model for the MSFT stock time series

日志差异 MSFTDataFrame的 ACF

从前面的 ACF 图中,我们可以看到相关性在的第一时间越过统计显着性阈值,因此滞后 1。因此,我们将p = 1用作我们 ARIMA 模型的 AR 阶数。

我们对 PACF 进行相同的操作:

smt.graphics.plot_pacf(logmsft_diff['Adj. Close'], lags=100)
plt.show()

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Build an ARIMA model for the MSFT stock time series

PACF 用于日志差异 MSFT DataFrame

从前面的 PACF 图中,我们还可以看到相关性以 1 的滞后首次超过统计显着性阈值。因此,我们将q = 1作为我们的 ARIMA 模型 MA 阶数。

我们还只需要应用一次日志差分转换。 因此,我们将d = 1用于 ARIMA 模型的集成部分。

注意

注意:在调用ARIMA类时,如果您使用d = 0,则可能必须手动进行日志区分,在这种情况下,您需要自己根据预测值还原转换。 如果不是,statsmodels包将负责在返回预测值之前恢复转换。

以下代码使用p = 1d = 1q = 1作为阶次值,在train_set时间序列上训练 ARIMA 模型ARIMA构造器的元组参数。 然后,我们调用fit()方法进行训练并获得模型:

from statsmodels.tsa.arima_model import ARIMA

import warnings
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    arima_model_class = ARIMA(train_set['Adj. Close'], dates=train_set['Date'], order=(1,1,1))
    arima_model = arima_model_class.fit(disp=0)

    print(arima_model.resid.describe())

注意

您可以在此处找到代码文件

注意:我们使用warnings包来避免在使用较旧版本的 NumPy 和 Pandas 时可能发生的弃用警告。

在前面的代码中,我们将train_set['Adj. Close']用作ARIMA构造器的参数。 由于我们对数据使用序列,因此我们还需要为dates参数传递train_set['Date']序列。 请注意,如果我们通过带有DateIndex索引的 Pandas DataFrame来传递,则不必使用dates参数。 ARIMA构造器的最后一个参数是order参数,它是表示pdq阶数的三个值的元组, 如本节开头所讨论的。

然后,我们调用fit()方法,该方法返回将用于预测值的实际 ARIMA 模型。 出于信息目的,我们使用arima_model.resid.describe()打印有关模型剩余误差的统计信息。

结果显示在这里:

count    8.061000e+03
mean    -5.785533e-07
std      4.198119e-01
min     -5.118915e+00
25%     -1.061133e-01
50%     -1.184452e-02
75%      9.848486e-02
max      5.023380e+00
dtype: float64

平均残留误差为-5.7 * 10 ** (-7),非常接近零,因此表明该模型可能过拟合了训练数据。

现在我们有了一个模型,让我们尝试对其进行诊断。 我们定义了一种称为plot_predict的方法,该方法将用作模型,一系列日期和一个数字,这些数字指示我们要追溯的距离。 然后,我们调用 ARIMA plot_predict()方法来创建包含预测值和观察值的图表。

以下代码显示了plot_predict()方法的实现,包括使用10010对其进行两次调用:

def plot_predict(model, dates_series, num_observations):
    fig = plt.figure(figsize = (12,5))
    model.plot_predict(
        start = str(dates_series[len(dates_series)-num_observations]),
        end = str(dates_series[len(dates_series)-1])
    )
    plt.show()

plot_predict(arima_model, train_set['Date'], 100)
plot_predict(arima_model, train_set['Date'], 10)

注意

您可以在此处找到代码文件

结果显示在这里:

Build an ARIMA model for the MSFT stock time series

观测值与预测图

前面的图表显示了预测与训练集中的实际观察值有多接近。 我们现在使用之前保留的测试集来进一步诊断模型。 对于这一部分,我们使用forecast()方法预测下一个数据点。 对于test_set的每个值,我们从称为历史的观察值数组中构建了新的 ARIMA 模型,其中包含包含每个预测值增加的训练数据。

以下代码显示了compute_test_set_predictions()方法的实现,该方法以train_settest_set作为参数,并返回一个 Pandas 数据帧,其中的forecast列包含所有预测值,而test列包含相应的实际观测值:

def compute_test_set_predictions(train_set, test_set):
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        history = train_set['Adj. Close'].values
        forecast = np.array([])
        for t in range(len(test_set)):
            prediction = ARIMA(history, order=(1,1,0)).fit(disp=0).forecast()
            history = np.append(history, test_set['Adj. Close'].iloc[t])
            forecast = np.append(forecast, prediction[0])
        return pd.DataFrame(
 {"forecast": forecast,
 "test": test_set['Adj. Close'],
 "Date": pd.date_range(start=test_set['Date'].iloc[len(test_set)-1], periods = len(test_set))
 }
 )

results = compute_test_set_predictions(train_set, test_set)
display(results)

注意

您可以在此处找到代码文件

以下屏幕截图显示了结果图表:

Build an ARIMA model for the MSFT stock time series

预测值与实际值的图表

我们可以使用 Scikit-learn 包的流行mean_squared_error方法来测量错误,其定义如下:

Build an ARIMA model for the MSFT stock time series

其中Y[i]是实际值,Y_hat[i]是预测值。

以下代码定义了compute_mean_squared_error方法,该方法接受检验和预测序列,并返回均方误差值:

from sklearn.metrics import mean_squared_error
def compute_mean_squared_error(test_series, forecast_series):
    return mean_squared_error(test_series, forecast_series)

print('Mean Squared Error: {}'.format(
compute_mean_squared_error( test_set['Adj. Close'], results.forecast))
)

注意

您可以在此处找到代码文件

结果显示在这里:

Mean Squared Error: 6.336538843075749

StockExplorer PixieApp 第 2 部分——添加使用 ARIMA 模型的时间序列预测

在本部分中,我们通过添加菜单来改进StockExplorer PixieApp,该菜单使用 ARIMA 模型为选定的股票行情提供时间序列预测。 我们创建一个名为ForecastArimaSubApp的新类并更新主要StockExplorer类中的tabs变量。

[[StockExplorer]]
@route(explore="*")
@templateArgs
def stock_explore_screen(self):
   tabs = [("Explore","StockExploreSubApp"),
           ("Moving Average", "MovingAverageSubApp"),
           ("ACF and PACF", "AutoCorrelationSubApp"),
 ("Forecast with ARIMA", "ForecastArimaSubApp")]
   ...

注意

您可以在此处找到代码文件

ForecastArimaSubApp子 PixieApp 由两个屏幕组成。 第一个屏幕显示时间序列图表以及 ACF 和 PACF 图表。 该屏幕的目的是为用户提供必要的数据探索,以找出pdq的值是什么。 ARIMA 模型的阶数,如上一节所述。 通过查看时间序列图,我们可以找出时间序列是否固定(提醒一下,这是构建 ARIMA 模型的必要条件)。 如果没有,用户可以单击添加差异按钮,尝试使用对数差异转换来制作DataFrame信纸。 然后,使用转换后的DataFrame更新这三个图表。

以下代码显示了ForecastArimaSubApp子 PixieApp 的默认路由:

from statsmodels.tsa.arima_model import ARIMA

@PixieApp
class ForecastArimaSubApp(BaseSubApp):
    def setup(self):
        self.entity_dataframe = self.parent_pixieapp.get_active_df().copy()
        self.differencing = False

    def set_active_ticker(self, ticker):
 BaseSubApp.set_active_ticker(self, ticker)
        self.setup()

    @route()
 @BaseSubApp.add_ticker_selection_markup([])
    def main_screen(self):
        return """
<div class="page-header text-center">
    <h2>1\. Data Exploration to test for Stationarity
        <button class="btn btn-default"
                pd_script="self.toggle_differencing()" pd_refresh>
            {%if this.differencing%}Remove differencing{%else%}Add differencing{%endif%}
        </button>
        <button class="btn btn-default"
                pd_options="do_forecast=true">
            Continue to Forecast
        </button>
    </h2>
</div>

<div class="row" style="min-height:300px">
    <div class="col-sm-10" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
    </div>
</div>

<div class="row" style="min-height:300px">
    <div class="col-sm-6">
        <div class="page-header text-center">
            <h3>Auto-correlation Function</h3>
        </div>
        <div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
        </div>
    </div>
    <div class="col-sm-6">
        <div class="page-header text-center">
            <h3>Partial Auto-correlation Function</h3>
        </div>
        <div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
        </div>
    </div>
</div>
        """

注意

您可以在此处找到代码文件

前面的代码遵循我们现在应该熟悉的模式:

  • 定义一个setup方法,该方法在 PixieApp 启动时一定会被调用。 在此方法中,我们将复制从父 PixieApp 获得的所选DataFrame。 我们还维护一个名为self.differencing的变量,该变量跟踪用户是否单击了添加差异按钮。

  • 我们创建一个默认的路由,该路由显示由以下组件组成的第一个屏幕:

    • 标头带有两个按钮:Add differencing用于使时间序列固定,而Continue to forecast显示第二个屏幕,我们将在后面讨论。 应用差分后,Add differencing按钮将切换为Remove differencing
    • 一个<div>元素,它调用show_chart路由以显示时间序列图。
    • 一个<div>元素,它调用show_acf路由以显示 ACF 图。
    • 调用show_pacf路由以显示 PACF 图表的<div>元素。
  • 我们使用一个空数组[]作为@BaseSubApp.add_ticker_selection_markup装饰器的参数,以确保当用户选择另一个股票报价器时刷新整个屏幕,并从第一个屏幕重新启动。 我们还需要重置内部变量。 为了实现这一目标,我们对add_ticker_selection_markup进行了更改,以在BaseSubApp中定义了一种称为set_active_ticker的新方法,该方法是父 PixieApp 对set_active_ticker的包装方法。 想法是让子类覆盖此方法,并在需要时注入额外的代码。 我们还更改了 TAB 元素的pd_script属性,以在用户选择新的代码符号时调用此方法,如以下代码所示:

    [[BaseSubApp]]
    def add_ticker_selection_markup(refresh_ids):
            def deco(fn):
                def wrap(self, *args, **kwargs):
                    return """
    <div class="row" style="text-align:center">
        <div class="btn-group btn-group-toggle"
             style="border-bottom:2px solid #eeeeee"
             data-toggle="buttons">
            {%for ticker, state in this.parent_pixieapp.tickers.items()%}
            <label class="btn btn-secondary {%if this.parent_pixieapp.active_ticker == ticker%}active{%endif%}"
                pd_refresh=\"""" + ",".join(refresh_ids) + """\" pd_script="self.set_active_ticker('{{ticker}}')">
                <input type="radio" {%if this.parent_pixieapp.active_ticker == ticker%}checked{%endif%}> 
                    {{ticker}}
            </label>
            {%endfor%}
        </div>
    </div>
                    """ + fn(self, *args, **kwargs)
                return wrap
            return deco
    
     def set_active_ticker(self, ticker):
     self.parent_pixieapp.set_active_ticker(ticker)
    
    

注意

您可以在此处找到代码文件

然后,在ForecastArimaSubApp子 PixieApp 中,覆盖set_active_tracker方法,首先调用父级,然后调用self.setup()重新初始化内部变量:

[[ForecastArimaSubApp]]
def set_active_ticker(self, ticker):
        BaseSubApp.set_active_ticker(self, ticker)
        self.setup()

注意

您可以在此处找到代码文件

第一个预测屏幕的路由实现非常简单。 Add differencing/Remove differencing按钮具有pd_script属性,该属性调用self.toggle_differencing()方法和pd_refresh属性以更新整个页面。 还定义了三个<div>元素,分别调用show_chartshow_acfshow_pacf路由,如以下代码所示:

[[ForecastArimaSubApp]]
@route()
    @BaseSubApp.add_ticker_selection_markup([])
    def main_screen(self):
        return """
<div class="page-header text-center">
  <h2>1\. Data Exploration to test for Stationarity
    <button class="btn btn-default"
            pd_script="self.toggle_differencing()" pd_refresh>
    {%if this.differencing%}Remove differencing{%else%}Add differencing{%endif%}
    </button>
    <button class="btn btn-default" pd_options="do_forecast=true">
        Continue to Forecast
    </button>
  </h2>
</div>

<div class="row" style="min-height:300px">
  <div class="col-sm-10" id="chart{{prefix}}" pd_render_onload pd_options="show_chart=Adj. Close">
  </div>
</div>

<div class="row" style="min-height:300px">
    <div class="col-sm-6">
        <div class="page-header text-center">
            <h3>Auto-correlation Function</h3>
        </div>
        <div id="chart_acf{{prefix}}" pd_render_onload pd_options="show_acf=true">
        </div>
    </div>
    <div class="col-sm-6">
      <div class="page-header text-center">
         <h3>Partial Auto-correlation Function</h3>
      </div>
      <div id="chart_pacf{{prefix}}" pd_render_onload pd_options="show_pacf=true">
      </div>
    </div>
</div>
        """

注意

您可以在此处找到代码文件

toggle_differencing()方法使用self.differencing变量跟踪当前差异状态,从parent_pixieapp复制活动的DataFrame或对self.entity_dataframe变量应用日志差异转换​​,如以下代码所示:

def toggle_differencing(self):
   if self.differencing:
       self.entity_dataframe = self.parent_pixieapp.get_active_df().copy()
       self.differencing = False
   else:
       log_df = np.log(self.entity_dataframe['Adj. Close'])
       log_df.index = self.entity_dataframe['Date']
       self.entity_dataframe = pd.DataFrame(log_df - log_df.shift()).reset_index()
       self.entity_dataframe.dropna(inplace=True)
       self.differencing = True

注意

您可以在此处找到代码文件

show_acfshow_pacf路由非常简单。 它们分别调用smt.graphics.plot_acfsmt.graphics.plot_pacf方法。 他们还使用@captureOutput装饰器将图表图像传递到目标小部件:

@route(show_acf='*')
@captureOutput
def show_acf_screen(self):
    smt.graphics.plot_acf(self.entity_dataframe['Adj. Close'], lags=50)

@route(show_pacf='*')
@captureOutput
def show_pacf_screen(self):
    smt.graphics.plot_pacf(self.entity_dataframe['Adj. Close'], lags=50)

注意

您可以在此处找到代码文件

以下屏幕快照显示了没有差异的预测子项 PixieApp 的数据浏览页面:

StockExplorer PixieApp Part 2 – add time series forecasting using the ARIMA model

第一预测屏幕,不应用差异

正如预期的那样,这些图表与不稳定的时间序列一致。 当用户单击添加差异按钮时,将显示以下屏幕:

StockExplorer PixieApp Part 2 – add time series forecasting using the ARIMA model

应用差异的第一个预测屏幕

下一步是实现由继续预测按钮调用的do_forecast路由。 这条路由负责建立 ARIMA 模型; 首先显示一个配置页面,其中包含三个输入文本,这些文本可让用户输入pdq阶数,通过查看数据浏览屏幕中的图表。 我们添加一个Go按钮以使用build_arima_model路由进行模型构建,我们将在本节稍后讨论。 标头上还有一个Diagnose Model按钮,该按钮调用另一个负责评估模型准确率的页面。

此处显示了do_forecast路由的实现。 请注意,当用户选择其他股票行情自动收录器时,我们将add_ticker_selection_markup与空数组一起使用以刷新整个页面:

[[ForecastArimaSubApp]] 
@route(do_forecast="true")
 @BaseSubApp.add_ticker_selection_markup([])
    def do_forecast_screen(self):
        return """
<div class="page-header text-center">
    <h2>2\. Build Arima model
        <button class="btn btn-default"
                pd_options="do_diagnose=true">
            Diagnose Model
        </button>
    </h2>
</div>
<div class="row" id="forecast{{prefix}}">
    <div style="font-weight:bold">Enter the p,d,q order for the ARIMA model you want to build</div>

    <div class="form-group" style="margin-left: 20px">
        <label class="control-label">Enter the p order for the AR model:</label>
        <input type="text" class="form-control"
               id="p_order{{prefix}}"
               value="1" style="width: 100px;margin-left:10px">

        <label class="control-label">Enter the d order for the Integrated step:</label>
        <input type="text" class="form-control"
               id="d_order{{prefix}}" value="1"
               style="width: 100px;margin-left:10px">

        <label class="control-label">Enter the q order for the MA model:</label>
        <input type="text" class="form-control" 
               id="q_order{{prefix}}" value="1"
               style="width: 100px;margin-left:10px">
    </div>

    <center>
        <button class="btn btn-default"
               pd_target="forecast{{prefix}}"
            pd_options="p_order=$val(p_order{{prefix}});d_order=$val(p_order{{prefix}});q_order=$val(p_order{{prefix}})">
        Go
        </button>
    </center>
</div>
"""

注意

您可以在此处找到代码文件

以下屏幕快照显示了构建 ARIMA 模型页面的配置页面:

StockExplorer PixieApp Part 2 – add time series forecasting using the ARIMA model

构建 Arima 模型页面的配置页面

转到按钮具有pd_options属性,该属性调用具有三个状态的路由:p_orderd_orderq_order,其值取自与每个属性相关的三个输入框。

以下代码显示了构建 ARIMA 模型的路由。 首先将活动的DataFrame分成训练和测试集,并保留 14 个测试集的观察值。 然后,它构建模型并计算残留误差。 成功建立模型后,我们将返回一个 HTML 标记,其中包含图表,该图表显示了训练集的预测值与训练集中的实际值。 这是通过调用plot_predict路由来完成的。 最后,我们还通过创建带有pd_entity属性的<div>元素和指向带有<pd_options>子元素的残差变量的<div>元素,该子元素配置了所有统计信息的表格视图,从而显示了模型剩余误差的统计信息

显示预测与实际训练集的图表使用的是plot_predict路由,该路由调用了我们先前在笔记本电脑中创建的plot_predict方法。 我们还使用@captureOutput装饰器将图表图像发送到正确的小部件。

plot_predict路由的实现如下所示:

    @route(plot_predict="true")
    @captureOutput
    def plot_predict(self):
        plot_predict(self.arima_model, self.train_set['Date'], 100)

注意

您可以在此处找到代码文件

build_arima_model路由实现如下所示:

@route(p_order="*",d_order="*",q_order="*")
def build_arima_model_screen(self, p_order, d_order, q_order):
    #Build the arima model
    self.train_set = self.parent_pixieapp.get_active_df()[:-14]
    self.test_set = self.parent_pixieapp.get_active_df()[-14:]
    self.arima_model = ARIMA(
        self.train_set['Adj. Close'], dates=self.train_set['Date'],
        order=(int(p_order),int(d_order),int(q_order))
    ).fit(disp=0)
    self.residuals = self.arima_model.resid.describe().to_frame().reset_index()
    return """
<div class="page-header text-center">
    <h3>ARIMA Model succesfully created</h3>
<div>
<div class="row">
    <div class="col-sm-10 col-sm-offset-3">
        <div pd_render_onload pd_options="plot_predict=true">
        </div>
        <h3>Predicted values against the train set</h3>
    </div>
</div>
<div class="row">
    <div pd_render_onload pd_entity="residuals">
        <pd_options>
 {
 "handlerId": "tableView",
 "table_noschema": "true",
 "table_nosearch": "true",
 "table_nocount": "true"
 }
 </pd_options>
    </div>
    <h3><center>Residual errors statistics</center></h3>
<div>
        """

注意

您可以在此处找到代码文件

以下屏幕截图显示了构建 Arima 模型页面的结果:

StockExplorer PixieApp Part 2 – add time series forecasting using the ARIMA model

模型制作页面

预测子应用的最终屏幕是do_diagnose路由调用的“诊断模型”屏幕。 在此屏幕中,我们仅显示由和test_set变量在笔记本中创建的compute_test_set_predictions方法返回的数据帧的折线图。 该图表的<div>元素使用pd_entity属性,该属性调用称为compute_test_set_predictions的中间类方法。 它还具有<pd_options>子元素,带有display()选项,用于显示折线图。

以下代码显示了do_diagnose_screen路由的实现:

    def compute_test_set_predictions(self):
        return compute_test_set_predictions(self.train_set, self.test_set)

    @route(do_diagnose="true")
    @BaseSubApp.add_ticker_selection_markup([])
    def do_diagnose_screen(self):
        return """
<div class="page-header text-center"><h2>3\. Diagnose the model against the test set</h2></div>
<div class="row">
    <div class="col-sm-10 center" pd_render_onload pd_entity="compute_test_set_predictions()">
        <pd_options>
 {
 "keyFields": "Date",
 "valueFields": "forecast,test",
 "handlerId": "lineChart",
 "rendererId": "bokeh",
 "noChartCache": "true" 
 }
        </pd_options>
    </div>
</div>
"""

注意

您可以在此处找到代码文件

以下屏幕截图显示了诊断页面的结果:

StockExplorer PixieApp Part 2 – add time series forecasting using the ARIMA model

模型诊断画面

在本节中,我们展示了如何改进StockExplorer样本 PixieApp,以包括使用 ARIMA 模型的预测功能。 顺便说一句,我们已经演示了如何使用 PixieApp 编程模型创建一个三步向导,该向导首先执行一些数据探索,然后配置模型的参数并构建它,最后根据测试集对模型进行诊断。

注意

笔记本的完整实现可以在这里找到

总结

在本章中,我们谈到了时间序列分析和预测的主题。 当然,我们只是从头开始,当然还有很多需要探索的地方。 通过非常积极的研究,它也是该行业的一个非常重要的领域,尤其是在金融领域。 例如,我们看到越来越多的数据科学家试图基于循环神经网络算法建立时间序列预测模型,并取得了巨大的成功。 我们还演示了 Jupyter 笔记本与 PixieDust 和pandasnumpystatsmodels,之类的库生态系统相结合如何帮助加速分析的开发,以及将其操作化投入业务用户线可使用的应用。

在下一章中,我们将研究另一个重要的数据科学用例:图形。 我们将构建一个与飞行旅行相关的示例应用,并讨论如何以及何时应用图算法来解决数据问题。

九、使用图的美国国内航班数据分析

“在获得数据之前先进行理论分析是一个重大错误。”

福尔摩斯

在本章中,我们重点介绍一种称为图的基本计算机科学数据模型,以及在它们上常用的不同类型的算法。 作为数据科学家或开发人员,熟悉图并迅速识别它们何时提供解决特定数据问题的正确解决方案非常重要。 例如,图非常适合基于 GPS 的应用(例如 Google Maps),并考虑到各种参数(包括用户是开车,步行还是乘坐公共交通工具)找到从 A 点到 B 点的最佳路线 ,或者用户想要的是最短的路线,还是无论公路总距离如何都可以最大限度地利用高速公路的路线。 这些参数中的一些也可以是实时参数,例如交通状况和天气。 使用图的另一类重要应用是社交网络,例如 Facebook 或 Twitter,其中顶点表示个人,边表示关系,例如好友关注

我们将从对图和相关图算法的高级介绍开始本章。 然后,我们将介绍networkx,这是一个 Python 库,可轻松加载,操纵和可视化图数据结构,并提供丰富的图算法集。 我们将通过建立样本分析来继续讨论,该样本分析使用各种图算法分析美国的航班数据,其中机场用作顶点,航班用作边线。 与往常一样,我们还将通过构建一个简单的仪表板 PixieApp 来对这些分析进行操作。 我们将通过使用在第 8 章,“金融时间序列分析和预测”中学习到的时间序列技术构建历史模型来建立预测模型。

图简介

图的引入和相关的图论在 1736 年被列昂哈德·欧拉(Leonhard Euler)研究柯尼斯堡(Königsberg)七桥

这座城市被普雷格尔河分开,普雷格尔河在某些时候形成了两个岛屿,并根据下图所示的布局建造了七座桥梁。 问题是找到一种方法让人们一次又一次地跨过每座桥,然后回到起点。 欧拉证明了这个问题没有解决方案,并且在此过程中诞生了图论。 基本思想是将城市图转换为一个图,其中每个土绘图都是一个顶点,每个桥都是连接两个顶点(即土绘图)的边。 然后将问题简化为找到一条路径,该路径是边缘和顶点的连续序列,仅包含每个桥一次。

下图显示了欧拉如何将柯尼斯堡七桥问题简化为图问题:

Introduction to graphs

将柯尼斯堡七桥问题简化为图问题

使用更正式的定义,是表示对象(称为顶点节点)之间对象之间的成对关系(称为边缘)的数据结构。 )。 通常使用以下表示法表示图:G = (V, E)其中V是顶点集,而E是顶点集的边缘。

图主要有两大类:

  • 有向图(称为有向图):成对关系的顺序很重要,即,从顶点A到顶点B的边(AB)不同于从顶点B到顶点A的边缘(BA)。
  • 无向图:成对关系的顺序无关紧要,即边(A-B)与边(B-A)相同。

下图显示了示例图的表示形式,即无向(边缘没有箭头)和有向(边缘有箭头):

Introduction to graphs

图的表示

主要有两种表示图的方式:

  • 邻接矩阵:使用n维矩阵表示图(我们称其为A),其中n是图中的顶点数。 使用 1 到n个整数对顶点进行索引。 我们用A[i, j] = 1来表示顶点i和顶点j之间存在边,而A[i, j] = 0来表示顶点i和顶点j之间不存在边缘。 在无向图的情况下,因为顺序无关紧要,所以我们总是有A[i, j] = A [j, i]。 然而,在有序关系重要的有向图的情况下,A[i, j]可能与A[j, i]不同。 以下示例显示了如何在有向和无向的邻接矩阵中表示样本图:

    Graph representations

    图的邻接矩阵表示(有向和无向)

    重要的是要注意,邻接矩阵表示具有恒定的空间复杂度,即O(n²),其中n是顶点数,但是的时间复杂度为O(1),这是恒定时间,用于计算两个顶点之间是否存在边连接。 当图密集(边缘很多)时,高空间复杂度可能还可以,但在图稀疏时可能会浪费空间,在这种情况下,我们可能更喜欢以下邻接表表示形式。

    注意

    注意大 O 表示法通常用于代码分析,以通过随着输入大小的增加评估其行为来表示算法的性能。 它用于评估运行时间(运行算法所需的指令数量)和空间需求(随着时间的推移需要多少存储空间)。

  • 邻接表:对于每个顶点,我们维护一个边连接的所有顶点的列表。 在无向图的情况下,每个边都被表示两次,每个端点代表一个边,对于顺序重要的有向图则不是这种情况。

    下图显示了有向图和无向图的图的邻接表表示形式:

    Graph representations

    图的邻接表表示(有向和无向)

    与邻接矩阵表示法相反,邻接列表表示法具有较小的空间复杂度,即O(m + n),其中m是边的个数,n是顶点数。 但是,与邻接矩阵的O(1)相比,时间复杂度增加到O(m)。 由于这些原因,当图稀疏连接时(即没有很多边),最好使用邻接表表示。

正如前面的讨论所暗示的那样,要使用哪种图表示形式在很大程度上取决于图密度,还取决于我们计划使用的算法类型。 在下一节中,我们将讨论最常用的图算法。

图算法

以下是最常用的图算法的列表:

  • 搜索:在图的上下文中,搜索意味着找到两个顶点之间的路径。路径定义为边和顶点的连续序列。在图中搜索路径的动机可能是多种多样的。可能是您有兴趣根据一些预定义的距离标准(例如,边的最小数量(例如 GPS 路线图))找到最短路径,或者只是想知道两个顶点之间存在一条路径(对于例如,请确保网络中的每台计算机均可从其他任何计算机访问)。一种搜索路径的通用算法是从给定的顶点开始,发现与之相连的所有顶点,将发现的顶点标记为已探索(因此我们不会两次找到它们),并继续进行相同的探索每个发现的顶点,直到找到目标顶点,或者用尽顶点为止。该搜索算法有两种常用的风格:广度优先搜索和深度优先搜索,每种都有各自的用例,它们更适合于这些用例。这两种算法的区别在于我们找到未探索顶点的方式:

    • 广度优先搜索(BFS):首先探索与相邻的未探索节点。 探索完相邻邻域后,开始探索层中每个节点的邻域,直到到达图的末尾。 由于我们正在探索首先直接连接的所有顶点,因此该算法保证找到与找到的邻域数量相对应的最短路径。 BFS 的扩展是著名的 Dijkstra 最短路径算法,其中每个边都与非负权重相关联。 在这种情况下,最短路径可能不是跳数最少的路径,而是使所有权重之和最小的路径。 Dijkstra 最短路径的一个示例应用是查找地图上两点之间的最短路径。
    • 深度优先搜索(DFS):对于每个直接相邻的顶点,请先尽可能深入地探索其相邻的邻居,然后在耗尽邻居时开始回溯。 DFS 的应用示例包括查找拓扑排序和有向图的强连接组件。 作为参考,拓扑排序是顶点的线性排列,以使线性顶点中的每个顶点都遵循下一个顶点的边缘方向(也就是说,它不会向后移动)。 有关更多信息,请参见这个页面

    下图说明了在 BFS 和 DFS 之间查找未探索的节点的区别:

    Graph algorithms

    在 BFS 和 DFS 中查找未探索的顶点的顺序

  • 连通组件和强连通组件:图的连通组件是一组顶点,其中任意两个顶点之间都有路径。 注意,该定义仅指定必须存在路径,这意味着只要存在路径,两个顶点之间就不必具有边。 在有向图的情况下,由于附加的方向约束,连通组件被称为“强连通组件”,这不仅要求任何顶点 A 都应具有通往任何其他顶点 B 的路径,而且 B 也必须具有通往 A 的路径。

    下图显示了牢固连通组件或有向图示例:

    Graph algorithms

    有向图的强连接组件

  • 中心:顶点的中心度指示符指示顶点相对于图中其他顶点的重要性。 这些中心指数有多个重要的应用。 例如,在社交网络中确定最有影响力的人或通过最重要的页面对网络搜索进行排名等。

    中心性有多个指标,但我们将重点关注本章稍后将使用的以下四个指标:

    • 阶数:顶点的阶数是该顶点是端点之一的边的数量。 对于有向图,是顶点是源或目标的边数,我们称入度是顶点为目标的边的数量,出度是顶点为源的边的数量。

    • PageRank:这是 Google,Larry Page 和 Sergey Brin 的创始人开发的著名算法。 PageRank 用于通过对给定网站的重要性进行衡量来对搜索结果进行排名,其中包括计算从其他网站到该网站的链接数。 它还会评估这些链接的质量(即网站链接到您的链接的可信度)。

    • 紧密度:紧密度中心度与给定顶点和图中所有其他顶点之间的最短路径的平均长度成反比。 直觉是顶点离所有其他节点越近,它就越重要。

      紧密度中心度可以使用以下简单方程式计算:

      Graph algorithms

      (来源:https://en.wikipedia.org/wiki/Centrality#Closeness_centrality

      其中d(y, x)是节点xy之间的边缘长度。

    • 最短路径之间的间隔:根据给定顶点是任意两个节点之间最短路径的一部分的次数进行度量。 直觉是,顶点对最短路径的贡献越大,它就越重要。 这里提供了最短路径之间的数学方程式:

      Graph algorithms

      (来源:https://en.wikipedia.org/wiki/Centrality#Betweenness_centrality

      其中σ[st]是从顶点s到顶点t的最短路径总数,σ[st](v)是通过vσ[st]的子集。

      注意

    注意:有关集中性的更多信息,请参见这里

图和大数据

到目前为止,我们的图讨论集中于可以容纳在一台机器中的数据,但是当我们拥有非常庞大的具有数十亿个顶点和边的图而无法将整个数据加载到内存中时,会发生什么? 一个自然的解决方案是将数据分布在多个节点的集群中,这些节点并行处理数据并合并单个结果以形成最终答案。 幸运的是,有多个框架提供了这种图并行功能,并且它们几乎都包含了大多数常用的图算法的实现。 流行的开源框架的示例有 Apache Spark GraphXApache Giraph。 Facebook 目前正在使用来分析其社交网络。

无需过多讨论,重要的是要知道这些框架都是从分布式计算的批量同步并行BSP)模型,它使用机器之间的消息在整个集群中查找顶点。 要记住的关键点是这些框架通常非常易于使用,例如,使用 Apache Spark GraphX 编写本章的分析本来就很容易。

在本节中,我们仅回顾了所有可用的图算法中的一小部分,并且深入探讨将超出本书的范围。 自己实现这些算法将花费大量时间,但是幸运的是,有很多开源库提供了图算法的相当完整的实现,并且易于使用并将其集成到您的应用中。 在本章的其余部分,我们将使用networkx开源 Python 库。

NetworkX 图库入门

在开始之前,如果尚未完成,则需要使用pip工具安装networkx库。 在自己的单元格中执行以下代码:

!pip install networkx

注意

注意:与往常一样,不要忘记在安装完成后重新启动内核。

networkx提供的大多数算法都可以直接从主模块调用。 因此,用户将只需要以下import语句:

import networkx as nx

创建图

首先,让我们回顾一下networkx和创建空图的构造器支持的不同类型的图:

  • Graph:无向图,只允许顶点之间有一个边。 允许自环边。 构造器示例:

    G = nx.Graph()
    
  • Digraph:实现有向图的Graph的子类。 构造器示例:

    G = nx.DiGraph()
    
  • MultiGraph:无向图,允许顶点之间有多个边。 构造器示例:

    G = nx.MultiGraph()
    
  • MultiDiGraph:有向图,允许顶点之间有多个边。 构造器示例:

    G = nx.MultiDiGraph()
    

Graph类提供了许多用于添加和删除顶点和边的方法。 这是可用方法的子集:

  • add_edge(u_of_edge, v_of_edge, **attr):在顶点u和顶点v之间添加一条边,并带有与该边相关联的可选附加属性。 如果图中尚未存在顶点uv,则会自动创建它们。
  • remove_edge(u, v):移除uv之间的边缘。
  • add_node(self, node_for_adding, **attr):使用可选的附加属性将节点添加到图。
  • remove_node(n):删除由给定参数n标识的节点。
  • add_edges_from(ebunch_to_add, **attr):批量添加具有可选附加属性的多个边。 边必须以两元组(u,v)或三元组(u,v,d)的列表形式给出,其中d是包含边数据的字典。
  • add_nodes_from(self, nodes_for_adding, **attr):使用可选附加属性批量添加多个节点。 可以将节点提供为列表,字典,集合,数组等。

作为练习,我们从头开始构建一个一直用作示例的有向图:

Creating a graph

使用 NetworkX 以编程方式创建的示例图

以下代码首先创建一个DiGraph()对象,然后使用add_nodes_from()方法在一次调用中添加所有节点,然后使用add_edge()add_edges_from()的组合开始添加边线:

G = nx.DiGraph()
G.add_nodes_from(['A', 'B', 'C', 'D', 'E'])
G.add_edge('A', 'B')
G.add_edge('B', 'B')
G.add_edges_from([('A', 'E'),('A', 'D'),('B', 'C'),('C', 'E'),('D', 'C')])

注意

您可以在此处找到代码文件

Graph类还提供了通过变量类视图轻松访问其属性的方法。 例如,您可以使用G.nodesG.edges遍历图的顶点和边缘,还可以使用以下符号访问单个边缘:G.edges[u,v]

以下代码遍历图的节点并打印它们:

for n in G.nodes:
    print(n)

networkx库还提供了一组丰富的预建图生成器,可用于测试算法。 例如,您可以使用complete_graph()生成器轻松生成完整的图,如以下代码所示:

G_complete = nx.complete_graph(10)

注意

您可以在此处找到所有可用图生成器的完整列表

可视化图

NetworkX 支持多种渲染引擎,包括 Matplotlib,Graphviz AGraph 和 Graphviz 和 pydot。 尽管 Graphviz 提供了非常强大的绘图功能,但我发现很难安装。 但是,Matplotlib 已预先安装在 Jupyter 笔记本中,可以让您快速入门。

核心图函数称为draw_networkx,该函数将图作为参数以及一堆可选的关键字参数,这些参数可用于设置图的样式,例如颜色,宽度以及节点和边的标签字体。 通过将GraphLayout对象传递到pos关键字参数来配置图绘图的总体布局。 默认布局为spring_layout(使用强制控制算法),但是 NetworkX 支持许多其他布局,包括circular_layoutrandom_layoutspectral_layout您可以在此处找到所有可用布局的列表

为了方便起见,networkx将这些布局中的每一个封装到其自己的高级绘制方法中,这些方法调用合理的默认值,以便调用者不必处理这些布局中的每一个的复杂性。 例如,draw()方法将使用sprint_layoutdraw_circular()circular_layout以及draw_random()random_layout绘制图。

在下面的示例代码中,我们使用draw()方法可视化我们先前创建的G_complete图:

%matplotlib inline
import matplotlib.pyplot as plt
nx.draw(G_complete, with_labels=True)
plt.show()

注意

您可以在此处找到代码文件

结果显示在以下输出中:

Visualizing a graph

绘制具有 10 个节点的完整图

使用networkx绘制图既简单又有趣,并且由于它使用的是 Matplotlib,因此您可以使用 Matplotlib 的绘制功能进一步美化它们。 我鼓励读者通过可视化笔记本中的不同图来进一步试验。 在下一节中,我们将开始实现一个示例应用,该应用使用图算法分析飞行数据。

第 1 部分——将美国国内航班数据加载到图中

要初始化笔记本,让我们在其自己的单元格中运行以下代码,以导入本章其余部分将大量使用的包:

import pixiedust
import networkx as nx
import pandas as pd
import matplotlib.pyplot as plt

我们还将使用 Kaggle 网站上位于以下位置的 2015 年航班延误和取消数据集。 数据集由三个文件组成:

  • airports.csv:美国所有机场的列表,包括其 IATA 代码(国际航空运输协会),城市,州,经度和纬度。
  • airlines.csv:美国航空公司的列表,包括其 IATA 代码。
  • flights.csv:2015 年发生的航班列表。此数据包括日期,始发和目的地机场,计划和实际时间以及延误。

flights.csv文件包含将近 600 万条记录,需要清除这些记录才能删除始发地或目的地机场中所有没有 IATA 三字母代码的航班。 我们还想删除ELAPSED_TIME列中值缺失的行。 如果不这样做,则会在将数据加载到图结构中时引起问题。 另一个问题是数据集包含一些时间列,例如DEPARTURE_TIMEARRIVAL_TIME,并且为了节省空间,这些列仅以HHMM格式存储时间,而实际日期存储在YEARMONTHDAY列中。 我们将在本章中进行的一项分析将需要DEPARTURE_TIME的完整日期时间,并且由于进行此转换是一项耗时的操作,因此我们现在进行并将其存储在我们将存储在 GitHub 上的flights.csv的处理版本中。 此操作使用通过to_datetime()函数和axis=1调用的 Pandas apply()方法(指示对每行应用了转换)。

另一个问题是我们希望将文件存储在 GitHub 上,但是最大文件大小限制为 100M。因此,为使文件小于 100 M,我们还删除了一些不需要的列。 我们尝试构建的分析文件,然后将其压缩,然后再将其存储在 GitHub 上。 当然,另一个好处是DataFrame可以使用较小的文件更快地加载。

从 Kaggle 网站下载文件后,我们运行以下代码,该代码首先将 CSV 文件加载到 Pandas DataFrame中,删除不需要的行和列,然后将数据写回到文件中:

注意

注意:原始数据存储在名为flights.raw.csv的文件中。

由于包含 600 万条记录的文件很大,因此运行以下代码可能需要一些时间。

import pandas as pd
import datetime
import numpy as np

# clean up the flights data in flights.csv
flights = pd.read_csv('flights.raw.csv', low_memory=False)

# select only the rows that have a 3 letter IATA code in the ORIGIN and DESTINATION airports
mask = (flights["ORIGIN_AIRPORT"].str.len() == 3) & (flights["DESTINATION_AIRPORT"].str.len() == 3)
flights = flights[ mask ]

# remove the unwanted columns
dropped_columns=["SCHEDULED_DEPARTURE","SCHEDULED_TIME",
"CANCELLATION_REASON","DIVERTED","DIVERTED","TAIL_NUMBER",
"TAXI_OUT","WHEELS_OFF","WHEELS_ON",
"TAXI_IN","SCHEDULED_ARRIVAL", "ARRIVAL_TIME", "AIR_SYSTEM_DELAY","SECURITY_DELAY",
"AIRLINE_DELAY","LATE_AIRCRAFT_DELAY", "WEATHER_DELAY"]
flights.drop(dropped_columns, axis=1, inplace=True)

# remove the row that have NA in the ELAPSED_TIME column
flights.dropna(subset=["ELAPSED_TIME"], inplace=True)

# remove the row that have NA in the DEPARTURE_TIME column
flights.dropna(subset=["ELAPSED_TIME"], inplace=True)

# Create a new DEPARTURE_TIME columns that has the actual datetime
def to_datetime(row):
    departure_time = str(int(row["DEPARTURE_TIME"])).zfill(4)
    hour = int(departure_time[0:2])
    return datetime.datetime(year=row["YEAR"], month=row["MONTH"],
                             day=row["DAY"],
                             hour = 0 if hour >= 24 else hour,
                             minute=int(departure_time[2:4])
                            )
flights["DEPARTURE_TIME"] = flights.apply(to_datetime, axis=1)

# write the data back to file without the index
flights.to_csv('flights.csv', index=False)

注意

您可以在此处找到代码文件

注意

注意:如pandas.read_csv文档中所述, 我们使用关键字参数low_memory=False来确保未按块加载数据,这可能会导致类型推断的问题,尤其是对于非常大的文件。

为了方便起见,这三个文件存储在以下 GitHub 位置

以下代码使用pixiedust.sampleData()方法将数据加载到对应于airlinesairportsflights的三个 Pandas DataFrame中:

airports = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/airports.csv")
airlines = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/airlines.csv")
flights = pixiedust.sampleData("https://github.com/DTAIEB/Thoughtful-Data-Science/raw/master/chapter%209/USFlightsAnalysis/flights.zip")

注意

您可以在此处找到代码文件

注意:GitHub URL 使用/raw/段,该段指示我们要下载原始文件,而不是相应 GitHub 页面的 HTML。

下一步是使用flights``DataFrame作为edge列表并将ELAPSED_TIME列中的值作为权重,将数据加载到networkx有向加权图对象中。 我们首先通过使用pandas.groupby()方法和以ORIGIN_AIRPORTDESTINATION_AIRPORT为键的多索引对它们进行分组,从而对所有与始发地和目的地具有相同机场的航班进行重复数据删除。 然后,从DataFrameGroupBy对象中选择ELAPSED_TIME列,并使用mean()方法汇总结果。 这将为我们提供一个新的DataFrame,其具有相同始发地和目的地机场的每个航班的平均平均值ELAPSED_TIME

edges = flights.groupby(["ORIGIN_AIRPORT","DESTINATION_AIRPORT"]) [["ELAPSED_TIME"]].mean()
edges

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Part 1 – Loading the US domestic flight data into a graph

按出发地和目的地分组的航班,平均平均飞行时间为ELAPSED_TIME

在使用此DataFrame创建有向图之前,我们需要将索引从多索引重置为常规单索引,将索引列转换为常规列。 为此,我们仅使用reset_index()方法,如下所示:

edges = edges.reset_index()
edges

注意

您可以在此处找到代码文件

现在,我们有了一个形状正确的DataFrame,可以用来创建有向图,如以下屏幕截图所示:

Part 1 – Loading the US domestic flight data into a graph

按始发地和目的地分组的航班,平均平均飞行时间为ELAPSED_TIME,并且只有一个索引

要创建有向加权图,我们使用 NetworkX from_pandas_edgelist()方法,该方法以 Pandas DataFrame作为输入源。 我们还指定了源列和目标列,以及权重列(在我们的示例中为ELAPSED_TIME)。 最后,我们告诉 NetworkX 我们想通过使用create_using关键字参数来创建有向图,并将DiGraph的实例作为值传递。

以下代码显示如何调用from_pandas_edgelist()方法:

flight_graph = nx.from_pandas_edgelist(
    flights, "ORIGIN_AIRPORT","DESTINATION_AIRPORT",
    "ELAPSED_TIME",
    create_using = nx.DiGraph() )

注意

您可以在此处找到代码文件

注意:NetworkX 支持通过从多种格式创建,包括字典,列表,NumPy 和 SciPy 矩阵,当然还有 Pandas。 您可以在此处找到有关这些转换函数的更多信息

我们可以通过直接打印其节点和边来快速验证我们的图是否具有正确的值:

print("Nodes: {}".format(flight_graph.nodes))
print("Edges: {}".format(flight_graph.edges))

注意

您可以在此处找到代码文件

产生以下输出(被截断):

Nodes: ['BOS', 'TYS', 'RKS', 'AMA', 'BUF', 'BHM', 'PPG', …, 'CWA', 'DAL', 'BFL']
Edges: [('BOS', 'LAX'), ('BOS', 'SJC'), ..., ('BFL', 'SFO'), ('BFL', 'IAH')]

我们还可以使用networkx中提供的内置绘图 API 创建更好的可视化,这些 API 支持多个渲染引擎,包括 Matplotlib,Graphviz AGraph 和带有 pydot 的 Graphviz

为简单起见,我们将使用 NetworkX draw()方法,该方法使用现成的 Matplotlib 引擎。 为了美化可视化效果,我们将配置为适当的宽度和高度(12, 12),并添加具有鲜艳色彩的色图(我们使用matplolib.cm中的coolspring色图,请参阅这里)。

以下代码显示了图可视化的实现:

import matplotlib.cm as cm
fig = plt.figure(figsize = (12,12))
nx.draw(flight_graph, arrows=True, with_labels=True,
        width = 0.5,style="dotted",
        node_color=range(len(flight_graph)),
        cmap=cm.get_cmap(name="cool"),
        edge_color=range(len(flight_graph.edges)),
        edge_cmap=cm.get_cmap(name="spring")
       )
plt.show()

注意

您可以在此处找到代码文件

产生以下结果:

Part 1 – Loading the US domestic flight data into a graph

使用 Matplotlib 快速可视化我们的有向图

在上一张图表中,节点使用称为spring_layout的默认图布局进行定位,这是一种强制控制的布局。 这种布局的一个好处是它可以迅速显示出具有最多边缘连接的节点,它们位于图的中心。 调用draw()方法时,可以使用pos关键字参数通过更改图布局。 networkx支持其他类型的布局,包括circular_layoutrandom_layoutshell_layoutspectral_layout

例如,使用random_layout

import matplotlib.cm as cm
fig = plt.figure(figsize = (12,12))
nx.draw(flight_graph, arrows=True, with_labels=True,
        width = 0.5,style="dotted",
        node_color=range(len(flight_graph)),
        cmap=cm.get_cmap(name="cool"),
        edge_color=range(len(flight_graph.edges)),
        edge_cmap=cm.get_cmap(name="spring"),
        pos = nx.random_layout(flight_graph)
       )
plt.show()

注意

您可以在此处找到代码文件

我们得到以下结果:

Part 1 – Loading the US domestic flight data into a graph

使用random_layout的飞行数据图

注意

注意:您可以在此处找到有关这些布局的更多信息

图的中心

关于该图要分析的下一个有趣的事情是它的中心指数,它使我们能够发现哪些节点是最重要的顶点。 作为练习,我们将计算四种类型的中心度索引:PageRank紧密度最短路径间隔。 然后,我们将扩充机场数据框架以为每个中心度索引添加一个列,并使用 PixieDust display()在 Mapbox 地图中可视化结果。

使用networkx可以很容易地计算图的程度。 只需使用flight_graph对象的degree属性,如下所示:

print(flight_graph.degree)

这将输出具有机场代码和程度索引的元组数组,如下所示:

[('BMI', 14), ('RDM', 8), ('SBN', 13), ('PNS', 18), ………, ('JAC', 26), ('MEM', 46)]

现在,我们想向机场数据帧添加DEGREE列,其中包含前一个数组中每个机场行的度值。 为此,我们需要创建一个具有两列的新DataFrameIATA_CODEDEGREE并在IATA_CODE上执行 Pandas merge()操作。

下图说明了合并操作:

Graph centrality

将度数DataFrame合并到机场DataFrame

以下代码显示了如何实现上述步骤。 我们首先通过遍历flight_path.degree输出来创建 JSON 有效负载,然后使用pd.DataFrame()构造器来创建DataFrame。 然后,我们以airportsdegree_df作为参数来使用pd.merge()。 我们还将on参数与值IATA_CODE一起使用,这是我们要在其上进行连接的键列:

degree_df = pd.DataFrame([{"IATA_CODE":k, "DEGREE":v} for k,v in flight_graph.degree], columns=["IATA_CODE", "DEGREE"])
airports_centrality = pd.merge(airports, degree_df, on='IATA_CODE')
airports_centrality

注意

您可以在此处找到代码文件

结果在以下屏幕快照中显示:

Graph centrality

使用DEGREE列增强的机场DataFrame

要可视化 Mapbox 地图中的数据,我们只需在airport_centrality``DataFrame上使用PixieDust.display()

display(airports_centrality)

以下屏幕截图显示了选项对话框:

Graph centrality

用于显示机场的 Mapbox 选项

在选项对话框中单击 OK 后,我们得到以下结果:

Graph centrality

以学位为中心向机场展示

对于其他中心指数,我们可以注意到,所有相应的计算函数都返回了一个 JSON 输出(与度属性的数组相对),其中IATA_CODE机场代码为键,而中心指数为值。

例如,如果我们使用以下代码计算 PageRank:

nx.pagerank(flight_graph)

我们得到以下结果:

{'ABE': 0.0011522441195896051,
 'ABI': 0.0006671948649909588,
 ...
 'YAK': 0.001558809391270303,
 'YUM': 0.0006214341604372096}

考虑到这一点,我们无需执行与degree相同的步骤,而是可以实现一个名为compute_centrality()的泛型函数,该函数将计算中心度和列名的函数作为参数,创建一个临时函数。 包含计算出的中心值的数据帧,并将其与airports_centrality数据帧合并。

以下代码显示了compute_centrality()的实现:

from six import iteritems
def compute_centrality(g, centrality_df, compute_fn, col_name, *args, **kwargs):
    # create a temporary DataFrame that contains the computed centrality values
    temp_df = pd.DataFrame(
        [{"IATA_CODE":k, col_name:v} for k,v in iteritems(compute_fn(g, *args, **kwargs))],
        columns=["IATA_CODE", col_name]
    )
    # make sure to remove the col_name from the centrality_df is already there
    if col_name in centrality_df.columns:
        centrality_df.drop([col_name], axis=1, inplace=True)
    # merge the 2 DataFrame on the IATA_CODE column
    centrality_df = pd.merge(centrality_df, temp_df, on='IATA_CODE')
    return centrality_df

注意

您可以在此处找到代码文件

我们现在可以简单地用三个计算函数nx.pagerank()nx.closeness_centrality()nx.betweenness_centrality()分别用列PAGE_RANKCLOSENESSBETWEENNESS调用来调用compute_centrality()方法,如以下代码所示:

airports_centrality = compute_centrality(flight_graph, airports_centrality, nx.pagerank, "PAGE_RANK")
airports_centrality = compute_centrality(flight_graph, airports_centrality, nx.closeness_centrality, "CLOSENESS")
airports_centrality = compute_centrality(
    flight_graph, airports_centrality, nx.betweenness_centrality, "BETWEENNESS", k=len(flight_graph))
airports_centrality

注意

您可以在此处找到代码文件

airports_centrality``DataFrame现在具有多余的列,如以下输出所示:

Graph centrality

使用PAGE_RANKCLOSENESSBETWEENNESS值增强的机场 DataFrame

作为练习,我们可以验证四个中心指数为顶级机场提供了一致的结果。 使用 Pandas nlargest()方法,我们可以获得四个索引的前 10 个机场,如以下代码所示:

for col_name in ["DEGREE", "PAGE_RANK", "CLOSENESS", "BETWEENNESS"]:
    print("{} : {}".format(
        col_name,
        airports_centrality.nlargest(10, col_name)["IATA_CODE"].values)
    )

注意

您可以在此处找到代码文件

产生以下结果:

DEGREE : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'EWR' 'LAX']
PAGE_RANK : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'SFO' 'LAX']
CLOSENESS : ['ATL' 'ORD' 'DFW' 'DEN' 'MSP' 'IAH' 'DTW' 'SLC' 'EWR' 'LAX']
BETWEENNESS : ['ATL' 'DFW' 'ORD' 'DEN' 'MSP' 'SLC' 'DTW' 'ANC' 'IAH' 'SFO']

正如我们所看到的,亚特兰大机场成为所有中心指数的最高机场。 作为练习,我们创建一个名为visualize_neighbors()的通用方法,该方法可视化给定节点的所有邻居,并使用 Atlanta 节点对其进行调用。 在这种方法中,我们通过向自身的所有邻居添加一条边来创建以父节点为中心的子图。 我们使用 NetworkX neighbors()方法来获取特定节点的所有邻居。

以下代码显示visualize_neighbors()方法的实现:

import matplotlib.cm as cm
def visualize_neighbors(parent_node):
    fig = plt.figure(figsize = (12,12))
    # Create a subgraph and add an edge from the parent node to all its neighbors
    graph = nx.DiGraph()
    for neighbor in flight_graph.neighbors(parent_node):
        graph.add_edge(parent_node, neighbor)
    # draw the subgraph
    nx.draw(graph, arrows=True, with_labels=True,
            width = 0.5,style="dotted",
            node_color=range(len(graph)),
            cmap=cm.get_cmap(name="cool"),
            edge_color=range(len(graph.edges)),
            edge_cmap=cm.get_cmap(name="spring"),
           )
    plt.show()

注意

您可以在此处找到代码文件

然后,我们在ATL节点上调用visualize_neighbors()方法:

visualize_neighbors("ATL")

产生以下输出:

Graph centrality

可视化顶级节点 ATL 及其邻居

通过使用著名的 Dijkstra 算法计算两个节点之间的最短路径,我们完成了第 1 部分部分。 我们将尝试使用不同的权重属性以检查是否获得了不同的结果。

例如,我们使用 NetworkX dijkstra_path()方法来计算马萨诸塞州波士顿洛根机场(BOS)和华盛顿州帕斯科三城市机场(PSC)之间的最短路径。

我们首先使用ELAPSED_TIME列作为权重属性:

注意

:提醒一下,ELAPSED_TIME是我们在本节前面计算的,具有相同出发地和目的地机场的每个航班的平均飞行时间。

nx.dijkstra_path(flight_graph, "BOS", "PSC", weight="ELAPSED_TIME")

哪个返回:

['BOS', 'MSP', 'PSC']

不幸的是,我们之前计算的中心度索引不是flight_graph DataFrame的一部分,因此将其用作weight属性的列名将不起作用。 但是dijkstra_path()还允许我们使用一个函数来动态计算权重。 由于我们想尝试不同的中心指数,因此我们需要创建一个工厂方法,该方法将为给定的中心指数创建函数参数。 此参数用作嵌套包装函数的闭包,该包装函数符合dijkstra_path()方法的weight参数。 我们也使用字典来记住给定机场的权重,因为该算法将为同一机场多次调用该函数。 如果权重不在缓存中,则使用centrality_indice_col参数在airports_centrality``DataFrame中查找权重。 由于 Dijkstra 算法偏爱距离较短的路径,因此通过获得中心值的倒数来计算最终权重。

以下代码显示compute_weight工厂方法的实现:

# use a cache so we don't recompute the weight for the same airport every time
cache = {}
def compute_weight(centrality_indice_col):
    # wrapper function that conform to the dijkstra weight argument
    def wrapper(source, target, attribute):
        # try the cache first and compute the weight if not there
        source_weight = cache.get(source, None)
        if source_weight is None:
            # look up the airports_centrality for the value
            source_weight = airports_centrality.loc[airports_centrality["IATA_CODE"] == source][centrality_indice_col].values[0]
            cache[source] = source_weight
        target_weight = cache.get(target, None)
        if target_weight is None:
            target_weight = airports_centrality.loc[airports_centrality["IATA_CODE"] == target][centrality_indice_col].values[0]
            cache[target] = target_weight
        # Return weight is inversely proportional to the computed weighted since
        # the Dijkstra algorithm give precedence to shorter distances
        return float(1/source_weight) + float(1/target_weight)
    return wrapper

注意

您可以在此处找到代码文件

现在,我们可以为每个中心性索引调用 NetworkX dijkstra_path()方法。 请注意,我们不使用BETWEENNESS,因为某些值等于零,因此不能用作权重。 在调用dijkstra_path()方法之前,我们还需要清除缓存,因为使用不同的中心性索引会为每个机场产生不同的值。

以下代码显示了如何为每个中心指数计算最短路径:

for col_name in ["DEGREE", "PAGE_RANK", "CLOSENESS"]:
    #clear the cache
    cache.clear()
    print("{} : {}".format(
        col_name,
        nx.dijkstra_path(flight_graph, "BOS", "PSC",
                         weight=compute_weight(col_name))
    ))

注意

您可以在此处找到代码文件

产生以下结果:

DEGREE : ['BOS', 'DEN', 'PSC']
PAGE_RANK : ['BOS', 'DEN', 'PSC']
CLOSENESS : ['BOS', 'DEN', 'PSC']

有趣的是,正如预期的那样,经过计算的最短路径对于三个中心指数而言都是相同的,它们都经过丹佛机场(这是最高的中央机场)。 但是,它与不同,后者是使用ELAPSED_TIME权重计算得出的值,而这会使我们通过明尼阿波利斯。

在本节中,我们展示了如何将美国航班数据加载到图数据结构中,如何计算不同的中心性指数,并使用它们来计算机场之间的最短路径。 我们还讨论了可视化图数据的不同方法。

注意

可在以下位置找到第 1 部分的完整笔记本

在下一节中,我们将创建USFlightsAnalysis PixieApp,以对这些分析进行操作。

第 2 部分——创建USFlightsAnalysis PixieApp

对于USFlightsAnalysis的第一个迭代,我们想实现一个简单的用户故事,该故事利用第 1 部分中创建的分析:

  • 欢迎屏幕将显示两个下拉控件,用于选择来源和目的地机场
  • 选择机场后,我们将显示一个图表,显示选定的机场及其直接邻居
  • 选择两个机场后,用户单击分析按钮以显示包含所有机场的 Mapbox 地图
  • 用户可以选择一个中心性指标作为复选框,以根据所选中心性显示最短的飞行路径

首先让我们看一下欢迎屏幕的实现,它是在USFlightsAnalysis PixieApp 的默认路由中实现的。 下面的代码定义了USFlightsAnalysis类,该类用@PixieApp装饰器装饰成一个 PixieApp。 它包含一个main_screen()方法,该方法用@route()装饰器装饰以使其成为默认路由。 此方法返回一个 HTML 片段,该片段将在 PixieApp 启动时用作欢迎屏幕。 HTML 片段由两部分组成:一部分显示用于选择始发机场的下拉控件,另一部分包含用于选择目标机场的下拉控件。 我们使用遍历每个机场的 Jinja2 {%for...%}循环(由get_airports()方法返回)来生成一组<options>元素。 在这些控件的每个控件下,我们添加一个占位符<div>元素,该元素将在选择机场时托管图可视化。

注意

注意:与往常一样,我们使用[[USFlightsAnalysis]]表示法表示该代码仅显示部分实现,因此,在提供完整实现之前,读者不应尝试按原样运行它。

稍后我们将解释为什么USFlightsAnalysis类继承自MapboxBase类。

[[USFlightsAnalysis]]
from pixiedust.display.app import *
from pixiedust.apps.mapboxBase import MapboxBase
from collections import OrderedDict

@PixieApp
class USFlightsAnalysis(MapboxBase):
    ...
    @route()
    def main_screen(self):
        return """
<style>
    div.outer-wrapper {
        display: table;width:100%;height:300px;
    }
    div.inner-wrapper {
        display: table-cell;vertical-align: middle;height: 100%;width: 100%;
    }
</style>
<div class="outer-wrapper">
    <div class="inner-wrapper">
        <div class="col-sm-6">
            <div class="rendererOpt" style="font-weight:bold">
                 Select origin airport:
            </div>
            <div>
                <select id="origin_airport{{prefix}}"
                        pd_refresh="origin_graph{{prefix}}">
                    <option value="" selected></option>
                    {%for code, airport in this.get_airports() %}
 <option value="{{code}}">{{code}} - {{airport}}</option>
 {%endfor%}
                </select>
            </div>
            <div id="origin_graph{{prefix}}" pd_options="visualize_graph=$val(origin_airport{{prefix}})"></div>
        </div>
        <div class="input-group col-sm-6">
            <div class="rendererOpt" style="font-weight:bold">
                 Select destination airport:
            </div>
            <div>
                <select id="destination_airport{{prefix}}"
                        pd_refresh="destination_graph{{prefix}}">
                    <option value="" selected></option>
                    {%for code, airport in this.get_airports() %}
 <option value="{{code}}">{{code}} - {{airport}}</option>
 {%endfor%}
                </select>
            </div>
            <div id="destination_graph{{prefix}}"
pd_options="visualize_graph=$val(destination_airport{{prefix}})">
            </div>
        </div>
    </div>
</div>
<div style="text-align:center">
    <button class="btn btn-default" type="button"
pd_options="org_airport=$val(origin_airport{{prefix}});dest_airport=$val(destination_airport{{prefix}})">
        <pd_script type="preRun">
            if ($("#origin_airport{{prefix}}").val() == "" || $("#destination_airport{{prefix}}").val() == ""){
                alert("Please select an origin and destination airport");
                return false;
            }
            return true;
        </pd_script>
        Analyze
    </button>
</div>
"""

def get_airports(self):
    return [tuple(l) for l in airports_centrality[["IATA_CODE", "AIRPORT"]].values.tolist()]

注意

您可以在此处找到代码文件

当用户选择始发机场时,将触发以 ID 为origin_graph{{prefix}}的占位符<div>元素为目标的pd_refresh。 反过来,此<div>元素使用状态visualize_graph=$val(origin_airport{{prefix}}触发路由。 提醒一下,$val()指令在运行时通过获取origin_airport{{prefix}}下拉元素的机场值来解析。 目的地机场使用类似的实现。

此处提供了visualize_graph路由的代码。 它只是调用我们在第 1 部分中实现的visualize_neighbors()方法,在第 2 部分中稍加更改,以添加一个可选图尺寸参数以适应该尺寸。 主机<div>元素。 提醒一下,我们也使用@captureOutput装饰器,因为visualize_neighbors()方法直接写入所选单元格的输出:

[[USFlightsAnalysis]]
@route(visualize_graph="*")
@captureOutput
def visualize_graph_screen(self, visualize_graph):
    visualize_neighbors(visualize_graph, (5,5))

注意

您可以在此处找到代码文件

Analyze按钮正在触发与org_airportdest_airport状态参数关联的compute_path_screen()路由。 我们还希望确保允许compute_path_screen()航线继续之前选择两个机场。 为此,我们将<pd_script>子元素与type="preRun"一起使用,其中包含将在触发路由之前执行的 JavaScript 代码。 如果我们想让路由继续进行,则该代码的合同规定返回布尔值true,否则返回false

对于Analyze按钮,我们检查两个下拉列表是否都具有值,如果是这种情况,则返回true,否则引发错误消息并返回false

<button class="btn btn-default" type="button" pd_options="org_airport=$val(origin_airport{{prefix}});dest_airport=$val(destination_airport{{prefix}})">
   <pd_script type="preRun">
 if ($("#origin_airport{{prefix}}").val() == "" || $("#destination_airport{{prefix}}").val() == ""){
 alert("Please select an origin and destination airport");
 return false;
 }
 return true;
   </pd_script>
      Analyze
   </button>

注意

您可以在此处找到代码文件

以下输出显示了选择 BOS 作为始发机场并且选择 PSC 作为目的地时的最终结果:

Part 2 – Creating the USFlightsAnalysis PixieApp

选择两个机场的欢迎屏幕

现在让我们看一下compute_path_screen()路由的实现,该路由负责显示所有机场的 Mapbox 地图和基于所选中心度索引的最短路径作为一层,这是叠加在整个地图上的额外可视化 。

以下代码显示了其实现:

[[USFlightsAnalysis]]
@route(org_airport="*", dest_airport="*")
def compute_path_screen(self, org_airport, dest_airport):
    return """
<div class="container-fluid">
    <div class="form-group col-sm-2" style="padding-right:10px;">
        <div><strong>Centrality Indices</strong></div>
 {% for centrality in this.centrality_indices.keys() %}
        <div class="rendererOpt checkbox checkbox-primary">
            <input type="checkbox"
                   pd_refresh="flight_map{{prefix}}"
pd_script="self.compute_toggle_centrality_layer('{{org_airport}}', '{{dest_airport}}', '{{centrality}}')">
            <label>{{centrality}}</label>
        </div>
 {%endfor%}
    </div>
    <div class="form-group col-sm-10">
        <h1 class="rendererOpt">Select a centrality index to show the shortest flight path
        </h1>
        <div id="flight_map{{prefix}}" pd_entity="self.airports_centrality" pd_render_onload>
            <pd_options>
 {
 "keyFields": "LATITUDE,LONGITUDE",
 "valueFields": "AIRPORT,DEGREE,PAGE_RANK,ELAPSED_TIME,CLOSENESS",
 "custombasecolorsecondary": "#fffb00",
 "colorrampname": "Light to Dark Red",
 "handlerId": "mapView",
 "quantiles": "0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0",
 "kind": "choropleth",
 "rowCount": "1000",
 "numbins": "5",
 "mapboxtoken": "pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4M29iazA2Z2gycXA4N2pmbDZmangifQ.-g_vE53SD2WrJ6tFX7QHmA",
 "custombasecolor": "#ffffff"
 }
            </pd_options>
        </div>
    </div>
</div>
"""

注意

您可以在此处找到代码文件

此屏幕的中心<div>元素是 Mapbox 地图,默认情况下显示所有机场的 Mapbox 地图。 如上面代码中的所示,<pd_options>子元素直接从相应的单元元数据中获取,我们在第 1 部分中配置了映射。

在左侧,我们使用centrality_indices变量上的 Jinja2 {%for …%}循环,生成了一组与每个中心度索引相对应的复选框。 我们在USFlightsAnalysis PixieApp 的setup()方法中初始化此变量,该方法保证在 PixieApp 启动时会被调用。 此变量是OrderedDict,其键为中心索引,值为在 Mapbox 渲染中将使用的配色方案:

[[USFlightsAnalysis]]
def setup(self):
   self.centrality_indices = OrderedDict([
      ("ELAPSED_TIME","rgba(256,0,0,0.65)"),
      ("DEGREE", "rgba(0,256,0,0.65)"),
      ("PAGE_RANK", "rgba(0,0,256,0.65)"),
      ("CLOSENESS", "rgba(128,0,128,0.65)")
  ])

注意

您可以在此处找到代码文件

以下输出显示未选择中心性索引的分析屏幕:

Part 2 – Creating the USFlightsAnalysis PixieApp

未选择中心指数的分析屏幕

现在我们到达步骤,用户选择中心度索引以触发最短路径搜索。 每个复选框都有一个pd_script属性,该属性调用compute_toggle_centrality_layer()方法。 此方法负责使用通过调用第 1 部分中讨论的compute_weight()方法生成的weight参数调用 NetworkX dijkastra_path()方法。 此方法返回一个数组,其中每个机场构成最短路径。 然后使用此路径创建一个 JSON 对象,该对象包含 GeoJSON 有效载荷,作为要在地图上显示的一组线。

在这一点上,值得暂停讨论什么是层。 是使用 GeoJSON 格式定义的,我们在第 5 章,“最佳做法和 PixieDust 高级内容”中对此进行了简要讨论。 提醒一下,GeoJSON 有效负载是具有特定架构的 JSON 对象,该架构除其他外还包含geometry元素,该元素定义了要绘制的对象的形状。

例如,我们可以使用LineString类型定义一条直线,并为直线的两端定义一个经度和纬度坐标数组:

{
    "geometry": {
        "type": "LineString",
        "coordinates": [
            [-93.21692, 44.88055],
 [-119.11903000000001, 46.26468]
        ]
    },
    "type": "Feature",
    "properties": {}
}

注意

您可以在此处找到代码文件

假设我们可以从最短的路径生成此 GeoJSON 有效负载,我们可能想知道如何将其传递给 PixieDust Mapbox 渲染器,以便可以显示它。 好了,机制很简单:Mapbox 渲染器将对主机 PixieApp 进行内省,以查找符合特定格式的任何类变量,然后使用它来生成要显示的 Mapbox 图层。 为了帮助遵循此机制,我们使用了我们先前简要介绍的MapboxBase工具类。 此类具有get_layer_index()方法,该方法采用唯一名称(我们使用centrality索引)作为参数并返回其索引。 它还需要一个额外的可选参数,以在尚不存在的情况下创建该图层。 然后,我们调用toggleLayer()方法,将图层索引作为参数传递以打开和关闭图层。

以下代码显示compute_toggle_centrality_layer()方法的实现,该方法实现了上述步骤:

[[USFlightsAnalysis]]
def compute_toggle_centrality_layer(self, org_airport, dest_airport, centrality):
    cache.clear()
    cities = nx.dijkstra_path(flight_graph, org_airport, dest_airport, weight=compute_weight(centrality))
    layer_index = self.get_layer_index(centrality, {
        "name": centrality,
        "geojson": {
            "type": "FeatureCollection",
            "features":[
                {"type":"Feature",
                 "properties":{"route":"{} to {}".format(cities[i], cities[i+1])},
                 "geometry":{
                     "type":"LineString",
                     "coordinates":[
                         self.get_airport_location(cities[i]),
 self.get_airport_location(cities[i+1])
                     ]
                 }
                } for i in range(len(cities) - 1)
            ]
        },
        "paint":{
 "line-width": 8,
 "line-color": self.centrality_indices[centrality]
 }
    })
 self.toggleLayer(layer_index)

注意

您可以在此处找到代码文件

使用get_airport_location()方法计算几何对象中的坐标,该方法查询我们在第 1 部分中创建的airports_centrality DataFrame,如以下代码所示:

[[USFlightsAnalysis]]
def get_airport_location(self, airport_code):
    row = airports_centrality.loc[airports["IATA_CODE"] == airport_code]
    if row is not None:
        return [row["LONGITUDE"].values[0], row["LATITUDE"].values[0]]
    return None

注意

您可以在此处找到代码文件

传递给get_layer_index()方法的图层对象具有以下属性:

  • name:唯一标识图层的字符串。
  • geojson:定义图层的特征和几何的 GeoJSON 对象。
  • url:仅当不存在geojson时使用。 指向返回 GeoJSON 有效负载的 URL。
  • paint:特定于 Mapbox 规范的可选额外属性,用于定义图层数据的样式,例如颜色,宽度和不透明度。
  • layout:特定于 Mapbox 规范的可选附加属性,用于定义图层数据的绘制方式,例如填充,可见性和符号。

注意

注意:您可以在此处找到有关 Mapbox 布局和绘画属性的更多信息

在前面的代码中,我们指定了额外的paint属性来配置line-widthline-color,这些属性是从setup()方法中定义的centrality_indices JSON 对象获取的。

以下输出显示了使用ELAPSED_TIME(红色)和DEGREE(绿色)中心指数从BOSPSC的最短飞行路线:

Part 2 – Creating the USFlightsAnalysis PixieApp

使用ELAPSED_TIMEDEGREE中心指数显示从 BOS 到 PSC 的最短路径

在本节中,我们构建了一个 PixieApp,该应用使用 PixieDust Mapbox 渲染器提供两个机场之间最短路径的可视化。 我们已经展示了如何使用MapboxBase实用工具类创建一个新图层,以在地图上添加更多信息。

注意

您可以在此处找到完整的第二部分的笔记本

在下一部分中,我们将添加与航班延误和相关航空公司有关的其他数据探索。

第 3 部分——向USFlightsAnalysis PixieApp 添加数据浏览

在本节中,我们想扩展USFlightsAnalysis PixieApp 的航线分析屏幕,以添加两个图表,以显示从所选始发机场起飞的每家航空公司的历史到达延迟:一个表示所有从始发机场起飞的航班,另一个表示与机场无关的所有航班。 这将为我们提供一种在视觉上比较特定机场的延误是好于还是差于其他机场的方法。

我们首先实现一种选择给定航空公司的航班的方法。 我们还添加了一个可选的机场参数,可用于控制我们是包含所有航班还是仅包含源自该机场的航班。 返回的DataFrame应该具有两列:DATEARRIVAL_DELAY

以下代码显示了此方法的实现:

def compute_delay_airline_df(airline, org_airport=None):
    # create a mask for selecting the data
    mask = (flights["AIRLINE"] == airline)
    if org_airport is not None:
        # Add the org_airport to the mask
        mask = mask & (flights["ORIGIN_AIRPORT"] == org_airport)
    # Apply the mask to the Pandas dataframe
    df = flights[mask]
    # Convert the YEAR, MONTH and DAY column into a DateTime
    df["DATE"] = pd.to_datetime(flights[['YEAR','MONTH', 'DAY']])
    # Select only the columns that we need
    return df[["DATE", "ARRIVAL_DELAY"]]

注意

您可以在此处找到代码文件

我们可以通过将其与来自波士顿的达美航班一起使用来测试上述代码。 然后我们可以调用 PixieDust display()方法来创建折线图,该折线图将在 PixieApp 中使用:

bos_delay = compute_delay_airline_df("DL", "BOS")
display(bos_delay)

在 PixieDust 输出中,我们选择折线图菜单,并配置选项对话框,如下所示:

Part 3 – Adding data exploration to the USFlightsAnalysis PixieApp

用于生成达美航空从波士顿起飞的到达延迟折线图的选项对话框

当单击 OK 时,我们得到以下图表:

Part 3 – Adding data exploration to the USFlightsAnalysis PixieApp

波士顿起飞的所有达美航班的延误图表

当我们要在 PixieApp 中使用此图表时,最好从编辑单元元数据对话框中复制 JSON 配置:

Part 3 – Adding data exploration to the USFlightsAnalysis PixieApp

需要为 PixieApp 复制的延迟图的 PixieDust display()配置

现在我们知道如何生成延迟图,我们可以开始设计 PixieApp。 我们首先更改主屏幕的布局,以使用TemplateTabbedApp助手类,该类免费提供给我们标签式布局。 整体分析屏幕现在由RouteAnalysisApp子 PixieApp 驱动,它包含两个选项卡:与SearchShortestRouteApp子 PixieApp 关联的Search Shortest Route选项卡和与AirlinesApp子 PixieApp 关联的Explore Airlines选项卡。

下图提供了新布局中涉及的所有类的高层流程:

Part 3 – Adding data exploration to the USFlightsAnalysis PixieApp

新的选项卡式布局类图

使用TemplateTabbedAppRouteAnalysisApp实现非常简单,如以下代码所示:

from pixiedust.apps.template import TemplateTabbedApp

@PixieApp
class RouteAnalysisApp(TemplateTabbedApp):
    def setup(self):
        self.apps = [
            {"title": "Search Shortest Route",
             "app_class": "SearchShortestRouteApp"},
            {"title": "Explore Airlines",
             "app_class": "AirlinesApp"}
        ]

注意

您可以在此处找到代码文件

SearchShortestRouteApp子 PixieApp 基本上是我们在第 2 部分中创建的主要 PixieApp 类的副本。 唯一的区别是,它是RouteAnalysisApp的子 PixieApp,它本身又是USFlightsAnalysis主 PixieApp 的子 PixieApp。 因此,我们需要一种机制将始发和目的地机场传递到各个子 PixieApps。 为此,我们在实例化RouteAnalysisApp子 PixieApp 时使用pd_options属性。

USFlightAnalysis类中,我们更改analyze_route方法以返回触发RouteAnalysisApp的简单<div>元素。 我们还为org_airportdest_airport添加了pd_options属性,如以下代码所示:

[[USFlightsAnalysis]]
@route(org_airport="*", dest_airport="*")
def analyze_route(self, org_airport, dest_airport):
    return """
<div pd_app="RouteAnalysisApp"
pd_options="org_airport={{org_airport}};dest_airport={{dest_airport}}"
     pd_render_onload>
</div>
        """

注意

您可以在此处找到代码文件

相反,在SearchShortestRouteApp子 PixieApp 的setup()方法中,我们从parent_pixieapp的选项字典中读取的org_airportdest_airport值,如以下代码所示 :

[[SearchShortestRouteApp]]
from pixiedust.display.app import *
from pixiedust.apps.mapboxBase import MapboxBase
from collections import OrderedDict

@PixieApp
class SearchShortestRouteApp(MapboxBase):
    def setup(self):
 self.org_airport = self.parent_pixieapp.options.get("org_airport")
 self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
        self.centrality_indices = OrderedDict([
            ("ELAPSED_TIME","rgba(256,0,0,0.65)"),
            ("DEGREE", "rgba(0,256,0,0.65)"),
            ("PAGE_RANK", "rgba(0,0,256,0.65)"),
            ("CLOSENESS", "rgba(128,0,128,0.65)")
        ])
        ...

注意

您可以在此处找到代码文件

注意:为简洁起见,省略了SearchShortestRouteApp的其余实现,因为它与第 2 部分完全相同。 要访问该实现,请参阅完整的第 3 部分笔记本。

最后要实现的 PixieApp 类是AirlinesApp,它将显示所有延迟图。 与SearchShortestRouteApp相似,我们从parent_pixieapp选项字典中存储org_airportdest_airport。 我们还计算了中所有航班超出给定org_airport的航空公司的元组列表(代码和名称)。 为此,我们在AIRLINE列上使用 Pandas groupby()方法并获取索引值的列表,如以下代码所示:

[[AirlinesApp]]
@PixieApp
class AirlinesApp():
    def setup(self):
        self.org_airport = self.parent_pixieapp.options.get("org_airport")
 self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
        self.airlines = flights[flights["ORIGIN_AIRPORT"] == self.org_airport].groupby("AIRLINE").size().index.values.tolist()
        self.airlines = [(a, airlines.loc[airlines["IATA_CODE"] == a]["AIRLINE"].values[0]) for a in self.airlines]

注意

您可以在此处找到代码文件

AirlinesApp的主屏幕中,我们使用 Jinja2 {%for...%}循环为每个航空公司生成一组行。 在每一行中,我们添加两个<div>元素,这些元素将保存给定航空公司的延误折线图:一个元素用于始发机场的航班,另一个元素用于该航空公司的所有航班。 每个<div>元素都有一个pd_options attribute,,其中org_airportdest_airport作为状态属性,从而触发delay_airline_screen路由。 我们还添加了delay_org_airport布尔状态属性,以表示要显示的延迟图类型。 为了确保立即显示<div>元素,我们还添加了pd_render_onload属性。

以下代码显示了AirlinesApp默认路由的实现:

[[AirlinesApp]]
@route()
    def main_screen(self):
        return """
<div class="container-fluid">
    {%for airline_code, airline_name in this.airlines%}
    <div class="row" style="max-e">
        <h1 style="color:red">{{airline_name}}</h1>
        <div class="col-sm-6">
            <div pd_render_onload pd_options="delay_org_airport=true;airline_code={{airline_code}};airline_name={{airline_name}}"></div>
        </div>
        <div class="col-sm-6">
            <div pd_render_onload pd_options="delay_org_airport=false;airline_code={{airline_code}};airline_name={{airline_name}}"></div>
        </div>
    </div>
    {%endfor%}
</div>
        """

注意

您可以在此处找到代码文件

delay_airline_screen()路由具有三个参数:

  • delay_org_airport:如果仅希望从始发机场起飞的航班,则为true;如果我们希望给定航空公司的所有航班,则为false。 我们使用此标志来构建掩码,以从飞行数据帧中过滤数据。
  • airline_code:给定航空公司的 IATA 代码。
  • airline_name:航空公司的全名。 在 Jinja2 模板中构建 UI 时,将使用它。

delay_airline_screen()方法的主体中,我们还计算了average_delay局部变量中所选数据的平均延迟。 提醒一下,为了在 Jinja2 模板中使用此变量,我们使用@templateArgs装饰器,该装饰器会自动使所有局部变量在 Jinja2 模板中可用。

包含图表的<div>元素具有pd_entity属性,该属性使用我们在本节开头创建的compute_delay_airline_df()方法。 但是,由于参数已更改,因此我们需要将该方法重写为类的成员:org_airport现在是类变量,delay_org_airport现在是字符串布尔值。 我们还添加了<pd_options>子元素和 PixieDust display() JSON 配置,该配置是从编辑单元元数据对话框复制的。

以下代码显示了delay_airline_screen()路由的实现:

[[AirlinesApp]]
@route(delay_org_airport="*",airline_code="*", airline_name="*")
    @templateArgs
    def delay_airline_screen(self, delay_org_airport, airline_code, airline_name):
        mask = (flights["AIRLINE"] == airline_code)
        if delay_org_airport == "true":
            mask = mask & (flights["ORIGIN_AIRPORT"] == self.org_airport)
        average_delay = round(flights[mask]["ARRIVAL_DELAY"].mean(), 2)
        return """
{%if delay_org_airport == "true" %}
<h4>Delay chart for all flights out of {{this.org_airport}}</h4>
{%else%}
<h4>Delay chart for all flights</h4>
{%endif%}
<h4 style="margin-top:5px">Average delay: {{average_delay}} minutes</h4>
<div pd_render_onload pd_entity="compute_delay_airline_df('{{airline_code}}', '{{delay_org_airport}}')">
    <pd_options>
    {
 "keyFields": "DATE",
 "handlerId": "lineChart",
 "valueFields": "ARRIVAL_DELAY",
 "noChartCache": "true"
 }
    </pd_options>
</div>
        """

注意

您可以在此处找到代码文件

compute_delay_airline_df()方法具有两个参数:对应于 IATA 代码的航空公司和delay_org_airport字符串布尔值。 我们已经介绍了此方法的实现,但此处提供了新的改编代码:

[[AirlinesApp]]
def compute_delay_airline_df(self, airline, delay_org_airport):
        mask = (flights["AIRLINE"] == airline)
        if delay_org_airport == "true":
            mask = mask & (flights["ORIGIN_AIRPORT"] == self.org_airport)
        df = flights[mask]
        df["DATE"] = pd.to_datetime(flights[['YEAR','MONTH', 'DAY']])
        return df[["DATE", "ARRIVAL_DELAY"]]

注意

您可以在此处找到代码文件

在分别以 BOS 和 PSC 作为始发地和目的地机场的情况下运行USFlightsAnalysis PixieApp,我们单击探索航空公司标签。

结果显示在以下屏幕截图中:

Part 3 – Adding data exploration to the USFlightsAnalysis PixieApp

所有从波士顿机场提供服务的航空公司的延线图

在本节中,我们将提供另一个示例,说明如何使用 PixieApp 编程模型来构建功能强大的仪表板,这些仪表板可为笔记本中开发的分析结果提供可视化和见解。

注意

可在以下位置找到USFlightsAnalysis PixieApp 的第 3 部分的完整笔记本

在下一节中,我们将建立一个 ARIMA 模型,尝试预测航班延误。

第 4 部分——创建 ARIMA 模型来预测航班延误

在第 8 章,“金融时间序列分析和预测”中,我们使用时间序列分析建立了预测金融股票的预测模型。 实际上,我们可以在航班延误中使用相同的技术,因为毕竟我们还在这里处理时间序列,因此在本节中,我们将遵循完全相同的步骤。 对于每个目的地机场和可选航空公司,我们将构建一个 Pandas DataFrame,其中包含匹配的航班信息。

注意

注意:我们将再次使用statsmodels库。 如果尚未安装,请确保进行安装,有关更多信息,请参考第 8 章,“金融时间序列分析和预测”。

例如,让我们集中研究以BOS为目的地的所有达美航空(DL)航班:

df = flights[(flights["AIRLINE"] == "DL") & (flights["ORIGIN_AIRPORT"] == "BOS")]

使用ARRIVAL_DELAY列作为时间序列的值,我们绘制 ACF 和 PACF 图以识别趋势和季节性,如以下代码所示:

import statsmodels.tsa.api as smt
smt.graphics.plot_acf(df['ARRIVAL_DELAY'], lags=100)
plt.show()

注意

您可以在此处找到代码文件

结果显示在以下屏幕截图中:

Part 4 – Creating an ARIMA model for predicting flight delays

ARRIVAL_DELAY数据的自相关函数

同样,我们还使用以下代码绘制部分自相关函数:

import statsmodels.tsa.api as smt
smt.graphics.plot_pacf(df['ARRIVAL_DELAY'], lags=50)
plt.show()

注意

您可以在此处找到代码文件

结果显示在这里:

Part 4 – Creating an ARIMA model for predicting flight delays

ARRIVAL_DELAY数据的部分自相关

从前面的图表中,我们可以假设数据具有趋势和/或季节性,并且不稳定。 使用我们在第 8 章,“金融时间序列分析和预测”中介绍的对数差异技术,对序列进行转换并使用 PixieDust display()方法将其可视化,如以下代码所示:

注意

注意:我们还确保通过先调用replace()方法将np.inf-np.inf替换为np.nan,然后再调用dropna()来删除具有 NA 和无穷大值的行。 方法删除具有np.nan值的所有行。

import numpy as np
train_set, test_set = df[:-14], df[-14:]
train_set.index = train_set["DEPARTURE_TIME"]
test_set.index = test_set["DEPARTURE_TIME"]
logdf = np.log(train_set['ARRIVAL_DELAY'])
logdf.index = train_set['DEPARTURE_TIME']
logdf_diff = pd.DataFrame(logdf - logdf.shift()).reset_index()
logdf_diff.replace([np.inf, -np.inf], np.nan, inplace=True)
logdf_diff.dropna(inplace=True)
display(logdf_diff)

注意

您可以在此处找到代码文件

以下屏幕截图显示了 PixieDust 选项对话框:

Part 4 – Creating an ARIMA model for predicting flight delays

用于ARRIVAL_DELAY数据的日志差异的选项对话框

单击 OK 后,我们得到以下结果:

注意

注意:运行前面的代码时,您可能无法获得与以下屏幕快照完全相同的图表。 这是因为我们在选项对话框中将要显示的行数配置为100,这意味着 PixieDust 在创建图表之前将采取大小为 100 的样本。

Part 4 – Creating an ARIMA model for predicting flight delays

ARRIVAL_DELAY数据的对数差异折线图

前面的图表看起来很平稳; 我们可以通过在对数差异上再次绘制 ACF 和 PACF 来加强这一假设,如以下代码所示:

smt.graphics.plot_acf(logdf_diff["ARRIVAL_DELAY"], lags=100)
plt.show()

注意

您可以在此处找到代码文件

结果如下:

Part 4 – Creating an ARIMA model for predicting flight delays

ACF 图表以获取ARRIVAL_DELAY数据的对数差异

在以下代码中,我们对 PACF 执行相同的操作:

smt.graphics.plot_pacf(logdf_diff["ARRIVAL_DELAY"], lags=100)
plt.show()

注意

您可以在此处找到代码文件

结果如下:

Part 4 – Creating an ARIMA model for predicting flight delays

PARI 图表以获取ARRIVAL_DELAY数据的对数差异

作为第 8 章,“金融时间序列分析和预测”的提醒,ARIMA 模型由三个阶组成:pdq。 从前面的两个图表中,我们可以推断出要构建的 ARIMA 模型的这些顺序:

  • 阶数p为 1 的自回归:对应于 ACF 第一次越过有效水平
  • 阶数d为 1 的集成:我们必须做一次对数差异
  • 阶数q为 1 的移动平均:对应于 PACF 首次超过有效水平

基于这些假设,我们可以使用statsmodels包构建 ARIMA 模型,并获取有关其残留误差的信息,如以下代码所示:

from statsmodels.tsa.arima_model import ARIMA

import warnings
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    arima_model_class = ARIMA(train_set['ARRIVAL_DELAY'],
                              dates=train_set['DEPARTURE_TIME'],
                              order=(1,1,1))
    arima_model = arima_model_class.fit(disp=0)
    print(arima_model.resid.describe())

注意

您可以在此处找到代码文件

结果如下所示:

count    13882.000000
mean         0.003116
std         48.932043
min       -235.439689
25%        -17.446822
50%         -5.902274
75%          6.746263
max       1035.104295
dtype: float64

如我们所见,的平均误差仅为 0.003,这非常好,因此我们准备使用train_set中的值运行模型,并可视化与实际值的差异。

以下代码使用 ARIMA plot_predict()方法创建图表:

def plot_predict(model, dates_series, num_observations):
    fig,ax = plt.subplots(figsize = (12,8))
    model.plot_predict(
        start = dates_series[len(dates_series)-num_observations],
        end = dates_series[len(dates_series)-1],
        ax = ax
    )
    plt.show()
plot_predict(arima_model, train_set['DEPARTURE_TIME'], 100)

注意

您可以在此处找到代码文件

结果如下所示:

Part 4 – Creating an ARIMA model for predicting flight delays

预测与实际

在上图中,我们可以清楚地看到预测线比实际值平滑得多。 该是有道理的,因为实际上,总是存在意想不到的延迟原因,这些延迟可以被视为异常值,因此很难建模。

我们仍然需要使用test_set使用模型尚未看到的数据来验证模型。 以下代码创建一个compute_test_set_predictions()方法,以使用 PixieDust display()方法比较预测数据和测试数据并可视化结果:

def compute_test_set_predictions(train_set, test_set):
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        history = train_set['ARRIVAL_DELAY'].values
        forecast = np.array([])
        for t in range(len(test_set)):
            prediction = ARIMA(history, order=(1,1,0)).fit(disp=0).forecast()
            history = np.append(history, test_set['ARRIVAL_DELAY'].iloc[t])
            forecast = np.append(forecast, prediction[0])
        return pd.DataFrame(
          {"forecast": forecast,
 "test": test_set['ARRIVAL_DELAY'],
 "Date": pd.date_range(start=test_set['DEPARTURE_TIME'].iloc[len(test_set)-1], periods = len(test_set))
 }
        )

results = compute_test_set_predictions(train_set, test_set)
display(results)

注意

您可以在此处找到代码文件

PixieDust 选项对话框如下所示:

Part 4 – Creating an ARIMA model for predicting flight delays

预测与测试比较折线图的“选项”对话框

单击 OK 后,我们得到以下结果:

Part 4 – Creating an ARIMA model for predicting flight delays

预测与测试数据折线图

现在,通过在RouteAnalysisApp主屏幕上添加一个名为Flight Delay Prediction的第三个选项卡,我们可以将该模型集成到USFlightsAnalysis PixieApp 中。 此选项卡将由一个名为PredictDelayApp的新子 PixieApp 驱动,它将使用户选择使用 Dijkstra 最短路径算法并以DEGREE作为中心度指标计算出的最短路径的航段。 用户还可以选择一家航空公司,在这种情况下,训练数据将仅限于所选航空公司运营的航班。

在以下代码中,我们创建PredictDelayApp子 PixieApp 并实现setup()方法,该方法为选定的始发和目的地机场计算 Dijkstra 最短路径:

[[PredictDelayApp]]
import warnings
import numpy as np
from statsmodels.tsa.arima_model import ARIMA

@PixieApp
class PredictDelayApp():
    def setup(self):
        self.org_airport = self.parent_pixieapp.options.get("org_airport")
        self.dest_airport = self.parent_pixieapp.options.get("dest_airport")
        self.airlines = flights[flights["ORIGIN_AIRPORT"] == self.org_airport].groupby("AIRLINE").size().index.values.tolist()
        self.airlines = [(a, airlines.loc[airlines["IATA_CODE"] == a]["AIRLINE"].values[0]) for a in self.airlines]
        path = nx.dijkstra_path(flight_graph, self.org_airport, self.dest_airport, weight=compute_weight("DEGREE"))
        self.paths = [(path[i], path[i+1]) for i in range(len(path) - 1)]

PredictDelayApp的默认路由中,我们使用 Jinja2 {%for..%}循环来构建两个下拉框,以显示航班航段和航空公司,如以下代码所示:

[[PredictDelayApp]]
@route()
    def main_screen(self):
        return """
<div class="container-fluid">
    <div class="row">
        <div class="col-sm-6">
            <div class="rendererOpt" style="font-weight:bold">
                Select a flight segment:
            </div>
            <div>
                <select id="segment{{prefix}}" pd_refresh="prediction_graph{{prefix}}">
                    <option value="" selected></option>
                    {%for start, end in this.paths %}
 <option value="{{start}}:{{end}}">{{start}} -> {{end}}</option>
 {%endfor%}
                </select>
            </div>
        </div>
        <div class="col-sm-6">
            <div class="rendererOpt" style="font-weight:bold">
                Select an airline:
            </div>
            <div>
                <select id="airline{{prefix}}" pd_refresh="prediction_graph{{prefix}}">
                    <option value="" selected></option>
                    {%for airline_code, airline_name in this.airlines%}
 <option value="{{airline_code}}">{{airline_name}}</option>
 {%endfor%}
                </select>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12">
            <div id="prediction_graph{{prefix}}"
                pd_options="flight_segment=$val(segment{{prefix}});airline=$val(airline{{prefix}})">
            </div>
        </div>
    </div>
</div>
        """

注意

您可以在此处找到代码文件

这两个下拉菜单具有pd_refresh属性,该属性指向 ID 为prediction_graph{{prefix}}<div>元素。 触发后,此<div>元素使用flight_segmentairline状态属性调用predict_screen()路由。

predict_screen()路由中,我们使用flight_segmentairline参数创建训练数据集,构建一个可预测模型的 ARIMA 模型,并在折线图中可视化结果,以比较预测值和实际值。

注意

时间序列预测模型仅限于接近实际数据的预测,并且由于我们只有 2015 年的数据,因此我们无法真正使用此模型来预测更多最新数据。 当然,在生产应用中,假定我们拥有最新的航班数据,因此这不会成为问题。

以下代码显示了predict_screen()路由的实现:

[[PredictDelayApp]]
@route(flight_segment="*", airline="*")
 @captureOutput
    def predict_screen(self, flight_segment, airline):
        if flight_segment is None or flight_segment == "":
            return "<div>Please select a flight segment</div>"
 airport = flight_segment.split(":")[1]
 mask = (flights["DESTINATION_AIRPORT"] == airport)
        if airline is not None and airline != "":
 mask = mask & (flights["AIRLINE"] == airline)
        df = flights[mask]
        df.index = df["DEPARTURE_TIME"]
        df = df.tail(50000)
 df = df[~df.index.duplicated(keep='first')]
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            arima_model_class = ARIMA(df["ARRIVAL_DELAY"], dates=df['DEPARTURE_TIME'], order=(1,1,1))
            arima_model = arima_model_class.fit(disp=0)
            fig, ax = plt.subplots(figsize = (12,8))
            num_observations = 100
            date_series = df["DEPARTURE_TIME"]
 arima_model.plot_predict(
 start = str(date_series[len(date_series)-num_observations]),
 end = str(date_series[len(date_series)-1]),
 ax = ax
 )
            plt.show()

注意

您可以在此处找到代码文件

在下面的代码中,我们还希望确保对数据集索引进行重复数据删除,以避免在绘制结果时出错。 这可以通过使用df = df[~df.index.duplicated(keep='first')]过滤重复索引来完成。

剩下要做的最后一件事是将PredictDelayApp子 PixieApp 连接到RouteAnalysisApp,如以下代码所示:

from pixiedust.apps.template import TemplateTabbedApp

@PixieApp
class RouteAnalysisApp(TemplateTabbedApp):
    def setup(self):
        self.apps = [
            {"title": "Search Shortest Route",
             "app_class": "SearchShortestRouteApp"},
            {"title": "Explore Airlines",
             "app_class": "AirlinesApp"},
            {"title": "Flight Delay Prediction",
 "app_class": "PredictDelayApp"}
        ]

注意

您可以在此处找到代码文件

当我们像上一节中一样使用 BOS 和 PSC 运行USFlightsAnalysis PixieApp 时。 在飞行延迟预测标签中,我们选择BOS -> DEN飞行段。

结果如下所示:

Part 4 – Creating an ARIMA model for predicting flight delays

波士顿至丹佛航班段的预测

在本节中,我们展示了如何使用时间序列预测模型根据历史数据预测航班延误。

注意

您可以在此处找到完整的笔记本

提醒一下,虽然只是一个示例应用,仍有很大的改进空间,但使用 PixieApp 编程模型进行数据分析的技术将在其他任何项目中同样应用。

总结

在本章中,我们讨论了图及其相关的图论,并探讨了其数据结构和算法。 我们还简要介绍了networkx Python 库,该库提供了丰富的用于处理和可视化图形的 API 集。 然后,我们将这些技术应用于构建示例应用,该应用通过将飞行数据视为一个图问题(以机场为顶点并沿边缘飞行)来分析飞行数据。 与往常一样,我们还展示了如何将这些分析操作化为一个简单但功能强大的仪表板,该仪表板可以直接在 Jupyter 笔记本中运行,然后可以选择通过 PixieGateway 微服务作为 Web 分析应用进行部署。

本章完成了一系列示例应用,涵盖了许多重要的行业用例。 在下一章中,我对本书的主题提供了一些最终的想法,即通过使数据的使用变得简单且所有人都可以访问来弥合数据科学与工程学之间的鸿沟。

十、最终思想

"我们正在创造和雇用人员来填补"新领"职位–在网络安全,数据科学,人工智能和认知业务等领域扮演着全新的角色。"

Ginni Rometty,IBM 董事长兼首席执行官

再次感谢读者,祝贺您阅读这些长章节并尝试了部分或全部示例代码的漫长旅程。 我试图在深入研究特定主题的基础知识(例如深度学习或时间序列分析)与为从业者提供全面的示例代码之间提供良好的组合。 我特别希望您发现在单个 Jupyter 笔记本中将数据科学分析与 PixieApp 应用编程模型紧密集成的想法有趣且新颖。 但是,最重要的是,我希望您发现它有用,并且可以在您自己的项目中以及与您自己的团队一起重用。

在第 1 章,"开发人员对数据科学的观点"开头,我使用了 Drew's Conway 维恩图(这是我的最爱之一)来表示什么是数据科学。 以及为什么数据科学家被广泛认为是独角兽。 考虑到 Drew Conway 的所有方面,我想扩展此图,以表示开发人员在数据科学领域的重要且不断增长的作用,如下图所示:

Final Thoughts

Drew 的数据科学 Conway Venn 图现在包括开发人员

我现在想利用上一章的内容对未来以及对 AI 和数据科学的期望发表自己的看法。

前瞻性思维——对 AI 和数据科学的期望

这是我非常喜欢的部分,因为我无需表达准确率就可以表达前瞻性意见,因为根据定义,这些只是我的观点😊。

正如我在第 1 章,"开发人员对数据科学的观点"中所解释的那样,我相信 AI 和数据科学将继续存在,它们将继续对现有行业造成破坏。 在可预见的未来,最有可能以加速的速度发展。 这肯定会影响工作的总数,并且类似于我们过去看到的其他技术革命(农业,工业,信息等),有些将消失,而新的将被创造。

2016 年,IBM 董事长兼首席执行官 Ginny Rometty 在致唐纳德·特朗普总统的一封信中,讨论了通过创建她称为"新领"的新型工作来更好地为 AI 革命做准备的需要,如以下摘录所示:

"在当今的 IBM 工作并不总是需要大学学位;在我们美国的一些中心,多达三分之一的员工不到四年制学位。最重要的是相关技能,有时通过假期训练获得。 此外,我们正在创造和雇用人员来填补"新领"职位-在网络安全,数据科学,人工智能和认知业务等领域扮演全新角色。"

如果我们成功地实现了数据科学的民主化,那么这些"新领子"工作就只能创造足够的数量,因为数据科学是 AI 的命脉,每个人都需要以某种能力参与进来; 开发人员,业务线用户,数据工程师等。 不难想象,对这些新型工作的需求将如此之大,以至于传统的学术途径将无法满足需求。 相反,该行业将有责任通过制定旨在重新培训所有可能面临裁员风险的现有工人的新计划来填补这一空白。 将会出现类似于 Apple 的Everyone Can Code程序的新程序; 也许像任何人都可以做数据科学。 我还认为 MOOC大规模开放在线课程的缩写)将发挥更大的作用,今天,由于主要 MOOC 参与者之间建立了许多合作关系, 例如 Coursera 和 edX,以及像 IBM 这样的公司(请参阅这个页面)。

公司还可以做其他事情,以便更好地为 AI 和数据科学革命做准备。 在第 1 章和"开发人员对数据科学的观点"中,我讨论了数据科学策略的三个支柱,它们可以帮助我们实现这一宏伟的目标:数据 ,服务和工具。

在服务方面,公有云的高速增长在很大程度上促进了多个领域的高质量服务的整体增长:数据持久性,认知,流传输等。 亚马逊,Facebook,谷歌,IBM 和 Microsoft 等提供商在以服务为先的方法以及强大的平台支持下为服务开发者提供一致体验的创新能力建设中发挥着领导作用。 随着越来越多的强大服务以越来越快的速度发布,这一趋势将继续加速。

一个很好的例子是称为 AlphaZero 的 Google 自学习 AI,它在 4 小时内自学了国际象棋,并继续击败国际象棋冠军。 另一个很好的例子来自 IBM 最近宣布的辩论者项目,这是第一个可以对人类进行辩论的 AI 系统。 复杂的话题。 这些类型的进步将继续推动越来越多的强大服务的可用性,包括开发人员在内的每个人都可以使用它。 聊天机器人是已成功实现民主化的服务的另一个示例,因为开发人员从未如此轻松地创建包含对话功能的应用。 我相信,随着时间的流逝,使用这些服务将变得越来越容易,使开发人员能够构建令人惊奇的新应用,而这些我们今天甚至还无法想象。

在数据方面,我们需要比现在更轻松地访问高质量数据。 我想到的一个模型来自电视节目24。 全面披露; 我喜欢看电视并喜欢看电视连续剧,我认为其中一些可以很好地指示技术发展的方向。 在24中,反恐特工杰克·鲍尔(Jack Bauer)有 24 小时制止坏人造成灾难性事件。 看着那个节目,我总是惊讶于数据从指挥中心的分析员传回杰克鲍尔的手机如此容易,或者给定仅需几分钟即可解决的数据问题, 分析人员能够召集来自不同系统(卫星图像,记录系统等)的数据,对坏蛋进行零介入; 例如,我们正在寻找最近 2 个月内在指定半径内购买了此类化学品的人。 哇! 从我的角度来看,这就是数据科学家访问和处理数据应该多么容易且毫不费力。 我相信我们通过诸如 Jupyter 笔记本之类的工具在朝着这个目标迈进,该工具充当控制平面,用于将数据源与处理它们的服务和分析相连接。 Jupyter 笔记本将工具带到了数据中,而不是相反,从而大大降低了想要参与数据科学的任何人的入门成本。

参考

十一、附录 A:PixieApp 快速参考

本附录是开发人员快速参考指南,提供了所有 PixieApp 属性的摘要。

注解

  • @PixieApp: 必须添加到任何 PixieApp 类的类注解。

    参数:无

    例子:

    from pixiedust.display.app import *
    @PixieApp
    class MyApp():
        pass
    
  • @route: 需要使用方法注解来表示方法(可以具有任何名称)与路由相关联。

    参数:**kwargs。 表示路由定义的关键字参数(键值对)。 PixieApp 调度器将根据以下规则将当前内核请求与路由进行匹配:

    • 参数数量最多的路由将首先被评估。
    • 所有参数都必须匹配才能选择路由。 参数值可以使用*表示任何值都将匹配。
    • 如果未找到路由,则选择默认路由(不带参数的路由)。
    • route参数的每个键可以是过渡状态(由pd_options属性定义),也可以是持久状态(PixieApp 类的字段,在显式更改之前一直存在)。
    • 该方法可以具有任意数量的参数。 调用方法时,PixieApp 调度器将尝试将方法参数与具有相同名称的路由参数进行匹配。

    返回值:该方法必须返回将注入到前端的 HTML 片段(除非使用@captureOutput注解)。 该方法可以利用 Jinja2 模板语法生成 HTML。 HTML 模板可以访问一定数量的变量:

    • this:引用 PixieApp 类(请注意,由于 Jinja2 框架本身已使用self,因此我们使用this代替了self

    • 前缀:PixieApp 实例唯一的字符串 ID

    • 实体:请求的当前数据实体

    • 方法参数:可以在 Jinja2 模板中以变量形式访问方法的所有参数

      from pixiedust.display.app import *
      @PixieApp
      class MyApp():
          @route(key1="value1", key2="*")
          def myroute_screen(self, key1, key2):
              return "<div>fragment: Key1 = {{key1}} - Key2 = {{key2}}"
      

    例子:

    注意

    您可以在此处找到代码文件

  • @templateArgs: 允许在 Jinja2 模板中使用任何局部变量的注解。 请注意,不能将@templateArgs@captureOutput结合使用:

    参数:无

    例子:

    from pixiedust.display.app import *
    @PixieApp
    class MyApp():
        @route(key1="value1", key2="*")
        @templateArgs
        def myroute_screen(self, key1, key2):
            local_var = "some value"
            return "<div>fragment: local_var = {{local_var}}"
    

    注意

    您可以在此处找到代码文件

  • @captureOutput: 可以使用路由方法更改合同的注解,这样就不必再返回 HTML 片段。 相反,方法主体可以像在笔记本单元中那样简单地输出结果。 该框架将捕获输出并将其作为 HTML 返回。 请注意,在这种情况下,您不能使用 Jinja2 模板。

    参数:无

    例子:

    from pixiedust.display.app import *
    import matplotlib.pyplot as plt
    @PixieApp
    class MyApp():
        @route()
        @captureOutput
        def main_screen(self):
            plt.plot([1,2,3,4])
            plt.show()
    

    注意

    您可以在此处找到代码文件

  • @Logger: 通过向类中添加日志记录方法来添加日志记录函数:debug, warn, info, error, critical, exception

    参数:无

    例子:

    from pixiedust.display.app import *
    from pixiedust.utils import Logger
    @PixieApp
    @Logger()
    class MyApp():
        @route()
        def main_screen(self):
            self.debug("In main_screen")
            return "<div>Hello World</div>"
    

    注意

    您可以在此处找到代码文件

    注意

    您可以在此处找到代码文件

自定义 HTML 属性

这些可以与任何常规 HTML 元素一起使用以配置内核请求。 当元素接收到点击或更改事件时,或者在 HTML 片段完成加载后,PixieApp 框架可以触发这些请求。

  • pd_options: 根据以下格式定义用于内核请求的瞬时状态的键值对列表:pd_options="key1=value1;key2=value2;..."。 当与pd_entity属性结合使用时,pd_options属性将调用 PixieDust display()API。 在这种情况下,您可以从使用display() API 的单独笔记本单元的元数据中获取值。 当在display()模式下使用pd_options时,为方便起见,建议通过创建名为<pd_options>的子元素并使用 JSON 值作为文本来使用pd_options的 JSON 表示法。

    pd_options作为子元素调用display()的示例:

    <div pd_entity>
        <pd_options>
            {
                "mapboxtoken": "XXXXX",
                "chartsize": "90",
                "aggregation": "SUM",
                "rowCount": "500",
                "handlerId": "mapView",
                "rendererId": "mapbox",
                "valueFields": "IncidntNum",
                "keyFields": "X,Y",
                "basemap": "light-v9"
            }
        </pd_options>
    </div>
    

    注意

    您可以在此处找到代码文件

    pd_options作为 HTML 属性的示例:

    <!-- Invoke a route that displays a chart -->
    <button type="submit" pd_options="showChart=true" pd_target="chart{{prefix}}">
        Show Chart
    </button>
    

    注意

    您可以在此处找到代码文件

  • pd_entity:仅用于在特定数据上调用display() API。 必须与pd_options结合使用,其中键值对将用作display()的参数。 如果未为pd_entity属性指定任何值,则假定该实体为传递给启动 PixieApp 的run方法的实体。 pd_entity值可以是在笔记本中定义的变量或 PixieApp 的字段(例如pd_entity="df"),也可以是使用点表示法的对象的字段(例如pd_entity="obj_instance.df")。

  • pd_target: 默认情况下,内核请求的输出会注入到整个输出单元格或对话框中(如果您使用runInDialog = "true"作为run方法的参数)。 但是,您可以使用pd_target = "elementId"指定将接收输出的目标元素。 (请注意,elementId必须存在于当前视图中。)

    例子:

    <div id="chart{{prefix}}">
    <button type="submit" pd_options="showChart=true" pd_target="chart{{prefix}}">
        Show Chart
    </button>
    </div>
    

    注意

    您可以在此处找到代码文件

  • pd_script: 这将调用任意 Python 代码作为内核请求的一部分。 可以与其他属性(例如pd_entitypd_options)结合使用。 请务必注意,必须遵守 Python 缩进规则以避免运行时错误。

    如果 Python 代码包含多行,建议使用pd_script作为子元素并将代码存储为文本。

    例子:

    <!-- Invoke a method to load a dataframe before visualizing it -->
    <div id="chart{{prefix}}">
    <button type="submit"
        pd_entity="df"
        pd_script="self.df = self.load_df()"
        pd_options="handlerId=dataframe"
        pd_target="chart{{prefix}}">
        Show Chart
    </button>
    </div>
    

    注意

    您可以在此处找到代码文件

  • pd_app: 这将通过其完全限定的类名称动态调用一个单独的 PixieApp。 pd_options属性可用于传递路由参数以调用 PixieApp 的特定路由。

    例子:

    <div pd_render_onload
         pd_option="show_route_X=true"
         pd_app="some.package.RemoteApp">
    </div>
    

    注意

    您可以在此处找到代码文件

  • pd_render_onload: 与用户单击元素或发生更改事件时相反,这应用于在加载时触发内核请求。 pd_render_onload属性可以与定义请求的任何其他属性组合,例如pd_optionspd_script。 请注意,此属性仅应与div元素一起使用。

    例子:

    <div pd_render_onload>
        <pd_script>
    print('hello world rendered on load')
        </pd_script>
    </div>
    

    注意

    您可以在此处找到代码文件

  • pd_refresh: 即使没有事件(单击或更改事件)发生,它也用于强制 HTML 元素执行内核请求。 如果未指定任何值,则刷新当前元素,否则,将刷新具有在值中指定的 ID 的元素。

    例子:

    <!-- Update state before refreshing a chart -->
    <button type="submit"
        pd_script="self.show_line_chart()"
        pd_refresh="chart{{prefix}}">
        Show line chart
    </button>
    

    注意

    您可以在此处找到代码文件

  • pd_event_payload: 这将发出具有指定有效内容的 PixieApp 事件。 该属性遵循与pd_options相同的规则:

    • 每个键值对必须使用key=value表示法进行编码

    • 该事件将在点击或更改时触发

    • 支持$val()指令以动态注入用户输入的输入

    • 使用<pd_event_payload>子项输入原始 JSON。

      <button type="submit" pd_event_payload="type=topicA;message=Button clicked">
          Send event A
      </button>
      <button type="submit">
          <pd_event_payload>
          {
              "type":"topicA",
              "message":"Button Clicked"
          }
          </pd_event_payload>
          Send event A
      </button>
      

    例子:

    注意

    您可以在此处找到代码文件

  • pd_event_handler: 订阅者可以通过声明一个<pd_event_handler>子元素来监听事件,该子元素可以接受任何 PixieApp 内核执行属性,例如pd_optionspd_script。 这个元素必须使用pd_source属性来过滤他们想要处理的事件。 pd_source属性可以包含以下值之一:

    • targetDivId:仅接受来自具有指定 ID 的元素的事件

    • type:仅接受指定类型的事件。

      <div class="col-sm-6" id="listenerA{{prefix}}">
          Listening to button event
          <pd_event_handler
              pd_source="topicA"
              pd_script="print(eventInfo)"
              pd_target="listenerA{{prefix}}">
          </pd_event_handler>
      </div>
      

    例子:

    注意

    您可以在此处找到代码文件

    注意:将pd_source用作*表示将接受所有事件。

  • pd_refresh_rate: 这用于以毫秒为单位的指定间隔重复执行元素。 当您要轮询特定变量的状态并在 UI 中显示结果时,这很有用。

    例子:

    <div pd_refresh_rate="3000"
        pd_script="print(self.get_status())">
    </div>
    

    注意

    您可以在此处找到代码文件

方法

  • setup: 这是由 PixieApp 实现的用于初始化其状态的可选方法。 在 PixieApp 运行之前将被自动调用。

    参数:无

    例子:

    def setup(self):
        self.var1 = "some initial value"
        self.pandas_dataframe = pandas.DataFrame(data)
    

    注意

    您可以在此处找到代码文件

  • run: 这将启动 PixieApp。

    参数:

    • 实体:(可选)数据集作为输入传递到 PixieApp。 可以用pd_entity属性引用,也可以直接作为pixieapp_entity字段引用。

    • **kwargs:运行时传递给 PixieApp 的关键字参数。 例如,使用runInDialog="true"将在对话框中启动 PixieApp。

      app = MyPixieApp()
      app.run(runInDialog="true")
      

    例子:

  • invoke_route: 这用于以编程方式调用路由。

    参数:

    • 路由方法:要调用的方法。

    • **kwargs:要传递给route方法的关键字参数。

      app.invoke_route(app.route_method, arg1 = "value1", arg2 = "value2")
      

    例子:

  • getPixieAppEntity:用于检索调用run()方法时传递的当前 PixieApp 实体(可以为None)。 通常从 PixieApp 内部调用getPixieAppEntity(),即:

    self.getPixieAppEntity()
    
posted @ 2025-10-27 09:00  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报