Streamlit-数据科学第二版-全-

Streamlit 数据科学第二版(全)

原文:annas-archive.org/md5/ceb02068667ed96ace7c50fbb2a5b490

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

2010 年代的数据科学家和机器学习工程师主要进行静态分析。我们创建文档来传达决策,填充了图表和指标,展示我们的发现或我们创建的模型。创建完整的 Web 应用,让用户与分析互动,至少可以说是繁琐的!这时,Streamlit 应运而生,它是一个为数据工作者量身打造的 Python 库,旨在每一步都为用户提供便捷的 Web 应用创建体验。

Streamlit 缩短了数据驱动的 web 应用开发时间,让数据科学家可以在几小时内使用 Python 创建 Web 应用原型,而不再是几天。

本书采用实践式学习方式,帮助你掌握让你迅速上手 Streamlit 的技巧和窍门。你将从创建一个基本应用的 Streamlit 基础开始,并逐步建立这个基础,生成高质量的图形数据可视化,并测试机器学习模型。随着你逐步深入,你将学习如何通过个人及工作相关的数据驱动型 Web 应用实例,并了解一些更复杂的话题,如使用 Streamlit 组件、美化应用,以及快速部署你的新应用。

本书的适用人群

本书适合数据科学家、机器学习工程师或爱好者,尤其是那些想要使用 Streamlit 创建 Web 应用的人。无论你是初级数据科学家,想要部署你的第一个机器学习项目,以提升简历,还是资深数据科学家,想通过动态分析说服同事,本书都适合你!

本书内容概览

第一章Streamlit 简介,通过创建你的第一个应用来教授 Streamlit 的基本知识。

第二章上传、下载和操作数据,探讨了数据;数据应用需要数据!你将学习如何在生产应用中高效、有效地使用数据。

第三章数据可视化,讲解如何在 Streamlit 应用中使用你最喜欢的 Python 可视化库。无需学习新的可视化框架!

第四章使用 Streamlit 进行机器学习与人工智能,讲解了机器学习。曾经想过在几小时内将你新开发的复杂机器学习模型部署到用户可用的应用中吗?在这里,你将获得深入的示例和技巧,包括与 Hugging Face 和 OpenAI 模型的合作。

第五章在 Streamlit 社区云上部署 Streamlit,讲解了 Streamlit 自带的一键部署功能。你将在这里学到如何消除部署过程中的摩擦!

第六章美化 Streamlit 应用,介绍了 Streamlit 充满的各种功能,帮助你打造华丽的 Web 应用。你将在这一章学到所有的小技巧和窍门。

第七章探索 Streamlit 组件,教你如何通过开源集成(即 Streamlit 组件)利用 Streamlit 周围蓬勃发展的开发者生态系统。就像 LEGO,一样更强大。

第八章使用 Hugging Face 和 Heroku 部署 Streamlit 应用,教你如何使用 Hugging Face 和 Heroku 部署 Streamlit 应用,作为 Streamlit Community Cloud 的替代方案。

第九章连接数据库,将帮助你将生产数据库中的数据添加到 Streamlit 应用中,从而扩展你能制作的应用种类。

第十章使用 Streamlit 改进求职申请,将帮助你通过 Streamlit 应用向雇主展示你的数据科学能力,涵盖从简历制作应用到面试带回家任务的应用。

第十一章数据项目 – 在 Streamlit 中进行项目原型制作,介绍了如何为 Streamlit 社区和其他用户制作应用,这既有趣又富有教育意义。你将通过一些项目示例,学习如何开始自己的项目。

第十二章Streamlit 高级用户,提供了更多关于 Streamlit 的信息,Streamlit 作为一个年轻的库,已经被广泛使用。通过对 Streamlit 创始人、数据科学家、分析师和工程师的深入访谈,向最优秀的人学习。

致谢

本书的完成离不开我的技术审阅者 Chanin Nantasenamat 的帮助。你可以在 X/Twitter 上找到他,链接为 twitter.com/thedataprof,也可以在 YouTube 上找到他,链接为 www.youtube.com/dataprofessor。所有错误由我负责,但所有避免的错误都归功于他!

最大化利用本书

本书假设你至少是 Python 初学者,这意味着你已经熟悉基本的 Python 语法,并且之前接受过 Python 的教程或课程。本书同样适合对数据科学感兴趣的读者,涵盖统计学和机器学习等主题,但并不要求具备数据科学背景。如果你知道如何创建列表、定义变量,并且写过 for 循环,那么你已经具备足够的 Python 知识来开始了!

如果你正在使用本书的数字版,建议你自己输入代码,或者通过本书的 GitHub 仓库获取代码(下节将提供链接)。这样做可以帮助你避免因复制和粘贴代码而产生的潜在错误。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,链接为 github.com/tylerjrichards/Streamlit-for-Data-Science。如果代码有更新,它会在这些 GitHub 仓库中同步更新。

我们还提供了来自我们丰富书籍和视频目录中的其他代码包,访问链接 github.com/PacktPublishing/。快去看看吧!

下载彩色图片

我们还提供一个包含本书中截图和图表彩色图像的 PDF 文件。您可以在此下载:packt.link/6dHPZ

使用的规范

本书中使用了几个文本规范:

文本中的代码:表示文本中的代码词语、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个例子:“…它的格式将是ec2-10-857-84-485.compute-1.amazonaws.com。我编造了这些数字,但你的应该接近这个格式。”

一块代码的格式如下:

import pandas as pd
penguin_df = pd.read_csv('penguins.csv')
print(penguin_df.head()) 

任何命令行输入或输出都以以下形式编写:

git add .
git commit -m 'added heroku files'
git push 

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个例子:“我们将使用Amazon Elastic Compute Cloud,简称Amazon EC2。”

提示或重要说明

以这种形式出现。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:通过电子邮件联系 feedback@packtpub.com,并在邮件主题中提及书名。如果您对本书的任何内容有问题,请通过 questions@packtpub.com 与我们联系。

勘误:虽然我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在本书中发现错误,感谢您将其报告给我们。请访问 www.packtpub.com/submit-errata,点击提交勘误,并填写表格。

盗版:如果您在互联网上发现我们作品的任何非法副本,感谢您提供该地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关内容的链接。

如果您有兴趣成为作者:如果您在某个主题上有专业知识,并且有兴趣撰写或贡献书籍内容,请访问 authors.packtpub.com

分享您的想法

一旦您阅读完《Streamlit for Data Science, Second Edition》,我们非常希望听到您的想法!请点击这里直接进入亚马逊评价页面并分享您的反馈。

您的评价对我们和技术社区非常重要,能帮助我们确保提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢随时随地阅读,但又无法携带印刷版书籍吗?

您购买的电子书是否无法与您的设备兼容?

不用担心,现在每本 Packt 书籍都附带免费的无 DRM 版 PDF。

在任何设备上随时随地阅读,搜索、复制并粘贴您最喜欢的技术书籍中的代码,直接导入到您的应用程序中。

优惠不仅如此,你还可以每天通过电子邮件独享折扣、新闻通讯以及精彩的免费内容

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接

packt.link/free-ebook/9781803248226

  1. 提交你的购买凭证

  2. 就这些!我们会将你的免费 PDF 和其他福利直接发送到你的邮箱

第一章:Streamlit 简介

Streamlit 是创建数据应用的最快方式。它是一个开源的 Python 库,帮助你构建用于共享分析结果、构建复杂的互动体验,并在新的机器学习模型上进行迭代的 Web 应用。此外,开发和部署 Streamlit 应用非常快速且灵活,通常将应用开发时间从几天缩短到几小时。

在本章中,我们将从 Streamlit 基础知识开始。我们将学习如何下载和运行示例 Streamlit 应用,如何使用我们自己的文本编辑器编辑示例应用,如何组织我们的 Streamlit 应用,最后,如何制作我们自己的应用。然后,我们将探索 Streamlit 中的数据可视化基础。我们将学习如何接受一些初始用户输入,然后通过文本为我们的应用添加一些修饰。到本章结束时,你应该能够熟练地开始制作自己的 Streamlit 应用!

特别地,我们将涵盖以下主题:

  • 为什么选择 Streamlit?

  • 安装 Streamlit

  • 组织 Streamlit 应用

  • Streamlit 绘图示例

  • 从零开始制作一个应用

在我们开始之前,我们将首先了解技术要求,确保我们具备了开始所需的一切。

技术要求

以下是本章所需的安装和设置:

  • 本书的要求是下载 Python 3.9(或更高版本)(www.python.org/downloads/)并拥有一个文本编辑器来编辑 Python 文件。任何文本编辑器都可以。我使用的是 VS Code(code.visualstudio.com/download)。

  • 本书的某些部分使用 GitHub,推荐拥有一个 GitHub 账户(github.com/join)。理解如何使用 Git 对于本书并非必要,但总是很有用。如果你想入门,这个链接有一个实用的教程:guides.github.com/activities/hello-world/

  • 本书还需要一定的 Python 基础。如果你还不熟悉,可以通过这个教程(docs.python.org/3/tutorial/)或其他免费、随手可得的教程花些时间深入了解 Python,准备好后再回来继续。我们还需要安装 Streamlit 库,我们将在后面的章节中进行安装,章节名为 安装 Streamlit

为什么选择 Streamlit?

在过去十年中,数据科学家已成为公司和非营利组织越来越宝贵的资源。他们帮助做出数据驱动的决策,提升流程效率,并实现机器学习模型以大规模改善这些决策。数据科学家的一个痛点是,在他们发现新的见解或建立新的模型后,如何展示这些结果。如何展示动态结果、新模型或复杂的分析给同事看呢?他们可以发送一个静态的可视化,这在某些情况下有效,但对于那些依赖相互关联或需要用户输入的复杂分析就行不通了。他们还可以创建一个 Word 文档(或者将 Jupyter 笔记本导出为文档),该文档将文本和可视化结合起来,但它依然无法整合用户输入,并且使得结果难以复现。另一种选择是,使用 Flask 或 Django 等框架从头开始构建整个网页应用,然后再想办法将整个应用部署到 AWS 或其他云服务商上。

这些选项没有一个真正有效。许多方法都很慢,不能接受用户输入,或者在数据科学中非常关键的决策过程方面不够理想。

这就是 Streamlit 的魅力所在。Streamlit 注重速度和互动性。它是一个帮助你构建和开发 Python 网页应用的框架。它内置了方便的方法,可以处理从用户输入(如文本和日期)到显示使用最流行和强大的 Python 图形库绘制的互动图表的所有需求。

过去两年,我一直在构建各种类型的 Streamlit 应用,从个人作品集中的数据项目,到为数据科学的家庭作业任务快速构建的应用,再到为工作中可重复分析构建的迷你应用。当我开始这段旅程时,我在 Meta(当时是 Facebook)工作,但在本书的第一版发布后,我如此热爱 Streamlit 应用的开发,以至于我加入了 Streamlit 团队。不久后,数据云公司 Snowflake 收购了 Streamlit。这本书并没有得到 Snowflake 的赞助,我当然不代表 Snowflake 发言,但我真心相信,Streamlit 对你和你的工作来说,可能和对我一样有价值。

我写这本书是为了帮助你迅速掌握知识,以便你能够加快学习进程,在几分钟到几个小时内构建网页应用,而不是几天。如果这正是你所需要的,继续阅读吧!

我们将分三个部分进行学习,首先介绍 Streamlit,然后带你逐步构建自己的基础 Streamlit 应用程序。在第二部分,我们将扩展这些知识,涵盖更高级的主题,例如生产环境部署方法以及使用 Streamlit 社区创建的组件来构建越来越美观和可用的 Streamlit 应用程序。最后一部分,我们将重点采访那些在工作、学术界和数据科学学习中使用 Streamlit 的高级用户。在我们开始之前,我们需要先设置好 Streamlit,并讨论本书示例的结构。

安装 Streamlit

为了运行任何 Streamlit 应用程序,必须先安装 Streamlit。我使用了名为 pip 的包管理器来安装它,但你也可以使用任何你选择的包管理器进行安装(例如,brew)。本书使用的是 Streamlit 版本 1.13.0 和 Python 3.9,但它也应该适用于更新的版本。

在本书中,我们将结合使用终端命令和 Python 脚本中的代码。我们会明确指示在哪个位置运行代码,以便尽可能清晰。要安装 Streamlit,请在终端中运行以下代码:

pip install streamlit 

既然我们已经下载了 Streamlit,我们可以通过命令行直接调用它,使用以下代码启动 Streamlit 的演示:

streamlit hello 

花些时间浏览 Streamlit 的演示,并快速浏览任何你觉得有趣的代码!我们将借用并编辑绘图示例背后的代码,该示例展示了 Streamlit 中绘图和动画的结合。在深入之前,我们先花点时间讨论一下如何组织 Streamlit 应用程序。

组织 Streamlit 应用程序

本书中我们创建的每个 Streamlit 应用程序都应包含在它自己的文件夹中。虽然我们可能会想为每个 Streamlit 应用程序创建新的文件,但这种做法会养成不好的习惯,等到我们讨论如何部署 Streamlit 应用并处理权限和数据时,这个问题会影响我们。

我建议你为将要在本书中创建的所有应用程序准备一个独立的文件夹。我把它命名为 streamlit_apps。以下命令将创建一个名为 streamlit_apps 的新文件夹,并将其设置为当前工作目录:

mkdir streamlit_apps
cd streamlit_apps 

本书的所有代码都可以在 github.com/tylerjrichards/Getting-Started-with-Streamlit-for-Data-Science 上找到,但我强烈建议你通过手动编码进行练习。本书后面将介绍如何创建多页面应用程序,这实际上让我们可以在一个中央单一应用中拥有许多小型数据应用。确保我们的 Streamlit 应用程序组织良好,这将有助于我们实现这一目标!

Streamlit 绘图示例

首先,我们将通过再现之前在 Streamlit 演示中看到的绘图演示,来开始学习如何制作 Streamlit 应用,所用的 Python 文件是我们自己创建的。为了做到这一点,我们将执行以下操作:

  1. 创建一个 Python 文件,用来容纳所有的 Streamlit 代码。

  2. 使用演示中给出的绘图代码。

  3. 进行一些小的编辑来练习。

  4. 在本地运行我们的文件。

我们的第一步是创建一个名为 plotting_app 的文件夹,来容纳我们的第一个示例。以下代码会在终端中运行时创建此文件夹,将工作目录切换到 plotting_app,并创建一个空的 Python 文件,我们将其命名为 plot_demo.py

mkdir plotting_app
cd plotting_app
touch plot_demo.py 

现在,我们已经创建了一个名为 plot_demo.py 的文件,使用任何文本编辑器打开它(如果你还没有编辑器,我个人推荐 VS Code(code.visualstudio.com/download))。当你打开它时,将以下代码复制粘贴到你的 plot_demo.py 文件中:

import streamlit as st
import time
import numpy as np
progress_bar = st.sidebar.progress(0)
status_text = st.sidebar.empty()
last_rows = np.random.randn(1, 1)
chart = st.line_chart(last_rows)
for i in range(1, 101):
    new_rows = last_rows[-1, :] + np.random.randn(5, 1).cumsum(axis=0)
    status_text.text("%i%% Complete" % i)
    chart.add_rows(new_rows)
    progress_bar.progress(i)
    last_rows = new_rows
    time.sleep(0.05)
progress_bar.empty()
# Streamlit widgets automatically run the script from top to bottom. Since
# this button is not connected to any other logic, it just causes a plain
# rerun.
st.button("Re-run") 

这段代码完成了几件事。首先,它导入了所需的所有库,并在 Streamlit 的原生图形框架中创建了一个线性图表,起点为从均值为 0、方差为 1 的正态分布中随机抽取的一个数。然后,它运行一个 for 循环,不断从正态分布中抽取 5 个新的随机数,并将其加到之前的和中,同时等待 1/20 秒,以便我们能看到图表的变化,模拟动画效果。

到本书结束时,你将能够非常快速地制作类似的应用。但现在,让我们在本地运行它,在终端中输入以下代码:

streamlit run plot_demo.py 

这应该会在默认的 Web 浏览器中打开一个新标签页,显示你的应用。我们应该看到应用运行,正如下图所示。你的应用不会显示完全相同的内容,因为每次运行时都会生成随机数,但除此之外,应用应该看起来是一样的!

图 1.1:绘图演示输出

这是我们运行每个 Streamlit 应用的方法:首先调用 streamlit run,然后将 Streamlit 指向包含应用代码的 Python 脚本。现在,让我们在应用中做一些小的修改,以便更好地理解 Streamlit 的工作原理。以下代码将改变我们在图表上绘制的随机数数量,但你可以根据自己的需求做任何修改。使用以下代码进行修改,在你选择的文本编辑器中保存更改,并再次运行文件:

import streamlit as st
import time
import numpy as np
progress_bar = st.sidebar.progress(0)
status_text = st.sidebar.empty()
last_rows = np.random.randn(1, 1)
chart = st.line_chart(last_rows)
for i in range(1, 101):
    new_rows = last_rows[-1, :] + np.random.randn(50, 1).cumsum(axis=0)
    status_text.text("%i%% Complete" % i)
    chart.add_rows(new_rows)
    progress_bar.progress(i)
    last_rows = new_rows
    time.sleep(0.05)
progress_bar.empty()
# Streamlit widgets automatically run the script from top to bottom. Since
# this button is not connected to any other logic, it just causes a plain
# rerun.
st.button("Re-run") 

你应该注意到 Streamlit 检测到源文件发生了变化,并提示你是否希望重新运行文件。如果需要,可以点击 Rerun(或者选择 Always rerun,如果你希望此行为为默认设置,我几乎总是这么做),然后观察应用的变化。

随意尝试对绘图应用进行一些修改,以熟悉它!一旦准备好,我们就可以继续创建自己的应用了。

从零开始制作一个应用

现在我们已经尝试过别人制作的应用,让我们来制作自己的应用吧!这个应用将专注于使用中心极限定理,它是统计学的一个基本定理,说明如果我们从任何分布中进行足够多的有放回的随机抽样,那么我们样本的均值分布将近似正态分布。

我们不会通过应用来证明这个定理,而是尝试生成一些图表,帮助解释中心极限定理的威力。首先,让我们确保我们在正确的目录中(此时是我们之前创建的 streamlit_apps 文件夹),新建一个名为 clt_app 的文件夹,并放入一个新文件。

以下代码会创建一个名为 clt_app 的新文件夹,并再次创建一个空的 Python 文件,这次命名为 clt_demo.py

mkdir clt_app
cd clt_app
touch clt_demo.py 

每当我们启动一个新的 Streamlit 应用时,我们都需要确保导入 Streamlit(在本书中和其他地方通常别名为 st)。Streamlit 为每种类型的内容(文本、图表、图片以及其他媒体)提供了独特的函数,我们可以将它们作为构建所有应用的基础模块。我们首先使用的函数是 st.write(),它是一个接受字符串(正如我们稍后将看到的,它几乎可以接受任何 Python 对象,例如字典)的函数,并将其按调用顺序直接写入我们的网页应用中。当我们调用一个 Python 脚本时,Streamlit 会按顺序浏览文件,每当遇到某个函数时,就为该部分内容分配一个顺序插槽。这使得使用起来非常简单,因为你可以编写所有需要的 Python 代码,当你想让某个内容出现在你创建的应用上时,只需使用 st.write(),一切就绪。

在我们的 clt_demo.py 文件中,我们可以从基本的 'Hello World' 输出开始,使用 st.write(),并用以下代码:

import streamlit as st
st.write('Hello World') 

现在,我们可以通过在终端中运行以下代码来进行测试:

streamlit run clt_demo.py 

我们应该能在应用中看到字符串 'Hello World' 的输出,至此一切正常。以下截图展示了我们的应用在 Safari 中的样子:

图 1.2:Hello World 应用

在这个截图中有三点需要注意。首先,我们看到的是我们写的字符串,这很好。接下来,我们看到 URL 指向 localhost:8501,这只是告诉我们我们正在本地托管此应用(即,它不在互联网上),通过 8501 端口。我们不需要了解计算机上关于端口系统或 传输控制协议TCP)的任何内容。这里需要注意的重点是,这个应用是本地的,运行在你的计算机上。在本书后面,我们将学习如何将本地创建的应用通过链接与任何人分享!第三个重要点是右上角的汉堡菜单图标。以下截图展示了点击该图标后的效果:

图 1.3:图标选项

这是 Streamlit 应用的默认选项面板。在本书中,我们将详细讨论这些选项,特别是一些不太直观的选项,比如清除缓存。现在我们只需要知道的是,如果我们想重新运行应用或查找设置或文档,我们可以使用这个图标找到几乎所有需要的内容。

当我们托管应用以便其他人使用时,他们会看到相同的图标,但会有一些不同的选项(例如,用户将无法清除缓存)。我们稍后会更详细地讨论这个问题。现在回到我们的中心极限定理应用!

下一步将是生成一个我们想要从中带放回抽样的分布。我在这里选择了二项分布。我们可以将以下代码解读为使用 Python 包NumPy模拟 1,000 次抛硬币,并打印出这 1,000 次抛硬币中的正面朝上的平均次数:

import streamlit as st
import numpy as np
binom_dist = np.random.binomial(1, .5, 100)
st.write(np.mean(binom_dist)) 

现在,鉴于我们对中心极限定理的了解,我们可以预期,如果我们从binom_dist中多次抽样,那么这些样本的均值将近似于正态分布。

我们已经讨论过st.write()函数。我们接下来要向 Streamlit 应用写入内容的方式是通过图形。st.pyplot()是一个函数,它让我们能够使用流行的matplotlib库的所有功能,并将我们的matplotlib图形推送到 Streamlit。一旦我们在matplotlib中创建了一个图形,我们就可以明确地告诉 Streamlit 将其写入到我们的应用中,方法是使用st.pyplot()函数。所以,现在我们来总结一下!这个应用模拟了 1,000 次抛硬币,并将这些值存储在我们称之为binom_dist的列表中。然后,我们从这个列表中随机抽取(带放回)100 个样本,计算均值,并将这个均值存储在巧妙命名的变量list_of_means中。我们重复这个过程 1,000 次(这其实有些过头——我们可以只用几十个样本),然后绘制直方图。完成之后,以下代码的结果应该显示一个钟形分布:

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
binom_dist = np.random.binomial(1, .5, 1000)
list_of_means = []
for i in range(0, 1000):
     list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
fig, ax = plt.subplots()
ax = plt.hist(list_of_means)
st.pyplot(fig) 

每次运行这个应用时都会生成一个新的钟形曲线。当我运行它时,我的钟形曲线如下图所示。如果你的图形不完全是接下来图中的样子(但仍然是钟形曲线!),那也没问题,因为我们的代码使用了随机抽样:

图 1.4:钟形曲线

正如你可能已经注意到的,我们首先通过调用plt.subplots()创建了一个空的图形和空的坐标轴,然后将我们创建的直方图赋值给ax变量。正因如此,我们能够明确地告诉 Streamlit 在我们的 Streamlit 应用中显示这个图形。

这是一个重要的步骤,因为在 Streamlit 的版本中,我们也可以跳过这个步骤,不将直方图赋值给任何变量,然后直接调用st.pyplot()。以下代码采用了这种方法:

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
binom_dist = np.random.binomial(1, .5, 1000)
list_of_means = []
for i in range(0, 1000):
     list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
plt.hist(list_of_means)
st.pyplot() 

我不推荐这种方法,因为它可能会给你一些意想不到的结果。举个例子,我们首先想制作一个均值的直方图,然后再制作一个只包含数字 1 的新列表的直方图。

请花点时间猜测以下代码会做什么。我们会得到几个图形?输出是什么?

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
binom_dist = np.random.binomial(1, .5, 1000)
list_of_means = []
for i in range(0, 1000):
     list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
plt.hist(list_of_means)
st.pyplot()
plt.hist([1,1,1,1])
st.pyplot() 

我预计这会显示两个直方图,第一个是 list_of_means 的直方图,第二个是 1s 列表的直方图:

图 1.5:两张直方图的故事

然而,实际结果有所不同!第二个直方图的数据包含了第一个和第二个列表的数据!当我们调用 plt.hist() 而不将输出赋值给任何变量时,matplotlib 会将新的直方图追加到旧的图形上,而这个图形是全局存储的,Streamlit 会将这个新的图形推送到我们的应用中。如果你的 matplotlib 版本较新,你也可能会收到 PyplotGlobalUseWarning 警告。别担心,我们将在下一节解决这个问题!

这是解决此问题的一种方法。如果我们显式创建两个图形,我们可以在生成图形后随时调用 st.pyplot() 函数,这样可以更好地控制图形的放置位置。以下代码显式地分开了两个图形:

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
binom_dist = np.random.binomial(1, .5, 1000)
list_of_means = []
for i in range(0, 1000):
     list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
fig1, ax1 = plt.subplots()
ax1 = plt.hist(list_of_means)
st.pyplot(fig1)
fig2, ax2 = plt.subplots()
ax2 = plt.hist([1,1,1,1])
st.pyplot(fig2) 

上述代码通过首先使用 plt.subplots() 定义每个图形和坐标轴的单独变量,然后将直方图分配给相应的坐标轴,分别绘制两个直方图。之后,我们可以使用创建的图形调用 st.pyplot(),它会生成以下应用:

图 1.6:固定的直方图

从上面的图中我们可以清楚地看到,两个直方图现在已经分开了,这是我们期望的行为。在 Streamlit 中,我们经常会绘制多个可视化图表,并且在本书剩余部分我们将使用这种方法。

Matplotlib 是一个非常流行的数据可视化库,但在数据应用中使用时存在一些严重缺陷。它默认不支持交互,外观也不算特别漂亮,并且在非常大的应用中可能会导致性能下降。稍后在本书中,我们将切换到性能更高且支持交互的库。

现在,开始接收用户输入吧!

在 Streamlit 应用中使用用户输入

到目前为止,我们的应用只是展示可视化的一种花哨方式。但大多数网页应用都需要接收用户输入或是动态的,而不是静态的可视化。幸运的是,Streamlit 提供了许多用于接收用户输入的函数,它们根据我们希望输入的对象而有所不同。有自由文本输入 st.text_input();单选按钮 st.radio();数字输入 st.number_input();以及更多对构建 Streamlit 应用非常有帮助的函数。本书中我们将详细探索其中的大多数,但我们从数字输入开始。

在前面的例子中,我们假设我们抛的硬币是公平的,正反面各有 50/50 的机会。现在让我们让用户决定正面朝上的概率,将其赋值给一个变量,并将这个值作为输入用于我们的二项分布。数字输入函数需要一个标签、最小值、最大值和默认值,我已经在以下代码中填写了这些内容:

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
perc_heads = st.number_input(label = 'Chance of Coins Landing on Heads', min_value = 0.0, max_value = 1.0, value = .5)
binom_dist = np.random.binomial(1, perc_heads, 1000)
list_of_means = []
for i in range(0, 1000):
     list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
fig, ax = plt.subplots()
ax = plt.hist(list_of_means, range=[0,1])
st.pyplot(fig) 

上述代码使用st.number_input()函数来收集我们的百分比,将用户输入赋值给一个变量(perc_heads),然后使用该变量更改我们之前使用的二项分布函数的输入。它还将我们的直方图的X轴设置为始终在 0 到 1 之间,以便我们更好地观察到随着输入变化而产生的变化。试着稍微玩一下这个应用;改变数字输入,注意每次用户输入变化时,应用是如何响应的。例如,当我们将数字输入设置为.25时,这里是一个结果:

图 1.7:当我们将数字输入设置为.25 时的结果示例

正如你可能注意到的,每次我们改变脚本的输入时,Streamlit 都会重新运行整个应用程序。这是默认行为,并且对于理解 Streamlit 的性能非常重要;在本书后面的部分,我们将探讨几种方法,允许我们更改这个默认行为,比如添加缓存或表单!我们还可以使用st.text_input()函数接受文本输入,就像我们之前处理数字输入一样。接下来的代码段接收文本输入并将其赋值给我们图表的标题:

import streamlit as st  
import numpy as np  
import matplotlib.pyplot as plt
perc_heads = st.number_input(label='Chance of Coins Landing on Heads', min_value=0.0,  max_value=1.0, value=.5)
graph_title = st.text_input(label='Graph Title')
binom_dist = np.random.binomial(1, perc_heads, 1000)
list_of_means = []
for i in range(0, 1000):
list_of_means.append(np.random.choice(binom_dist, 100, replace=True).mean())
fig, ax = plt.subplots()
plt.hist(list_of_means, range=[0,1])
plt.title(graph_title)
st.pyplot(fig) 

这段代码创建了一个 Streamlit 应用,包含两个输入框,一个是数字输入框,另一个是文本输入框,并使用这两个输入框来改变我们的 Streamlit 应用。最后,这会生成一个 Streamlit 应用,呈现出下图所示,带有动态标题和概率:

图 1.8:一个带有动态标题和概率的 Streamlit 应用

现在我们已经稍微处理了一些用户输入,接下来让我们更深入地讨论文本和 Streamlit 应用。

精致的修饰 – 向 Streamlit 添加文本

我们的应用程序是功能齐全的,但缺少一些精致的细节。我们之前讨论过st.write()函数,Streamlit 文档称它是 Streamlit 命令的瑞士军刀。几乎我们将st.write()包裹的任何内容默认都会起作用,它应该是我们在不确定最佳路径时的首选函数。

除了st.write(),我们还可以利用其他内置函数来格式化文本,如st.title()st.header()st.markdown()st.subheader()。使用这五个函数可以轻松地在我们的 Streamlit 应用中格式化文本,并保持较大应用的一致性。

更具体地说,st.title()会在我们的应用中放置一大块文本,st.header()使用比st.title()稍小的字体,st.subheader()使用更小的字体。除此之外,st.markdown()允许任何熟悉 Markdown 的人在我们的 Streamlit 应用中使用这种流行的标记语言。让我们在接下来的代码中尝试其中的一些:

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
st.title('Illustrating the Central Limit Theorem with Streamlit')
st.subheader('An App by Tyler Richards')
st.write(('This app simulates a thousand coin flips using the chance of heads input below,'
     'and then samples with replacement from that population and plots the histogram of the'
     ' means of the samples in order to illustrate the central limit theorem!'))
perc_heads = st.number_input(
    label='Chance of Coins Landing on Heads', min_value=0.0, max_value=1.0, value=.5)
binom_dist = np.random.binomial(1, perc_heads, 1000)
list_of_means = []
for i in range(0, 1000):
    list_of_means.append(np.random.choice(
        binom_dist, 100, replace=True).mean())
fig, ax = plt.subplots()
ax = plt.hist(list_of_means)
st.pyplot(fig) 

上述代码添加了一个大标题(st.title()),并在其下方添加了一个较小的副标题(st.subheader()),接着在副标题下方添加了更小的文本(st.write())。我们还将前面的长文本字符串拆分成了三个更小的字符串,以提高可读性,并便于在文本编辑器中进行编辑。它应当呈现如下截图。请注意,由于我们使用的是随机生成的数据,因此,如果你的直方图看起来略有不同,这是完全可以接受的(并且是预期中的)!

图 1.9:中心极限定理应用

这就是我们关于中心极限定理的说明。你可以尝试 Streamlit 提供的其他文本书写选项(比如 st.markdown(),它可以解释并书写 Markdown 样式的文本)来进一步探索应用程序的创建。

总结

在本章中,我们首先学习了如何组织文件和文件夹,为本书的剩余部分做准备,并快速介绍了如何下载 Streamlit。接着,我们构建了第一个 Streamlit 应用程序 "Hello World",并学习了如何在本地运行 Streamlit 应用程序。然后,我们开始构建一个更复杂的应用程序,从零开始展示中心极限定理的含义,从一个简单的直方图开始,到接受用户输入,再到在应用程序中格式化不同类型的文本,以增强可读性和美观性。

到现在为止,你应该已经对一些基本的内容感到熟悉,比如数据可视化、在文本编辑器中编辑 Streamlit 应用程序以及本地运行 Streamlit 应用程序。在下一章中,我们将深入探讨数据处理。

在 Discord 上了解更多

要加入本书的 Discord 社区——你可以在这里分享反馈、向作者提问、了解新版本的发布——请扫描以下二维码:

packt.link/sl

第二章:上传、下载和操作数据

到目前为止,本书中我们在 Streamlit 应用中使用的都是模拟数据。这对于掌握 Streamlit 的一些基本概念非常有用,但大多数数据科学工作不是基于模拟数据,而是基于数据科学家已经拥有的真实数据集,或者是用户提供的数据集。

本章将重点介绍 Streamlit 应用中的数据世界,涵盖您需要了解的所有内容,教您如何通过 Streamlit 让数据集“活起来”。我们将讨论数据操作、使用用户导入的数据、流程控制、调试 Streamlit 应用,以及如何通过缓存加速我们的数据应用,示例数据集为 Palmer’s Penguins。

特别地,我们将覆盖以下主题:

  • 设置 - Palmer’s Penguins

  • 调试 Streamlit 应用

  • Streamlit 中的数据操作

  • 使用会话状态保持数据持久性

技术要求

对于本章,我们需要下载 Palmer’s Penguins 数据集,您可以在github.com/tylerjrichards/streamlit_apps/blob/main/penguin_app/penguins.csv找到该数据集。本章的设置以及数据集的解释可以在以下章节中找到。

设置 - Palmer’s Penguins

对于本章,我们将使用一个关于北极企鹅的有趣数据集,该数据集来自 Kristen Gorman 博士(www.uaf.edu/cfos/people/faculty/detail/kristen-gorman.php)及 Palmer 站点,南极 LTER (pallter.marine.rutgers.edu/)的工作。

数据集致谢

来自 Palmer LTER 数据存储库的数据得到了极地项目办公室(Office of Polar Programs)的支持,NSF 资助编号包括 OPP-9011927、OPP-9632763 和 OPP-0217282。

该数据集是著名的 Iris 数据集的常见替代方案,包含 344 只企鹅的个体数据,涵盖了 3 种企鹅物种。数据可以在本书的 GitHub 代码库中找到(github.com/tylerjrichards/Streamlit-for-Data-Science),在penguin_app文件夹中,名为penguins.csv

正如我们之前讨论的,Streamlit 应用程序是在我们的 Python 脚本内部运行的。这会将基础目录设置为 Python 文件所在的位置,意味着我们可以访问放置在应用程序目录中的任何其他文件。

首先,让我们使用以下代码块,在现有的streamlit_apps文件夹中为我们的新应用程序创建一个文件夹:

mkdir penguin_app
cd penguin_app
touch penguins.py 

接下来,下载数据并将生成的 CSV 文件(示例中名为 penguins.csv)放入 penguin_app 文件夹中。现在,我们的文件夹应包含 penguins.py 文件和 penguins.csv 文件。首次运行时,我们将仅通过在 penguins.py 文件中写入以下代码,使用 st.write() 函数打印出 DataFrame 的前五行:

import streamlit as st
import pandas as pd
st.title("Palmer's Penguins")
#import our data
penguins_df = pd.read_csv('penguins.csv')
st.write(penguins_df.head()) 

运行streamlit run penguins.py命令后,前述代码将生成以下的 Streamlit 应用:

图 2.1 – 前五只企鹅

图 2.1:前五只企鹅

现在我们对数据的样貌有了基本了解,我们将进一步探索数据集,并开始向应用程序中添加内容。

探索 Palmer’s Penguins 数据集

在开始处理这个数据集之前,我们应该先制作一些可视化,以便更好地理解数据。如前所述,这些数据中有很多列,无论是喙长、鳍长、企鹅栖息的岛屿,还是企鹅的物种。我已经在 Altair 中为我们做了第一个可视化,Altair 是一个流行的可视化库,我们将在本书中广泛使用它,因为它默认是交互式的,并且通常看起来很美观:

图 2.2:喙长与喙深

从中我们可以看出,阿德利企鹅的喙较短,但一般拥有相对较深的喙。现在,如果我们将体重与鳍长作图,结果会是什么样呢?

图 2.3:喙长与体重

现在我们看到,金图企鹅似乎比其他两种企鹅更重,而且喙长和体重是正相关的。这些发现并不令人感到意外,但达到这些简单结论的过程有点繁琐。我们可以绘制的变量组合还有很多,那么我们能否创建一个数据探索者 Streamlit 应用来为我们完成这些工作呢?

这个小型应用的最终目标是通过让用户定义他们想查看的企鹅物种,以及要在散点图上绘制的 xy 变量,从而减少探索性数据分析的摩擦。我们将从学习如何获取这些输入、如何将数据加载到 Streamlit 中,接着学习如何创建动态可视化开始。

在上一章中,我们了解了一个 Streamlit 输入组件 st.number_input()。这个组件在这里不适用,但 Streamlit 还有一个非常相似的输入组件 st.selectbox(),它允许我们让用户从多个选项中选择一个,函数返回用户选择的内容。我们将使用这个来获取散点图的三个输入:

import streamlit as st
import pandas as pd
import altair as alt
import seaborn as sns
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
selected_species = st.selectbox('What species would you like to visualize?',
     ['Adelie', 'Gentoo', 'Chinstrap'])
selected_x_var = st.selectbox('What do you want the x variable to be?',
     ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
     ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']) 

这段代码创建了三个新变量,这些变量来自于我们 Streamlit 应用中的三个新选择框,用户可以在其中提供输入。以下截图展示了前面代码生成的 Streamlit 应用:

图 2.4:用户对企鹅的输入

现在我们有了 selected_species 变量,可以通过它过滤 DataFrame,并使用选定的 xy 变量快速生成散点图,如下面这段代码所示:

import streamlit as st
import pandas as pd
import altair as alt
import seaborn as sns
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
selected_species = st.selectbox('What species would you like to visualize?',
     ['Adelie', 'Gentoo', 'Chinstrap'])
selected_x_var = st.selectbox('What do you want the x variable to be?',
     ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
     ['bill_depth_mm', 'bill_length_mm', 'flipper_length_mm', 'body_mass_g'])
penguins_df = pd.read_csv('penguins.csv')
penguins_df = penguins_df[penguins_df['species'] == selected_species] 
alt_chart = (
    alt.Chart(penguins_df)
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
    )
)
st.altair_chart(alt_chart) 

这段前面的代码通过加载我们的 DataFrame、按物种过滤数据,然后使用上一章的方法进行绘图,进一步完善了之前的例子。最终结果是,与之前相同的应用程序,但这次附带了一个散点图,截图如下所示:

图 2.5:第一个企鹅散点图

尝试操作这个应用程序,并确保所有输入和输出都能正常工作。注意,当我们将鼠标悬停在任何一个单独的点上时,可以看到该点的底层数据;如果我们更改 Streamlit 输入,整个图表也会随之变化。

我们的图表没有明确显示正在绘制的是哪个物种,所以让我们练习一下动态文本的创建。以下内容使用了 Python 原生的 f-strings,将动态文本添加到我们 Streamlit 应用程序的图表标题中:

import altair as alt
import pandas as pd
import seaborn as sns
import streamlit as st

st.title("Palmer's Penguins")
st.markdown("Use this Streamlit app to make your own scatterplot about penguins!")

selected_species = st.selectbox(
    "What species would you like to visualize?", ["Adelie", "Gentoo", "Chinstrap"]
)

selected_x_var = st.selectbox(
    "What do you want the x variable to be?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)

selected_y_var = st.selectbox(
    "What about the y?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)
penguins_df = pd.read_csv("penguins.csv")
penguins_df = penguins_df[penguins_df["species"] == selected_species]

alt_chart = (
    alt.Chart(penguins_df, title=f"Scatterplot of {selected_species} Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
    )
)
st.altair_chart(alt_chart) 

上述代码将物种添加到我们的散点图中,并生成了以下 Streamlit 应用程序:

图 2.6:动态图表标题

这个看起来很棒,但我们还能做一些改进。现在我们无法放大图表,所以大部分图表都是空白的。我们可以通过使用 Altair 编辑坐标轴,或者使 Altair 图表可交互,以便用户可以在图表上任意放大来改进这一点。以下代码使 Altair 图表可以缩放,并通过 use_container_width 参数将图表扩展以适应整个屏幕:

import altair as alt
import pandas as pd
import seaborn as sns
import streamlit as st
st.title("Palmer's Penguins")
st.markdown("Use this Streamlit app to make your own scatterplot about penguins!")
selected_species = st.selectbox(
    "What species would you like to visualize?", ["Adelie", "Gentoo", "Chinstrap"]
)
selected_x_var = st.selectbox(
    "What do you want the x variable to be?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)
selected_y_var = st.selectbox(
    "What about the y?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)
penguins_df = pd.read_csv("penguins.csv")
penguins_df = penguins_df[penguins_df["species"] == selected_species]
alt_chart = (
    alt.Chart(penguins_df, title=f"Scatterplot of {selected_species} Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

以下截图展示了我们改进后的 Palmer's Penguins 应用程序,它具有适当大小的图表和交互性(我对图表的一些地方进行了放大,以展示新的交互特性)。我还将鼠标放在了某个点上,这时显示了该点的底层数据:

图 2.7:带有交互的截图

本章开始时,我们觉得允许用户选择物种来过滤 DataFrame 是一个不错的主意。但现在,在制作了这个应用程序之后,似乎让用户只修改 xy 输入,而始终以不同颜色绘制物种,可能会更好。以下代码正是做到了这一点,它移除了我们添加的过滤机制,并在 altair 部分的代码中添加了一个颜色参数:

import altair as alt
import pandas as pd
import seaborn as sns
import streamlit as st
st.title("Palmer's Penguins")
st.markdown("Use this Streamlit app to make your own scatterplot about penguins!")
selected_x_var = st.selectbox(
    "What do you want the x variable to be?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)
selected_y_var = st.selectbox(
    "What about the y?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)
penguins_df = pd.read_csv("penguins.csv")
alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

现在,我们的应用程序为每个物种赋予了不同的颜色(在这张截图中,可能看起来是黑白的,但在你自己的应用程序中应该能看到不同的颜色!),并且具有交互性,允许用户输入,所有这些都只用了 26 行代码和 3 条 Streamlit 命令:

图 2.8:彩色企鹅图

这个应用程序的最后一步是允许用户上传自己的数据。如果我们希望研究团队能够随时向应用程序上传他们自己的数据并查看结果,该怎么办?或者如果有三个研究小组,每个小组都有自己独特的、不同列名的数据,且希望使用我们创建的方法,应该如何处理?我们将逐步解决这个问题。首先,我们如何接受应用程序用户的数据?

Streamlit 有一个名为file_uploader()的函数,允许应用程序的用户上传最大为 200 MB 的数据(默认情况下)。它的工作原理与我们之前使用过的其他交互式小部件一样,有一个例外。交互式小部件(如选择框)的默认值通常是列表中的第一个值,但在用户实际上与应用程序交互之前,设置默认上传文件是没有意义的!上传文件的默认值为None

这开始涉及到 Streamlit 开发中的一个非常重要的概念——流控制。流控制可以理解为在构建应用程序时仔细考虑每一个步骤,因为如果我们没有明确指定,Streamlit 会尝试一次性运行整个应用程序。例如,如果我们希望等用户上传文件之后再尝试创建图形或操作数据框,Streamlit 就会立即执行所有步骤。

Streamlit 中的流控制

如我们刚刚所讨论的,这种数据上传默认情况有两种解决方案。我们可以提供一个默认文件,直到用户与应用程序互动,或者我们可以在文件上传之前停止应用程序。我们先从第一个选项开始。以下代码在if语句中使用了st.file_uploader()函数。如果用户上传了文件,应用程序则使用该文件;如果没有上传,应用程序则使用我们之前的默认文件:

import altair as alt
import pandas as pd
import seaborn as sns
import streamlit as st

st.title("Palmer's Penguins")
st.markdown("Use this Streamlit app to make your own scatterplot about penguins!")

penguin_file = st.file_uploader("Select Your Local Penguins CSV (default provided)")
if penguin_file is not None:
    penguins_df = pd.read_csv(penguin_file)
else:
    penguins_df = pd.read_csv("penguins.csv")

selected_x_var = st.selectbox(
    "What do you want the x variable to be?",
    ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"],
)

selected_y_var = st.selectbox(
    "What about the y?",
    ["bill_depth_mm", "bill_length_mm", "flipper_length_mm", "body_mass_g"],
)

alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

当我们在终端运行前面的代码时,我们会看到我们的三个用户输入(x轴、y轴和数据集),以及图形,尽管我们还没有上传文件。以下截图展示了这个应用程序:

图 2.9:文件输入

这种方法的明显优势是,应用程序中总是会有结果显示,但这些结果可能对用户来说并不有用!对于更大型的应用程序来说,这也是一种不理想的解决方案,因为应用程序中存储的任何数据,无论是否使用,都会导致应用程序变慢。稍后,在第七章探索 Streamlit 组件中,我们将讨论所有的部署选项,包括一个内置的部署选项——Streamlit 社区云。

第二种选择是完全停止应用程序,除非用户上传了文件。对于这种选择,我们将使用一个名为 stop() 的新 Streamlit 函数,当调用时它(可以预见)会停止流程。最好使用此方法查找应用程序中的错误,并鼓励用户做出一些更改或描述发生的错误。虽然我们目前不需要这样做,但了解它对未来的应用程序是很有帮助的。以下代码使用 if-else 语句,并在 else 语句中使用 st.stop(),以防止在 st.file_uploader() 未被使用时整个应用程序运行:

import streamlit as st
import pandas as pd
import altair as alt 
import seaborn as sns
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
selected_x_var = st.selectbox('What do you want the x variable to be?',
     ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
     ['bill_depth_mm', 'bill_length_mm', 'flipper_length_mm', 'body_mass_g'])
penguin_file = st.file_uploader('Select Your Local Penguins CSV')
if penguin_file is not None:
     penguins_df = pd.read_csv(penguin_file)
else:
     st.stop()
sns.set_style('darkgrid')
markers = {"Adelie": "X", "Gentoo": "s", "Chinstrap":'o'}
alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

正如我们在下面的截图中所看到的,直到我们上传自己的数据,我们才会看到散点图,并且应用程序会停止。Streamlit 应用程序会在用户上传文件之前等待完全运行,而不是抛出错误:

图 2.10:Streamlit stop()

在我们继续进行数据处理并创建更复杂的 Streamlit 应用程序之前,我们应该先讨论一些调试 Streamlit 应用程序的最佳实践。

调试 Streamlit 应用程序

我们大致有两种选择来进行 Streamlit 开发:

  • 在 Streamlit 中开发并使用 st.write() 作为调试器。

  • 在 Jupyter 中进行探索,然后复制到 Streamlit 中。

在 Streamlit 中开发

在第一种选择中,我们直接在 Streamlit 中编写代码,进行实验并探索应用程序将做什么。我们基本上已经在采用这种方式,它非常适合当我们有较少的探索工作和更多的实现工作时。

优点:

  • 所见即所得 – 无需同时维护同一个应用程序的 IPython 和 Python 版本。

  • 提供更好的体验,帮助学习如何编写生产级代码。

缺点:

  • 较慢的反馈循环(整个应用程序必须运行完才能获得反馈)。

  • 可能不熟悉的开发环境。

在 Jupyter 中进行探索,然后复制到 Streamlit 中

另一种选择是利用广受欢迎的 Jupyter 数据科学工具,在将代码放入必要的脚本并正确格式化之前,编写并测试 Streamlit 应用程序的代码。这对于探索将要在 Streamlit 应用中使用的新功能非常有用,但也有一些严重的缺点。

优点:

  • 快速的反馈循环使得实验非常大的应用程序变得更容易。

  • 用户可能更熟悉 Jupyter。

  • 结果无需运行整个应用程序,因为 Jupyter 可以在单个单元格中运行。

缺点:

  • 如果 Jupyter 按顺序运行,可能会提供误导性结果。

  • 将代码从 Jupyter “复制”过来非常耗时。

  • Python 版本可能在 Jupyter 和 Streamlit 之间不同。

我的建议是在应用将要运行的环境中开发 Streamlit 应用(即 Python 文件)。对于调试,建议大量使用 st.write() 函数,它可以打印出几乎所有 Python 对象(字典、DataFrame、列表、字符串、数字、图表等)。尽量将 Jupyter 等其他开发环境作为最后的手段!接下来是数据处理部分。

Streamlit 中的数据处理

Streamlit 会像脚本一样从上到下运行我们的 Python 文件,因此我们可以像在 Jupyter Notebook 或常规 Python 脚本中一样,使用强大的库如 pandas 进行数据处理。如前所述,我们可以照常进行常规的数据操作。在我们的 Palmer’s Penguins 应用中,如果我们希望用户能够根据性别筛选企鹅呢?以下代码使用 pandas 过滤我们的 DataFrame:

import streamlit as st
import pandas as pd
import altair as alt 
import seaborn as sns
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
penguin_file = st.file_uploader(
    'Select Your Local Penguins CSV (default provided)')
if penguin_file is not None:
    penguins_df = pd.read_csv(penguin_file)
else:
    penguins_df = pd.read_csv('penguins.csv')
selected_x_var = st.selectbox('What do you want the x variable to be?',
                              ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
                              ['bill_depth_mm', 'bill_length_mm', 'flipper_length_mm', 'body_mass_g'])
selected_gender = st.selectbox('What gender do you want to filter for?',
                               ['all penguins', 'male penguins', 'female penguins'])
if selected_gender == 'male penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'male']
elif selected_gender == 'female penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'female']
else:
    pass
alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

这里有几点需要注意。首先,我们添加了另一个 selectbox 小部件,包含男性、女性和所有选项。我们本可以通过文本输入来实现,但为了数据操作,我们希望尽量限制用户的操作。同时,我们确保动态更改了标题,这是为了清晰起见,因为我们希望直接在图表中向用户展示数据已根据他们的输入进行过滤。

缓存简介

随着我们创建越来越多计算密集型的 Streamlit 应用,并开始使用和上传更大的数据集,我们应该开始思考这些应用的运行时间,并在可能的情况下提高效率。使 Streamlit 应用更高效的最简单方法是通过缓存,即将一些结果存储在内存中,以便应用在可能的情况下避免重复执行相同的工作。

应用缓存的一个很好的类比是人类的短期记忆,我们将可能有用的信息保存在身边。当某些信息存在于我们的短期记忆中时,我们不需要费力地去思考就能快速访问该信息。同样,当我们在 Streamlit 中缓存某些信息时,我们是在下注,认为我们会经常使用这些信息。

Streamlit 缓存工作方式的具体实现是将一个函数的结果存储在我们的应用中,如果其他用户(或我们重新运行应用时)使用相同的参数调用该函数,Streamlit 不会再次运行相同的函数,而是从内存中加载该函数的结果。

让我们来验证这个方法是否有效!首先,我们将为 Penguins 应用的数据上传部分创建一个函数,然后使用 time 库人为地让该函数的执行时间比正常情况长,并查看是否能够通过 st.cache_data 使我们的应用更快。Streamlit 有两个缓存函数,一个用于数据(st.cache_data),另一个用于像数据库连接或机器学习模型这样的资源(st.cache_resource)。

不用担心,我们将在第四章《使用 Streamlit 的机器学习与人工智能》中详细了解 st.cache_resource,但现在我们不需要它,因此我们先专注于缓存数据。

正如你在下面的代码中看到的,我们首先创建了一个名为 load_file() 的新函数,它等待 3 秒,然后加载我们需要的文件。通常情况下,我们不会故意拖慢应用程序的速度,但我们想知道缓存是否有效:

import streamlit as st
import pandas as pd
import altair as alt 
import seaborn as sns
import time
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
penguin_file = st.file_uploader(
    'Select Your Local Penguins CSV (default provided)')
def load_file(penguin_file):
    time.sleep(3)
    if penguin_file is not None:
        df = pd.read_csv(penguin_file)
    else:
        df = pd.read_csv('penguins.csv')
    return(df)
penguins_df = load_file(penguin_file)
selected_x_var = st.selectbox('What do you want the x variable to be?',
                              ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
                              ['bill_depth_mm', 'bill_length_mm', 'flipper_length_mm', 'body_mass_g'])
selected_gender = st.selectbox('What gender do you want to filter for?',
                               ['all penguins', 'male penguins', 'female penguins'])
if selected_gender == 'male penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'male']
elif selected_gender == 'female penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'female']
else:
    pass 
alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

现在,让我们运行这个应用程序,然后选择右上角的汉堡菜单图标,按下重新运行按钮(我们也可以直接按R键来重新运行)。

我们注意到每次重新运行应用程序时,都会至少需要 3 秒钟。现在,让我们在 load_file() 函数上方添加缓存装饰器,然后再次运行应用程序:

import streamlit as st
import pandas as pd
import altair as alt
import seaborn as sns
import time
st.title("Palmer's Penguins")
st.markdown('Use this Streamlit app to make your own scatterplot about penguins!')
penguin_file = st.file_uploader(
    'Select Your Local Penguins CSV (default provided)')
@st.cache_data()
def load_file(penguin_file):
    time.sleep(3)
    if penguin_file is not None:
        df = pd.read_csv(penguin_file)
    else:
        df = pd.read_csv('penguins.csv')
    return(df)
penguins_df = load_file(penguin_file)
selected_x_var = st.selectbox('What do you want the x variable to be?',
                              ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'])
selected_y_var = st.selectbox('What about the y?',
                              ['bill_depth_mm', 'bill_length_mm', 'flipper_length_mm', 'body_mass_g'])
selected_gender = st.selectbox('What gender do you want to filter for?',
                               ['all penguins', 'male penguins', 'female penguins'])
if selected_gender == 'male penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'male']
elif selected_gender == 'female penguins':
    penguins_df = penguins_df[penguins_df['sex'] == 'female']
else:
    pass
alt_chart = (
    alt.Chart(penguins_df, title="Scatterplot of Palmer's Penguins")
    .mark_circle()
    .encode(
        x=selected_x_var,
        y=selected_y_var,
        color="species",
    )
    .interactive()
)
st.altair_chart(alt_chart, use_container_width=True) 

当我们运行几次应用程序时,我们可以注意到它变得更快了!当我们重新运行应用程序时,会发生两件事。首先,Streamlit 会检查缓存,确定是否之前已经运行过相同输入的相同函数,并从内存中返回 Palmer’s Penguins 数据;其次,它根本不会运行 load_file() 函数,这意味着我们从未运行过 time.sleep(3) 命令,也没有浪费时间将数据加载到 Streamlit 中。我们将更详细地探讨这个缓存功能,但这种方法为我们带来了大部分的效率提升。我们在这里要讲的最后一个与流相关的主题是 Streamlit 的 st.session_state,它用于在会话之间保存信息!

使用 Session State 保持持久性

对于刚开始的开发者来说,Streamlit 操作模型中最令人沮丧的部分是两个事实的结合:

  1. 默认情况下,信息不会在应用程序的重新运行之间保存。

  2. 在用户输入时,Streamlit 会从上到下重新运行。

这两个事实使得某些类型的应用程序变得难以制作!最好的方式是通过一个示例来展示。假设我们想做一个待办事项应用程序,便于你将项目添加到待办事项列表中。在 Streamlit 中添加用户输入非常简单,因此我们可以在名为 session_state_example.py 的新文件中快速创建一个,代码如下:

import streamlit as st
st.title('My To-Do List Creator')
my_todo_list = ["Buy groceries", "Learn Streamlit", "Learn Python"]
st.write('My current To-Do list is:', my_todo_list)
new_todo = st.text_input("What do you need to do?")
if st.button('Add the new To-Do item'):
    st.write('Adding a new item to the list')
    my_todo_list.append(new_todo)
st.write('My new To-Do list is:', my_todo_list) 

这个应用程序在第一次使用时似乎运行得很好。你可以从文本框中添加项目,见下图:

计算机截图 说明自动生成

图 2.11:待办事项列表

那么,如果我们尝试添加第二个项目,你认为会发生什么呢?让我们现在尝试通过向列表中添加另一个项目来验证一下:

计算机程序截图 说明自动生成

图 2.12:第二次添加

一旦你尝试将多个项目添加到列表中,你会注意到它会重置原始列表,并且忘记了你最初输入的第一个项目!现在我们的待办事项列表中没有之前添加的“吃水果”项目。

输入st.session_state。Session State 是 Streamlit 的一个特性,它是一个全局字典,在用户的会话中保持持久化。这使我们能够绕过本节中提到的两个烦恼,将用户的输入存放到这个全局字典中!我们可以通过首先检查是否将待办事项列表存放在session_state字典中来添加 Session State 功能,如果没有,就设置默认值。每次点击新按钮时,我们都可以更新我们放入session_state字典中的列表:

import streamlit as st
st.title('My To-Do List Creator')
if 'my_todo_list' not in st.session_state:
    st.session_state.my_todo_list = ["Buy groceries", "Learn Streamlit", "Learn Python"]
new_todo = st.text_input("What do you need to do?")
if st.button('Add the new To-Do item'):
    st.write('Adding a new item to the list')
    st.session_state.my_todo_list.append(new_todo)
st.write('My To-Do list is:', st.session_state.my_todo_list) 

现在,我们的应用程序将正常工作,并且会在离开应用或刷新页面之前保持我们的待办事项列表。我们还可以添加多个待办事项!

计算机截图  描述自动生成

图 2.13:多个添加

这有许多应用场景,从保持 Streamlit 输入的状态到在多页应用程序中应用过滤器(别担心,我们会在后面的书中讨论这些)。但每当你想在不同运行之间保持用户信息时,st.session_state都可以帮助你。

总结

本章充满了我们将在本书其余部分中频繁使用的基本构建模块,这些模块也将帮助你开发自己的 Streamlit 应用程序。

在数据方面,我们介绍了如何将自己的 DataFrame 导入 Streamlit,以及如何以数据文件的形式接受用户输入,这使得我们超越了仅能模拟数据的限制。在其他技能方面,我们学会了如何使用缓存加速数据应用程序,如何控制 Streamlit 应用的流程,以及如何使用st.write()调试 Streamlit 应用。这就是本章的内容,接下来我们将进入数据可视化部分!

了解更多信息请访问 Discord

要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问、并了解新版本的发布——请扫描下方的二维码:

packt.link/sl

第三章:数据可视化

可视化是现代数据科学家的一项基本工具。它通常是理解诸如统计模型(例如,通过 AUC 图表)、关键变量的分布(通过直方图)或甚至重要商业指标的核心方式。

在前两章中,我们在示例中使用了两个流行的 Python 绘图库(MatplotlibAltair)。本章将重点介绍如何将这一能力扩展到更多种类的 Python 绘图库,包括 Streamlit 原生的一些绘图函数。

本章结束时,你应该能够自如地使用 Streamlit 的原生绘图函数和可视化函数,将主要 Python 可视化库制作的图表嵌入到你自己的 Streamlit 应用中。

在本章中,我们将介绍以下主题:

  • 旧金山SF)树木 —— 一个新的数据集

  • Streamlit 内置的绘图函数

  • Streamlit 内置的可视化选项

  • 在 Streamlit 中使用 Python 可视化库。在本节中,我们将介绍以下库:

    • Plotly(用于互动可视化)

    • SeabornMatplotlib(用于经典统计可视化)

    • Bokeh(用于在 Web 浏览器中进行互动可视化)

    • Altair(用于声明式互动可视化)

    • PyDeck(用于基于地图的互动可视化)

技术要求

在本章中,我们将使用一个新的数据集,数据集可以在 github.com/tylerjrichards/streamlit_apps/blob/main/trees_app/trees.csv 找到。数据集的进一步说明将在以下部分中介绍。

旧金山树木 —— 一个新的数据集

在本章中,我们将处理各种类型的图表,因此需要一个包含更多信息、特别是日期和位置的新数据集。引入 SF Trees 数据集。旧金山市公共工程部有一个数据集(由 R 社区中的优秀成员清理,这些成员运营着 Tidy Tuesday 这一每周活动,发布有趣的数据可视化),包含了旧金山市所有种植和维护的树木的信息。该数据集巧妙地命名为 EveryTreeSF – 城市森林地图,并每天更新。我已随机选择了 10,000 棵树的完整信息,并将此数据存放在主 GitHub 仓库中的 trees 文件夹下(我并不像旧金山市公共工程部的数据工程师那么聪明,我知道)。GitHub 仓库链接为 github.com/tylerjrichards/streamlit_apps。如果你想下载完整数据集,可以通过此链接: data.sfgov.org/City-Infrastructure/Street-Tree-List/tkzw-k3nq

从我们在本书中一直使用的 streamlit_apps 主文件夹开始,创建一个新文件夹,创建一个新的 Python 文件,然后将数据下载到该文件夹中,这与我们在第二章《上传、下载和操作数据》中所做的相同,不过这次用的是一些新数据!你可以在终端中运行以下代码来设置:

mkdir trees_app
cd trees_app
touch trees.py
curl https://raw.githubusercontent.com/tylerjrichards/streamlit_apps/main/trees_app/trees.csv > trees.csv 

我在这里要指出的是,如果这种方法不起作用,或者你使用的是没有这些命令的操作系统(例如 Windows),你总是可以直接通过访问前面段落中提到的 GitHub 仓库 (github.com/tylerjrichards/streamlit_apps) 来下载 CSV 文件。

现在我们已经完成了设置,接下来的步骤是打开我们最喜欢的编辑器中的 trees.py 文件,开始制作我们的 Streamlit 应用。

我们将在本书接下来的章节一开始时遵循完全相同的步骤,因此未来我们将不会明确地涵盖这些内容。

让我们先为应用设置标题,并使用以下代码打印一些示例行:

import streamlit as st
import pandas as pd
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
st.write(trees_df.head()) 

然后我们可以在终端中运行以下命令,并在浏览器中查看生成的 Streamlit 应用:

streamlit run trees.py 

请注意,这既不是查看数据集前几行的最简单方法,也不是最好的方法,但我们之所以这样做,是因为我们已经知道将使用这些数据构建一个 Streamlit 应用。一般的工作流程通常始于在 Streamlit 之外对数据的探索(比如在 Jupyter Notebook 中,或通过 SQL 查询,或者任何你作为数据科学家或分析师所偏好的工作流程)。话虽如此,让我们继续通过在浏览器中的新 Streamlit 应用查看前面代码的输出,来进一步探索数据集:

图 3.1:树木数据集的前几行

这个数据集包含了关于旧金山树木的大量信息,从树木的宽度(dbh)到经纬度、物种、地址,甚至是它们的种植日期。在我们开始绘图之前,先谈谈我们面前的可视化选项。

Streamlit 可视化使用案例

一些 Streamlit 用户是相对经验丰富的 Python 开发者,他们在自己选择的可视化库中有着经过充分测试的工作流程。对于这些用户,最好的前进路径就是我们目前所采用的方式,那就是在我们选择的库(如 Seaborn、Matplotlib、Bokeh 等)中创建图表,然后使用适当的 Streamlit 函数将其写入应用程序。

其他 Streamlit 用户在 Python 图形绘制方面经验较少,尤其是对于这些用户,Streamlit 提供了一些内建函数。我们将从内建库开始,接着学习如何为我们的 Streamlit 应用导入最流行和最强大的库。

Streamlit 的内建图形功能

有四个内置的图表函数——st.line_chart()st.bar_chart()st.area_chart()st.map()。它们的工作原理相似,都是尝试识别你已经在尝试绘制的变量,然后将它们分别放入折线图、条形图、地图图或区域图。在我们的数据集中,有一个叫做 dbh 的变量,表示树木胸高的直径。首先,我们可以根据 dbh 对 DataFrame 进行分组,然后将其直接推送到折线图、条形图和区域图。以下代码将根据宽度对数据集进行分组,统计每个宽度的独特树木数量,然后绘制每个图表:

import streamlit as st
import pandas as pd
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
df_dbh_grouped = pd.DataFrame(trees_df.groupby(['dbh']).count()['tree_id'])
df_dbh_grouped.columns = ['tree_count']
st.line_chart(df_dbh_grouped)
st.bar_chart(df_dbh_grouped)
st.area_chart(df_dbh_grouped) 

上述代码应按顺序显示我们的三个图表,如下截图所示:

图 3.2:折线、条形图、区域图和树木高度

我们给这个函数提供的唯一参数就是 DataFrame,而它能够正确地猜测出应该放在 x 轴和 y 轴上的项目,并将它们绘制到我们的 Streamlit 图表上。每个图表默认都是交互式的!我们可以放大或缩小,鼠标悬停在点/条形/线条上查看每个数据点,甚至可以直接查看全屏。这些 Streamlit 函数调用了一个流行的图表库叫做 Altair(就是我们之前在 第二章 中使用的那个!)。

现在我们看到了内置函数的基础知识(显然,“内置”一词在这里的定义比较宽泛,因为 Streamlit 是为了成为一个很棒且便捷的 Web 应用程序库,而不是一个专门的可视化库),让我们把这些函数用到更多数据上,看看它们如何处理。首先,我们将在 df_dbh_grouped DataFrame 中生成一个介于 -500 和 500 之间的随机数新列,使用 numpy 库,并使用之前相同的绘图代码。以下代码绘制了两个折线图,一个是添加新列之前的,另一个是添加之后的:

import streamlit as st
import pandas as pd
import numpy as np
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
df_dbh_grouped = pd.DataFrame(trees_df.groupby(['dbh']).count()['tree_id'])
df_dbh_grouped.columns = ['tree_count']
st.line_chart(df_dbh_grouped)
df_dbh_grouped['new_col'] = np.random.randn(len(df_dbh_grouped)) * 500
st.line_chart(df_dbh_grouped) 

这段代码应该生成一个看起来像下面截图的应用,两个单独的折线图垂直排列在一起。

图 3.3:两个连续的折线图

再次提醒,这些函数将索引上的内容放在 x 轴上,并将它们能找到的所有列作为 y 轴上的变量。如果我们遇到一个非常简单的绘图问题(如示例中所示),这些内置函数非常有用。如果我们愿意,还可以显式地告诉 Streamlit 我们想要在 xy 轴上绘制的变量;以下代码段将索引转换为单独的列,然后绘制一个折线图:

import numpy as np
import pandas as pd
import streamlit as st

st.title("SF Trees")
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv("trees.csv")
df_dbh_grouped = pd.DataFrame(
    trees_df.groupby(["dbh"]).count()["tree_id"]
).reset_index()
df_dbh_grouped.columns = ["dbh", "tree_count"]
st.line_chart(df_dbh_grouped, x="dbh", y="tree_count") 

当你运行这段代码时,你会看到之前创建的相同折线图!这些内置函数非常棒,但与专注于可视化的库相比,它们的灵活性较差,而且可能很难调试这些函数背后的行为。

这里的建议是,如果你只需要一个相对基础的可视化,这些函数可能完全适合你。但如果你想要更复杂的东西,应该使用其他图形库(我个人最喜欢的是 Altair)。

还有一个内置的 Streamlit 图形函数我们需要讨论,st.map()。就像前面的函数一样,这个函数封装了另一个 Python 图形库,这次使用的是 PyDeck,而不是 Altair,它通过搜索 DataFrame 中标题为 longitudelonglatitudelat 等的列来识别认为是经度和纬度的列。然后,它将每一行作为一个点绘制在地图上,自动缩放并聚焦地图,最后将其写入我们的 Streamlit 应用。我们需要注意的是,与我们迄今使用的其他可视化形式相比,详细地图的可视化计算量要大得多,因此我们将从 DataFrame 中随机抽取 1,000 行,移除 null 值,并使用以下代码尝试 st.map()

import streamlit as st
import pandas as pd
import numpy as np
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
trees_df = trees_df.dropna(subset=['longitude', 'latitude'])
trees_df = trees_df.sample(n = 1000)
st.map(trees_df) 

这非常完美,开箱即用!我们得到了一个漂亮的交互式旧金山树木地图,如下图所示:

图 3.4 – 默认的旧金山树木地图

图 3.4:默认的旧金山树木地图

和其他函数一样,我们在这里的定制选项并不多,除了一个可选的缩放参数,但这个功能非常适合快速可视化。

正如我们所见,这些内置函数对于快速创建 Streamlit 应用非常有用,但我们在速度和可定制性之间做了权衡。在实际应用中,我很少在制作 Streamlit 应用时使用这些函数,但在进行快速可视化时,我经常使用这些函数。在生产环境中,更强大的库,如 Matplotlib、Seaborn 和 PyDeck,将能够提供我们所需的灵活性和可定制性。本章的剩余部分将提供六种不同流行 Python 可视化库的详细介绍。

Streamlit 内置的可视化选项

在本章的其余部分,我们将介绍 Streamlit 的其他可视化选项,包括 Plotly、Matplotlib、Seaborn、Bokeh、Altair 和 PyDeck。

Plotly

Plotly 是一个交互式可视化库,许多数据科学家用它在 Jupyter notebook 中可视化数据,无论是在本地浏览器中,还是托管在像 Dash(Plotly 的创建者)这样的 Web 平台上。这个库与 Streamlit 在目标上非常相似,主要用于内部或外部的仪表板(因此,才有了 Dash 这个名字)。

Streamlit 允许我们通过 st.plotly_chart() 函数在 Streamlit 应用中调用 Plotly 图表,这使得移植任何 Plotly 或 Dash 仪表板变得非常轻松。我们将通过绘制旧金山树木的身高直方图来测试这一点,这基本上是我们之前制作的图表。以下代码生成我们的 Plotly 直方图:

import streamlit as st
import pandas as pd
import plotly.express as px
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
st.subheader('Plotly Chart')
trees_df = pd.read_csv('trees.csv')
fig = px.histogram(trees_df['dbh'])
st.plotly_chart(fig) 

正如我们所注意到的,所有 Plotly 的原生交互性在 Streamlit 中默认可以使用。特别是,用户可以滚动直方图的条形图,并获取每个条形图的确切信息。Plotly 中还有一些其他有用的内置功能,可以直接移植到 Streamlit 中,例如放大缩小、下载图表为 .png 文件,以及选择一组数据点/条形图/线条。以下截图显示了完整功能:

图 3.5:我们的第一个 Plotly 图表

现在我们已经熟悉了 Plotly,我们可以继续学习其他流行的可视化库,Matplotlib 和 Seaborn。

Matplotlib 和 Seaborn

在本书的前面部分,我们学习了如何在 Streamlit 中使用 Matplotlib 和 Seaborn 可视化库,因此我们在这里将简单地回顾它们。树木数据集中有一个名为date的列,表示树木种植的日期。我们可以使用 datetime 库来计算每棵树的年龄(以天为单位),并分别使用 Seaborn 和 Matplotlib 绘制该直方图。以下代码创建了一个名为 age 的新列,表示树木种植日期和今天日期之间的天数差异,然后使用 Seaborn 和 Matplotlib 绘制该年龄的直方图:

import streamlit as st
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import datetime as dt
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
trees_df['age'] = (pd.to_datetime('today') -
                   pd.to_datetime(trees_df['date'])).dt.days
st.subheader('Seaborn Chart')
fig_sb, ax_sb = plt.subplots()
ax_sb = sns.histplot(trees_df['age'])
plt.xlabel('Age (Days)')
st.pyplot(fig_sb)
st.subheader('Matploblib Chart')
fig_mpl, ax_mpl = plt.subplots()
ax_mpl = plt.hist(trees_df['age'])
plt.xlabel('Age (Days)')
st.pyplot(fig_mpl) 

在前面的代码中,我们为每个图表定义了独立的子图,为每个图表创建了一个 Seaborn 或 Matplotlib 图表,然后使用 st.pyplot() 函数按顺序将每个图表插入到我们的 Streamlit 应用中。前面的代码应该会显示一个类似于以下截图的应用(我说“类似”是因为,根据你运行的时间,树木的年龄会有所不同,因为 pd.to_datetime'today')会返回你当前的日期):

图 3.6 – Seaborn 和 Matplotlib 直方图

图 3.6:Seaborn 和 Matplotlib 直方图

无论是使用 Seaborn 还是 Matplotlib,你都会以相同的方式使用 st.pyplot() 函数。现在我们已经更加熟悉这些库,我们可以学习另一种交互式可视化库 —— Bokeh

Bokeh

Bokeh 是另一个基于 Web 的交互式可视化库,也有基于其构建的仪表板产品。它是 Plotly 的直接竞争对手,坦白说,在使用上非常相似,但在样式上有所不同。不管怎样,Bokeh 是一个非常流行的 Python 可视化包,Python 用户可能非常熟悉使用它。

我们可以像使用 Plotly 一样调用 Bokeh 图表。首先,我们创建 Bokeh 图表,然后使用st.bokeh_chart()函数将应用写入 Streamlit。在 Bokeh 中,我们必须首先实例化一个 Bokeh 图形对象,然后在绘制之前修改该图形的各个方面。这里的重要教训是,如果我们在调用st.bokeh_chart()函数之后修改 Bokeh 图形对象的某个方面,我们将不会改变 Streamlit 应用中显示的图表。例如,当我们运行以下代码时,完全看不到新的x轴标题:

import streamlit as st
import pandas as pd
from bokeh.plotting import figure
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
st.subheader('Bokeh Chart')
trees_df = pd.read_csv('trees.csv')
scatterplot = figure(title = 'Bokeh Scatterplot')
scatterplot.scatter(trees_df['dbh'], trees_df['site_order'])
st.bokeh_chart(scatterplot)
scatterplot.xaxis.axis_label = "dbh" 

相反,我们需要交换最后两行的顺序,这样它们就会出现在我们的应用中。我们还将添加一个y轴作为补充:

import streamlit as st
import pandas as pd
from bokeh.plotting import figure
st.title('SF Trees')
st.write('This app analyzes trees in San Francisco using'
        ' a dataset kindly provided by SF DPW')
st.subheader('Bokeh Chart')
trees_df = pd.read_csv('trees.csv')
scatterplot = figure(title = 'Bokeh Scatterplot')
scatterplot.scatter(trees_df['dbh'], trees_df['site_order'])
scatterplot.yaxis.axis_label = "site_order"
scatterplot.xaxis.axis_label = "dbh"
st.bokeh_chart(scatterplot) 

上述代码将创建一个dbhsite_order的 Bokeh 图表,如下图所示:

图 3.7 – Bokeh 散点图

图 3.7:Bokeh 散点图

现在我们已经有了基本的dbh按地点顺序绘制的 Bokeh 图表,接下来是我们要使用的下一个可视化库——Altair!

Altair

我们在本章中已经通过 Streamlit 函数如st.line_chart()st.map(),以及直接通过st.altair_chart()使用了 Altair,所以为了完整性,我们将简要覆盖这个部分。

由于我们已经使用这个数据集做了不少图表,为什么不探索一个新列——caretaker列呢?这一列数据定义了谁负责这棵树(公共或私人),如果是公共的,负责养护的政府机构是哪个。真是令人兴奋!

以下代码将我们的 DataFrame 按 caretaker 分组,然后在 Altair 中使用该分组的 DataFrame:

import streamlit as st
import pandas as pd
import altair as alt
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
df_caretaker = trees_df.groupby(['caretaker']).count()['tree_id'].reset_index()
df_caretaker.columns = ['caretaker', 'tree_count']
fig = alt.Chart(df_caretaker).mark_bar().encode(x = 'caretaker', y = 'tree_count')
st.altair_chart(fig) 

Altair 也允许我们直接在mark_bar()y值中总结数据,因此我们可以通过使用以下代码简化此过程:

import streamlit as st
import pandas as pd
import altair as alt
st.title('SF Trees') 
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
fig = alt.Chart(trees_df).mark_bar().encode(x = 'caretaker', y = 'count(*):Q')
st.altair_chart(fig) 

上述代码将创建一个 Streamlit 应用,显示 SF 地区按照 caretaker 分类的树木数量,如下图所示:

图 3.8:Altair 条形图

这应该就是传统的可视化库,但 Streamlit 还允许我们使用更复杂的可视化库,比如 PyDeck 进行地理映射。事实上,我们已经通过原生的st.map()函数使用了 PyDeck,并将在接下来的章节中更深入地探讨它。

PyDeck

PyDeck 是一个可视化库,它将可视化作为图层绘制在Mapbox(一家提供卓越免费服务的地图公司)地图上。Streamlit 和 PyDeck 都有一套基本的功能,可以在没有注册 Mapbox 账户的情况下使用,但如果我们获得了Mapbox令牌,它们的免费功能将大大扩展,我们将在下一节中获取该令牌。

配置选项

为了设置你自己的 Mapbox 令牌(这是可选的),首先前往 www.Mapbox.com 并注册一个账户。一旦验证了账户,你可以在 www.Mapbox.com/install/ 找到你的令牌。我们不会将令牌直接传递给 Streamlit,因为否则我们可能会不小心将其推送到公共的 GitHub 仓库中。相反,Streamlit 有一个全局配置文件,名为 config.toml。要查看我们当前的设置,我们可以在终端中运行以下命令:

streamlit config show 

Streamlit 提供了四种方法来更改我们的默认配置设置;我将展示我推荐的选项和另外一个选项,它们应该能够满足大部分使用场景。如果你觉得这些选项不够,Streamlit 文档(docs.streamlit.io/library/advanced-features/configuration)会详细讲解所有四种选项。

第一种选择是通过直接编辑 config.toml 文件来设置全局配置选项。我们可以通过在文本编辑器中打开该文件直接进行编辑。以下命令将在 VSCode 中打开该文件。对于其他文本编辑器(如 Vim 和 Atom),将 code 替换为相应的命令,或者直接从文本编辑器中打开文件:

code ~/.streamlit/config.toml 

如果失败了,可能意味着我们还没有生成该文件。为了创建自己的文件,我们可以运行以下命令:

touch ~/.streamlit/config.toml 

在这个文件中,你可以选择复制并粘贴 'streamlit config show' 的内容,或者从头开始。两种方法都可以!现在,在 VSCode 中打开文件,以便我们可以直接查看和编辑任何配置选项。确保在你的配置文件中有一部分包含你的 Mapbox 令牌,格式如下:

[mapbox] 
token = "123my_large_mapbox_token456" 

当然,你的令牌会与我明显编造的那个不同!这个选项对于像 Mapbox 令牌这样的配置选项非常好,因为我不可能有多个 Mapbox 账户和多个令牌。

然而,一些 Streamlit 应用可能想要使用与默认的 8501 serverPort 不同的端口。例如,改变一个项目特定的设置就不适合修改全局选项,这也是我们需要第二种配置更改选项的原因。

第二种选择是创建并编辑一个项目特定的 config.toml 文件。我们之前的配置设置了默认的配置选项,而这个选项是针对每个 Streamlit 应用的。这里就是我们在 streamlit_apps 文件夹中的各个项目文件夹派上用场的时候!

从大致的角度来看,我们将进行以下操作:

  1. 检查当前工作目录。

  2. 为我们的项目创建一个配置文件。

  3. 在 PyDeck 中使用配置文件。

我们的第一步是确保当前工作目录是trees_app文件夹,可以在终端中运行pwd命令,它将显示当前工作目录,并且应该以trees_app结尾(例如,我的目录是Users/tyler/Documents/streamlit_apps/trees_app)。

现在,我们需要为我们的项目创建一个配置文件。首先,我们将创建一个名为.streamlit的文件夹,然后重复我们之前在 Mac/Linux 上使用的快捷操作:

mkdir .streamlit
touch .streamlit/config.toml 

然后,我们可以像之前一样编辑我们的配置选项,但这仅适用于当我们从目录运行 Streamlit 时的 Streamlit 应用程序。

现在,最后我们可以回到 PyDeck 图表绘制。我们的第一个任务是获取 SF 的基础地图,城市中心坐标为37.77, -122.4。我们可以使用如下代码来完成,首先定义初始状态(我们希望开始查看地图的位置),然后使用该初始状态调用st.pydeck_chart()

import streamlit as st
import pandas as pd
import pydeck as pdk 
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
sf_initial_view = pdk.ViewState(
     latitude=37.77,
     longitude=-122.4
     )
st.pydeck_chart(pdk.Deck(
     initial_view_state=sf_initial_view
     )) 

这段代码将生成一个 SF 的地图,我们可以用它来叠加数据点。这里有几点需要注意。首先,黑色的默认地图可能不容易看清,其次,我们需要花时间缩放到 SF 的区域,以便获得我们需要的视图。我们可以通过使用 Streamlit 文档中建议的默认值来解决这两个问题(docs.streamlit.io/),如下代码所示:

import streamlit as st
import pandas as pd
import pydeck as pdk
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
sf_initial_view = pdk.ViewState(
     latitude=37.77,
     longitude=-122.4,
     zoom=9
     )
st.pydeck_chart(pdk.Deck(
     map_style='mapbox://styles/mapbox/light-v9',
     initial_view_state=sf_initial_view,
     )) 

前面的代码应该生成一张如下截图所示的地图:

图 3.9 – PyDeck 映射:SF 基础地图

图 3.9:PyDeck 映射:SF 基础地图

这正是我们想要的!我们可以看到整个SF 湾区,接下来我们需要添加树木的层。PyDeck 库有交互功能的工具提示,但这些工具对于数据集中的null值处理不太好,因此我们将在接下来的代码中,在映射这些点之前移除null值。我们还会将zoom值增加到11,这样可以更清楚地看到每个点:

import streamlit as st
import pandas as pd
import pydeck as pdk
st.title('SF Trees') 
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
trees_df.dropna(how='any', inplace=True)
sf_initial_view = pdk.ViewState(
     latitude=37.77,
     longitude=-122.4,
     zoom=11
     )
sp_layer = pdk.Layer(
     'ScatterplotLayer',
     data = trees_df,
     get_position = ['longitude', 'latitude'],
     get_radius=30)
st.pydeck_chart(pdk.Deck(
     map_style='mapbox://styles/mapbox/light-v9',
     initial_view_state=sf_initial_view,
     layers = [sp_layer]
     )) 

zoomradius参数的最佳值取决于你的可视化偏好。尝试几种选项,看看哪个效果最好。前面的代码将生成以下地图:

图 3.10 – 映射 SF 树木

图 3.10:SF 树木映射

与之前的地图一样,默认情况下这是交互式的,因此我们可以缩放到 SF 的不同区域,查看树木密度最高的地方。接下来,我们将对这张地图进行修改,添加另一个层,这次是六边形层,根据 SF 中树木的密度进行着色。我们可以使用上面的相同代码,但将散点图层改为六边形层。我们还将包含将六边形沿垂直方向挤压的选项,使图表更具三维效果,虽然这不是必须的,但绝对是一个有趣的可视化风格。

我们最后的修改是改变我们查看地图的视角或角度。正如我们所看到的,默认的视角几乎是直接俯视城市,如果我们尝试在地图上查看垂直的六边形,这样的视角就无法使用。以下代码实现了这些更改:

import streamlit as st
import pandas as pd
import pydeck as pdk
st.title('SF Trees')
st.write(
    """This app analyzes trees in San Francisco using
    a dataset kindly provided by SF DPW"""
)
trees_df = pd.read_csv('trees.csv')
trees_df.dropna(how='any', inplace=True)
sf_initial_view = pdk.ViewState(
     latitude=37.77,
     longitude=-122.4,
     zoom=11,
     pitch=30
     )
hx_layer = pdk.Layer(
     'HexagonLayer',
     data = trees_df,
     get_position = ['longitude', 'latitude'],
     radius=100,
     extruded=True)
st.pydeck_chart(pdk.Deck(
     map_style='mapbox://styles/mapbox/light-v9',
     initial_view_state=sf_initial_view,
     layers = [hx_layer]
     )) 

与前一张地图一样,最佳的半径和视角参数将根据你的可视化效果有所不同。尝试改变这些参数几次,看看你是否能掌握它!前面的代码将生成以下应用:

图 3.11 – 最终的旧金山树木地图

图 3.11:最终的 SF 树木地图

从这张截图中,我们可以看到 PyDeck 在树木密度较高的地方创建了较深的圆圈。从中我们可以观察到很多有趣的细节,比如数据集中似乎缺少了位于城市西侧的著名金门公园的树木,而且金门大桥周围的区域似乎在数据集中也缺少树木。

总结

在这一章之后,希望你能对如何在 Streamlit 中利用几个令人惊叹的开源 Python 可视化库有一个扎实的理解。

让我们回顾一下。首先,我们学习了如何使用默认的可视化选项,例如 st.line_chart()st.map(),然后我们深入了解了交互式库,如 Plotly,地图库,如 PyDeck,以及其中的一切。

在下一章中,我们将继续介绍如何使用机器学习和 AI 与 Streamlit 配合使用。

在 Discord 上了解更多

要加入本书的 Discord 社区——你可以在这里分享反馈、向作者提问并了解新版本——请扫描下面的二维码:

packt.link/sl

第四章:使用 Streamlit 进行机器学习与人工智能

数据科学家常见的一个情况是,在模型创建过程的最后,无法确定如何说服非数据科学家相信他们的模型是有价值的。他们可能有模型的性能指标或一些静态可视化,但没有一个简单的方式让其他人与他们的模型进行互动。

在 Streamlit 之前,有几个其他选项,其中最流行的是在 Flask 或 Django 中创建一个完整的应用程序,或者甚至将模型转化为应用程序编程接口API),并引导开发人员使用它。这些都是不错的选择,但往往需要耗费时间,并且对于像应用原型开发这样的宝贵用例来说并不理想。

团队的激励机制在这里有些不一致。数据科学家希望为他们的团队创建最好的模型,但如果他们需要花一天或两天的时间(或者,如果有经验的话,几个小时)将模型转化为 Flask 或 Django 应用,直到他们认为模型过程几乎完成时才去做这个,似乎没有太大意义。理想情况下,数据科学家应该早期并且经常地与利益相关者进行沟通,这样他们才能构建人们真正需要的东西!

Streamlit 的好处在于,它帮助我们将这个繁琐的过程转化为一个无缝的应用程序创建体验。在本章中,我们将介绍如何在 Streamlit 中创建机器学习ML)原型,如何为你的机器学习应用添加用户互动,以及如何理解机器学习结果。我们将使用包括 PyTorch、Hugging Face、OpenAI 和 scikit-learn 在内的最流行的机器学习库来完成这一切。

本章将涵盖以下主题:

  • 标准机器学习工作流程

  • 预测企鹅物种

  • 利用预训练的机器学习模型

  • 在 Streamlit 应用中训练模型

  • 理解机器学习结果

  • 集成外部机器学习库——Hugging Face 示例

  • 集成外部人工智能库——OpenAI 示例

技术要求

本章我们将需要一个 OpenAI 账户。要创建账户,请前往(platform.openai.com/)并按照页面上的说明操作。

标准机器学习工作流程

创建一个使用机器学习(ML)的应用程序的第一步是创建机器学习模型本身。创建自己的机器学习模型有许多流行的工作流程。你可能已经有自己的方法了!这个过程有两个部分需要考虑:

  • 机器学习模型的生成

  • 在生产环境中使用机器学习模型

如果计划是一次训练一个模型,然后在 Streamlit 应用中使用该模型,最好的方法是先在 Streamlit 外部创建这个模型(例如,在 Jupyter 笔记本或标准 Python 文件中),然后在应用中使用该模型。

如果计划是使用用户输入来训练我们应用中的模型,那么我们就不能再在 Streamlit 外部创建模型,而需要在 Streamlit 应用内进行模型训练。

我们将首先在 Streamlit 之外构建我们的机器学习模型,然后再将模型训练过程移入 Streamlit 应用中。

预测企鹅物种

本章中我们主要使用的数据集是我们在第一章《Streamlit 入门》中使用的 Palmer Penguins 数据集。按照惯例,我们将创建一个新文件夹来存放我们的新 Streamlit 应用和相关代码。

以下代码会在我们的streamlit_apps文件夹内创建一个新文件夹,并将数据从我们的penguin_app文件夹复制过来。如果你还没有下载 Palmer Penguins 数据集,请按照第二章《上传、下载与数据处理中的The setup: Palmer Penguins*部分的说明操作:

mkdir penguin_ml
cp penguin_app/penguins.csv penguin_ml
cd penguin_ml
touch penguins_ml.py
touch penguins_streamlit.py 

正如你在前面的代码中看到的,这里有两个 Python 文件,一个用于创建机器学习模型(penguins_ml.py),另一个用于创建 Streamlit 应用(penguins_streamlit.py)。我们将从penguins_ml.py文件开始,一旦我们有了一个满意的模型,我们将继续处理penguins_streamlit.py文件。

你也可以选择在 Jupyter Notebook 中创建模型,尽管它的可复现性较差(因为单元格可以乱序运行),但它仍然非常流行。

让我们重新熟悉一下penguins.csv数据集。以下代码将读取数据集并打印出前五行:

import pandas as pd
penguin_df = pd.read_csv('penguins.csv')
print(penguin_df.head()) 

当我们在终端中运行我们的 Python 文件penguins_ml.py时,前面的代码输出将类似于以下截图:

图 4.1 – 前五只企鹅

图 4.1:前五只企鹅

对于这个应用程序,我们将尝试创建一个帮助野外研究人员识别企鹅物种的应用。它将根据企鹅的喙、鳍和体重的测量数据,以及企鹅的性别和位置,预测企鹅的物种。

下一节并不是要尝试制作最好的机器学习模型,而只是为了创建一个快速原型供我们的 Streamlit 应用迭代。在这种情况下,我们将丢弃一些带有空值的行,并且不在特征中使用year变量,因为它与我们的用例不匹配。我们需要定义我们的特征和输出变量,对特征进行独热编码(或称Pandas中的虚拟变量处理),并对输出变量进行因子化(将其从字符串转换为数字)。以下代码将把我们的数据集转换成适合分类算法的状态:

import pandas as pd
penguin_df = pd.read_csv('penguins.csv')
penguin_df.dropna(inplace=True)
output = penguin_df['species']
features = penguin_df[['island', 'bill_length_mm', 'bill_depth_mm',
      'flipper_length_mm', 'body_mass_g', 'sex']]
features = pd.get_dummies(features)
print('Here are our output variables')
print(output.head())
print('Here are our feature variables')
print(features.head() 

现在,当我们再次运行 Python 文件penguins_ml.py时,我们会看到输出和特征变量已被分开,如下图所示:

图 4.2 – 输出变量

图 4.2:输出变量

现在,我们想使用数据的一个子集(在本例中为 80%)来创建一个分类模型,并获取该模型的准确度。以下代码通过使用随机森林模型来执行这些步骤,但如果你愿意,也可以使用其他分类算法。再次强调,这里主要是为了快速构建一个原型,向企鹅研究人员展示以便获取反馈!

import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
penguin_df = pd.read_csv('penguins.csv')
penguin_df.dropna(inplace=True)
output = penguin_df['species']
features = penguin_df[['island', 'bill_length_mm', 'bill_depth_mm',
                       'flipper_length_mm', 'body_mass_g', 'sex']]
features = pd.get_dummies(features)
output, uniques = pd.factorize(output)
x_train, x_test, y_train, y_test = train_test_split(
    features, output, test_size=.8)
rfc = RandomForestClassifier(random_state=15)
rfc.fit(x_train.values, y_train)
y_pred = rfc.predict(x_test.values)
score = accuracy_score(y_pred, y_test)
print('Our accuracy score for this model is {}'.format(score)) 

现在我们已经有了一个相当不错的模型来预测企鹅的物种!在模型生成过程中的最后一步是保存我们最需要的两个部分——模型本身和 uniques 变量,它将因子化的输出变量映射到我们识别的物种名称。在之前的代码基础上,我们将添加几行代码,用于将这些对象保存为 pickle 文件(这是一种将 Python 对象转化为可直接保存并可以轻松从另一个 Python 文件(如我们的 Streamlit 应用)中导入的格式)。更具体地说,open() 函数创建了两个 pickle 文件,pickle.dump() 函数将我们的 Python 文件写入这些文件,而 close() 函数则关闭文件。open() 函数中的 wb 表示写入字节,它告诉 Python 我们要写入而不是读取该文件:

import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import pickle
penguin_df = pd.read_csv('penguins.csv')
penguin_df.dropna(inplace=True)
output = penguin_df['species']
features = penguin_df[['island', 'bill_length_mm', 'bill_depth_mm',
                       'flipper_length_mm', 'body_mass_g', 'sex']]
features = pd.get_dummies(features)
output, uniques = pd.factorize(output)
x_train, x_test, y_train, y_test = train_test_split(
    features, output, test_size=.8)
rfc = RandomForestClassifier(random_state=15)
rfc.fit(x_train.values, y_train)
y_pred = rfc.predict(x_test.values)
score = accuracy_score(y_pred, y_test)
print('Our accuracy score for this model is {}'.format(score))
rf_pickle = open('random_forest_penguin.pickle', 'wb')
pickle.dump(rfc, rf_pickle)
rf_pickle.close()
output_pickle = open('output_penguin.pickle', 'wb')
pickle.dump(uniques, output_pickle)
output_pickle.close() 

现在我们在 penguin_ml 文件夹中有了两个文件:一个名为 random_forest_penguin.pickle 的文件,其中包含我们的模型,另一个是 output_penguin_.pickle,它包含企鹅物种与模型输出之间的映射。这就是 penguins_ml.py 函数的内容!接下来我们可以开始创建我们的 Streamlit 应用,使用我们刚刚创建的机器学习模型。

在 Streamlit 中使用预训练的机器学习模型

现在我们有了模型,我们想将它(以及我们的映射函数)加载到 Streamlit 中。在我们之前创建的文件 penguins_streamlit.py 中,我们将再次使用 pickle 库通过以下代码加载我们的文件。我们使用与之前相同的函数,但这次我们使用 rb 参数,而不是 wbrb 表示读取字节。为了确保这些是我们之前使用的相同的 Python 对象,我们将使用我们非常熟悉的 st.write() 函数来进行检查:

import streamlit as st
import pickle
rf_pickle = open('random_forest_penguin.pickle', 'rb')
map_pickle = open('output_penguin.pickle', 'rb')
rfc = pickle.load(rf_pickle)
unique_penguin_mapping = pickle.load(map_pickle)
st.write(rfc)
st.write(unique_penguin_mapping) 

与我们之前的 Streamlit 应用一样,我们在终端运行以下代码来启动我们的应用:

streamlit run penguins_streamlit.py 

我们现在有了随机森林分类器,并且完成了企鹅的映射!接下来的步骤是添加 Streamlit 函数以获取用户输入。在我们的应用程序中,我们使用了岛屿、嘴长、嘴深、鳍肢长度、体重和性别来预测企鹅的物种,所以我们需要从用户那里获取这些信息。对于岛屿和性别,我们知道这些选项已经在我们的数据集中,并且希望避免解析用户输入的文本,因此我们将使用 st.selectbox()。对于其他数据,我们只需要确保用户输入的是一个正数,所以我们将使用 st.number_input() 函数并设置最小值为 0。以下代码会接收这些输入并在我们的 Streamlit 应用程序中显示它们:

import pickle
import streamlit as st
rf_pickle = open("random_forest_penguin.pickle", "rb")
map_pickle = open("output_penguin.pickle", "rb")
rfc = pickle.load(rf_pickle)
unique_penguin_mapping = pickle.load(map_pickle)
rf_pickle.close()
map_pickle.close()
island = st.selectbox("Penguin Island", options=["Biscoe", "Dream", "Torgerson"])
sex = st.selectbox("Sex", options=["Female", "Male"])
bill_length = st.number_input("Bill Length (mm)", min_value=0)
bill_depth = st.number_input("Bill Depth (mm)", min_value=0)
flipper_length = st.number_input("Flipper Length (mm)", min_value=0)
body_mass = st.number_input("Body Mass (g)", min_value=0)
user_inputs = [island, sex, bill_length, bill_depth, flipper_length, body_mass]
st.write(f"""the user inputs are {user_inputs}""".format()) 

上述代码应当生成如下的应用程序。尝试一下,看看通过更改值,输出是否也发生变化。

Streamlit 设计时默认情况下,每次更改值时,整个应用程序都会重新运行。以下截图显示了应用程序的实时效果,并展示了我更改过的一些值。我们可以通过右侧的 +- 按钮来更改数值,或者直接手动输入值:

图 4.3:模型输入

现在我们已经准备好了所有输入和模型,下一步是将数据格式化为与我们预处理数据相同的格式。例如,我们的模型没有一个名为 sex 的变量,而是有两个名为 sex_femalesex_male 的变量。一旦数据的格式正确,我们就可以调用 predict 函数,并将预测结果映射到我们的原始物种列表中,以查看我们的模型如何工作。以下代码正是完成这一任务,同时还向应用程序添加了一些基本标题和说明,使其更易于使用。这个应用程序比较长,因此我会将其分成多个部分,以便阅读。我们首先为应用程序添加说明和标题:

import streamlit as st
import pickle
st.title('Penguin Classifier')
st.write("This app uses 6 inputs to predict the species of penguin using"
         "a model built on the Palmer Penguins dataset. Use the form below"
         " to get started!")
rf_pickle = open('random_forest_penguin.pickle', 'rb')
map_pickle = open('output_penguin.pickle', 'rb')
rfc = pickle.load(rf_pickle)
unique_penguin_mapping = pickle.load(map_pickle)
rf_pickle.close()
map_pickle.close() 

我们现在已经有了一个带有标题和用户说明的应用程序。下一步是像之前一样获取用户输入。我们还需要将 sexisland 变量转换为正确的格式,如之前所述:

island = st.selectbox('Penguin Island', options=[
                      'Biscoe', 'Dream', 'Torgerson'])
sex = st.selectbox('Sex', options=['Female', 'Male'])
bill_length = st.number_input('Bill Length (mm)', min_value=0)
bill_depth = st.number_input('Bill Depth (mm)', min_value=0)
flipper_length = st.number_input('Flipper Length (mm)', min_value=0)
body_mass = st.number_input('Body Mass (g)', min_value=0)
island_biscoe, island_dream, island_torgerson = 0, 0, 0
if island == 'Biscoe':
    island_biscoe = 1
elif island == 'Dream':
    island_dream = 1
elif island == 'Torgerson':
    island_torgerson = 1
sex_female, sex_male = 0, 0
if sex == 'Female':
    sex_female = 1
elif sex == 'Male':
    sex_male = 1 

我们的所有数据格式正确!最后一步是使用 predict() 函数处理我们的模型和新数据,这一部分代码完成了这一任务:

new_prediction = rfc.predict([[bill_length, bill_depth, flipper_length,
                               body_mass, island_biscoe, island_dream,
                               island_torgerson, sex_female, sex_male]])
prediction_species = unique_penguin_mapping[new_prediction][0]
st.write(f"We predict your penguin is of the {prediction_species} species") 

现在我们的应用程序应该像下面的截图一样。

我已为输入添加了一些示例值,但你应该尝试更改数据,看看能否使物种发生变化!

图 4.4:用于预测的完整 Streamlit 应用

我们现在拥有一个完整的 Streamlit 应用程序,利用我们预训练的机器学习模型,获取用户输入并输出预测结果。接下来,我们将讨论如何直接在 Streamlit 应用程序中训练模型!

在 Streamlit 应用程序中训练模型

经常情况下,我们可能希望用户输入的数据能够影响模型的训练。我们可能希望接受用户的数据,或者询问用户他们希望使用哪些特征,甚至允许用户选择他们希望使用的机器学习算法类型。所有这些选项在 Streamlit 中都是可行的,在本节中,我们将介绍如何使用用户输入来影响训练过程的基础知识。如上节所讨论的那样,如果模型只需要训练一次,最好在 Streamlit 外部训练模型,并将模型导入到 Streamlit 中。但如果在我们的示例中,企鹅研究人员将数据保存在本地,或者不知道如何重新训练模型,但已有正确格式的数据呢?在这种情况下,我们可以添加st.file_uploader()选项,并为这些用户提供一种方法,让他们输入自己的数据,并部署一个定制的模型,而无需编写任何代码。以下代码将添加一个用户选项来接受数据,并使用我们最初在penguins_ml.py中的预处理/训练代码为该用户创建一个独特的模型。需要注意的是,这只有在用户的数据格式与我们使用的完全相同时才会有效,这可能性较小。这里的另一个潜在补充是,向用户展示数据需要具备的格式,以便该应用能够正确地训练模型,按预期输出!

import streamlit as st
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import pickle
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
st.title('Penguin Classifier')
st.write(
    """This app uses 6 inputs to predict the species of penguin using
    a model built on the Palmer Penguins dataset. Use the form below
    to get started!"""
)
penguin_file = st.file_uploader('Upload your own penguin data') 

这一部分首先导入了我们需要的库,添加了标题——如我们之前所用——并添加了file_uploader()函数。然而,当用户还没有上传文件时会发生什么呢?如果没有企鹅文件,我们可以将默认设置为加载我们的随机森林模型,如下一个代码块所示:

if penguin_file is None:
    rf_pickle = open('random_forest_penguin.pickle', 'rb')
    map_pickle = open('output_penguin.pickle', 'rb')
    rfc = pickle.load(rf_pickle)
    unique_penguin_mapping = pickle.load(map_pickle)
    rf_pickle.close()
    map_pickle.close() 

接下来我们需要解决的问题是如何接收用户的数据,清理数据,并基于这些数据训练一个模型。幸运的是,我们可以重用已经创建的模型训练代码,并将其放入下一个代码块中的else语句中:

else:
    penguin_df = pd.read_csv(penguin_file)
    penguin_df = penguin_df.dropna()
    output = penguin_df['species']
    features = penguin_df[['island', 'bill_length_mm', 'bill_depth_mm',
                           'flipper_length_mm', 'body_mass_g', 'sex']]
    features = pd.get_dummies(features)
    output, unique_penguin_mapping = pd.factorize(output)
    x_train, x_test, y_train, y_test = train_test_split(
        features, output, test_size=.8)
    rfc = RandomForestClassifier(random_state=15)
    rfc.fit(x_train.values, y_train)
    y_pred = rfc.predict(x_test.values)
    score = round(accuracy_score(y_pred, y_test), 2)
    st.write(
        f"""We trained a Random Forest model on these
        data, it has a score of {score}! Use the
        inputs below to try out the model"""
    ) 

我们现在已经在应用中创建了模型,并且需要从用户处获取输入来进行预测。然而,这次我们可以在之前的基础上进行改进。到目前为止,每当用户在应用中更改输入时,整个 Streamlit 应用都会重新运行。我们可以使用st.form()st.submit_form_button()函数将其余的用户输入包裹起来,并允许用户一次性更改所有输入并提交整个表单,而不是多次提交:

with st.form('user_inputs'):
island = st.selectbox('Penguin Island', options=		['Biscoe', 'Dream', 'Torgerson'])
sex = st.selectbox('Sex', options=['Female', 'Male'])
bill_length = st.number_input('Bill Length (mm)', min_value=0)
bill_depth = st.number_input('Bill Depth (mm)', min_value=0)
flipper_length = st.number_input('Flipper Length (mm)', min_value=0)
body_mass = st.number_input('Body Mass (g)', min_value=0)
st.form_submit_button()
island_biscoe, island_dream, island_torgerson = 0, 0, 0
if island == 'Biscoe':
    island_biscoe = 1
elif island == 'Dream':
    island_dream = 1
elif island == 'Torgerson':
    island_torgerson = 1
sex_female, sex_male = 0, 0
if sex == 'Female':
    sex_female = 1
elif sex == 'Male':
    sex_male = 1 

现在我们有了新的表单输入,需要创建预测并将预测结果写入用户界面,如下一个代码块所示:

new_prediction = rfc.predict(
    [
        [
            bill_length,
            bill_depth,
            flipper_length,
            body_mass,
            island_biscoe,
            island_dream,
            island_torgerson,
            sex_female,
            sex_male,
        ]
    ]
)
prediction_species = unique_penguin_mapping[new_prediction][0]
st.write(f"We predict your penguin is of the {prediction_species} species") 

就这样!我们现在拥有一个 Streamlit 应用,允许用户输入自己的数据,基于这些数据训练模型,并输出结果,如下图所示:

图 4.5:企鹅分类器应用

这里可以做一些潜在的改进,比如使用缓存函数(在第二章上传、下载和处理数据中有介绍)作为一个例子。像这种让用户提供自己数据的应用,通常要比直接在应用内构建的应用更难开发,尤其是没有统一的数据格式。根据写作时的情况,更常见的是看到展示令人印象深刻的 ML 模型和用例的 Streamlit 应用,而不是直接在应用内构建这些模型的应用(尤其是当模型训练的计算开销较大时)。如前所述,Streamlit 开发者通常会在要求用户输入数据集之前提供所需数据格式的参考。然而,允许用户提供自己数据的选项仍然可用且实用,特别是为了快速迭代模型构建。

理解 ML 结果

到目前为止,我们的应用可能是有用的,但仅仅显示一个结果通常不足以满足数据应用的需求。我们应该展示一些对结果的解释。为了做到这一点,我们可以在我们已经创建的应用输出中包含一个部分,帮助用户更好地理解模型。

首先,随机森林模型已经内建了一个特征重要性方法,这个方法是从构成随机森林的每个单独决策树中得出的。我们可以编辑我们的penguins_ml.py文件来绘制这个特征重要性图,并从 Streamlit 应用中调用该图像。我们也可以直接在 Streamlit 应用中绘制这个图,但是一次性在penguins_ml.py中生成这个图比每次 Streamlit 应用重新加载时(即每次用户更改输入时)都生成图像更高效。以下代码编辑了我们的penguins_ml.py文件,添加了特征重要性图,并将其保存到我们的文件夹中。我们还调用了tight_layout()功能,帮助我们更好地格式化图表,确保不会有标签被截断。这段代码较长,文件的上半部分没有更改,因此只省略了库导入和数据清理部分。还有一点需要说明的是,我们将尝试使用其他图表库,如 Seaborn 和 Matplotlib,只是为了在使用的图形库上增加一些多样性。

x_train, x_test, y_train, y_test = train_test_split(
    features, output, test_size=.8)
rfc = RandomForestClassifier(random_state=15)
rfc.fit(x_train, y_train)
y_pred = rfc.predict(x_test)
score = accuracy_score(y_pred, y_test)
print('Our accuracy score for this model is {}'.format(score))
rf_pickle = open('random_forest_penguin.pickle', 'wb')
pickle.dump(rfc, rf_pickle)
rf_pickle.close()
output_pickle = open('output_penguin.pickle', 'wb')
pickle.dump(uniques, output_pickle)
output_pickle.close()
fig, ax = plt.subplots()
ax = sns.barplot(x=rfc.feature_importances_, y=features.columns)
plt.title('Which features are the most important for species prediction?')
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.tight_layout()
fig.savefig('feature_importance.png') 

现在,当我们重新运行penguins_ml.py时,我们应该能看到一个名为feature_importance.png的文件,我们可以从我们的 Streamlit 应用中调用这个文件。现在就让我们来做吧!我们可以使用st.image()函数从.png文件中加载图像,并将其打印到我们的企鹅应用中。以下代码将把我们的图像添加到 Streamlit 应用中,并改进我们对预测结果的解释。由于代码块较长,我们只展示从开始使用用户数据进行预测的代码部分:

new_prediction = rfc.predict([[bill_length, bill_depth, flipper_length,
                               body_mass, island_biscoe, island_dream,
                               island_torgerson, sex_female, sex_male]])
prediction_species = unique_penguin_mapping[new_prediction][0]
st.subheader("Predicting Your Penguin's Species:")
st.write(f"We predict your penguin is of the {prediction_species} species")
st.write(
    """We used a machine learning (Random Forest)
    model to predict the species, the features
    used in this prediction are ranked by 
    relative importance below."""
)
st.image('feature_importance.png') 

现在,您的 Streamlit 应用底部应该看起来像下面的截图(注意您的字符串可能会根据输入有所不同):

图 4.6 – 特征重要性截图

图 4.6:特征重要性截图

如我们所见,鸟喙长度、鸟喙深度和鳍长是根据我们的随机森林模型得出的最重要变量。解释我们模型工作原理的另一个最终选项是绘制每个变量按物种分布的图表,并绘制一些表示用户输入的垂直线。理想情况下,用户可以开始从整体上理解底层数据,因此也能理解模型所做的预测。为了实现这一点,我们需要将数据实际导入到 Streamlit 应用中,这是我们之前没有做的。以下代码导入了我们用来构建模型的企鹅数据,并绘制了三个直方图(鸟喙长度鸟喙深度鳍长),同时将用户输入作为一条垂直线展示,从模型解释部分开始:

st.subheader("Predicting Your Penguin's Species:")
st.write(f"We predict your penguin is of the {prediction_species} species")
st.write(
    """We used a machine learning (Random Forest)
    model to predict the species, the features
    used in this prediction are ranked by 
    relative importance below."""
)
st.image('feature_importance.png')
st.write(
    """Below are the histograms for each 
    continuous variable separated by penguin 
    species. The vertical line represents 
    your the inputted value."""
) 

现在我们已经设置好应用程序来显示直方图,我们可以使用 Seaborn 可视化库中的displot()函数来创建我们最重要变量的三个直方图:

fig, ax = plt.subplots()
ax = sns.displot(x=penguin_df['bill_length_mm'],
                 hue=penguin_df['species'])
plt.axvline(bill_length)
plt.title('Bill Length by Species')
st.pyplot(ax)
fig, ax = plt.subplots()
ax = sns.displot(x=penguin_df['bill_depth_mm'],
                 hue=penguin_df['species'])
plt.axvline(bill_depth)
plt.title('Bill Depth by Species')
st.pyplot(ax)
fig, ax = plt.subplots()
ax = sns.displot(x=penguin_df['flipper_length_mm'],
                 hue=penguin_df['species'])
plt.axvline(flipper_length)
plt.title('Flipper Length by Species')
st.pyplot(ax) 

上述代码应当生成如下图所示的应用程序,这是我们应用程序的最终形式。为了方便查看,我们只展示第一个直方图:

图 4.7:按物种分类的鸟喙长度

如同往常一样,完整的最终代码可以在github.com/tylerjrichards/Streamlit-for-Data-Science找到。这部分到此为止。我们现在已经创建了一个完全构建的 Streamlit 应用程序,它可以接受一个预先构建的模型和用户输入,并输出预测结果以及对输出的解释。接下来,让我们探讨如何将你最喜爱的其他机器学习库集成到 Streamlit 中!

集成外部机器学习库——以 Hugging Face 为例

在过去几年里,由初创公司和机构创建的机器学习模型数量大幅增加。在我看来,有一个公司因其优先考虑开源和分享模型及方法而脱颖而出,那就是 Hugging Face。Hugging Face 使得使用一些领域内最优秀的研究人员创建的机器学习模型变得异常简单,你可以将这些模型应用到自己的用例中,在这一部分中,我们将快速展示如何将 Hugging Face 集成到 Streamlit 中。

在本书的初始设置中,我们已经下载了所需的两个库:PyTorch(最流行的深度学习 Python 框架)和 transformers(Hugging Face 的库,简化了使用其预训练模型的过程)。因此,对于我们的应用,让我们尝试进行自然语言处理中的一项基础任务:获取一段文本的情感!Hugging Face 通过其 pipeline 函数使这变得异常简单,该函数让我们按名称请求模型。接下来的代码片段会从用户那里获取文本输入,然后从 Hugging Face 检索情感分析模型:

import streamlit as st
from transformers import pipeline

st.title("Hugging Face Demo")
text = st.text_input("Enter text to analyze")
model = pipeline("sentiment-analysis")
if text:
    result = model(text)
    st.write("Sentiment:", result[0]["label"])
    st.write("Confidence:", result[0]["score"]) 

当我们运行这个时,应该会看到如下结果。

图 4.8:Hugging Face 演示

我在应用中放入了一个随机句子,但你可以随意尝试!试着给模型一些信心较低的文本(我试过“streamlit is a pizza pie”,并成功地让模型困惑)。想了解更多这里使用的模型,Hugging Face 提供了丰富的文档(huggingface.co/distilbert-base-uncased-finetuned-sst-2-english)。

当你玩这个应用时,你会发现应用经常加载较慢。这是因为每次运行应用时,transformers 库会从 Hugging Face 获取模型,并在应用中使用它。我们已经学习了如何缓存数据,但 Streamlit 有一个类似的缓存功能,叫做st.cache_resource,它可以让我们缓存像 ML 模型和数据库连接这样的对象。我们可以在这里使用它来加速应用:

import streamlit as st
from transformers import pipeline
st.title("Hugging Face Demo")
text = st.text_input("Enter text to analyze")
@st.cache_resource()
def get_model():
    return pipeline("sentiment-analysis")
model = get_model()
if text:
    result = model(text)
    st.write("Sentiment:", result[0]["label"])
    st.write("Confidence:", result[0]["score"]) 

现在,我们的应用在多次使用时应该会运行得更快。这个应用并不完美,但它向我们展示了将一些顶尖库集成到 Streamlit 中的简便性。在本书的后续章节中,我们将讨论如何将 Streamlit 应用直接免费部署在 Hugging Face 上,但我鼓励你浏览 Hugging Face 网站(huggingface.co/),看看他们提供的所有资源。

集成外部 AI 库——OpenAI 示例

2023 年无疑是生成式 AI 的年份,ChatGPT 以其强大的影响力席卷了全球和开发者社区。像 ChatGPT 这样的服务背后生成模型的可用性也急剧增加,各大科技公司纷纷推出了自己的版本(例如 Meta 的ai.meta.com/llama/和 Google 的bard.google.com/)。其中最受欢迎的生成模型系列是 OpenAI 的GPT生成预训练变换器)。本节将向你展示如何使用 OpenAI API 将生成式 AI 添加到你的 Streamlit 应用中!

使用 OpenAI 进行身份验证

我们的第一步是创建一个 OpenAI 账户并获取 API 密钥。为此,请访问platform.openai.com并创建一个账户。创建账户后,进入API 密钥部分(platform.openai.com/account/api-keys),点击创建新的秘密密钥按钮。创建密钥后,务必将其保存在安全的地方,因为 OpenAI 不会再向你显示密钥!我将它保存在我的密码管理器中,以确保不会丢失(1password.com/),但你可以将它保存在任何你想要的地方。

OpenAI API 费用

OpenAI API 不是免费的,但我们将使用的这一款(GPT-3.5 turbo)目前的费用为每千个 tokens(大约 750 个单词)输入$0.0015,输出每千个 tokens$0.002(有关最新信息,请参见openai.com/pricing)。你也可以在platform.openai.com/account/billing/limits设置一个硬性限制,限制你在该 API 上的最大消费。如果设置了硬性限制,OpenAI 将不允许你超过此限制。我强烈建议设置限制。对这个示例部分设置 1 美元的限制;我们应该完全在这个范围内!一旦你开始创建并公开分享你自己的生成 AI 应用,这个功能将变得更加有用(通常,开发者要么要求用户输入自己的 API 密钥,要么通过像github.com/tylerjrichards/st-paywall这样的库收取访问 Streamlit 应用的费用,以避免支付过多)。

Streamlit 和 OpenAI

在这个示例中,我们将重新创建我们 Hugging Face 示例中的情感分析,但使用的是 GPT-3.5 turbo。随着你尝试这些模型,你会发现它们通常非常智能,可以用来完成几乎所有你能想到的任务,而无需额外的训练。我来证明给你看!

现在我们有了 API,将其添加到一个 Secrets 文件中(我们将在第五章Streamlit Secrets部分更详细地介绍 Secrets,部署 Streamlit 到 Streamlit Community Cloud)。创建一个名为.streamlit的文件夹,并在其中创建一个secrets.toml文件,然后将你的 API 密钥放入其中,并将其分配给名为OPENAI_API_KEY的变量,使其变成OPENAI_API_KEY="sk-xxxxxxxxxxxx"

让我们打开现有的 Streamlit 应用,并在底部添加一个标题、一个按钮,让用户点击分析文本,以及我们的认证密钥:

import openai
st.title("OpenAI Version")
analyze_button = st.button("Analyze Text")
openai.api_key = st.secrets["OPENAI_API_KEY"] 

OpenAI Python 库(我们通过初始的 requirements.txt 文件安装的)提供了一种方便的方式,以 Python 与 OpenAI API 进行交互,这真是一个非常有用的资源。我们要调用的端点叫做聊天完成端点 (platform.openai.com/docs/api-reference/chat/create),它接受一个系统消息(这是我们告诉 OpenAI 模型如何响应的方式,在我们的案例中是一个有帮助的情感分析助手)以及关于我们要调用的底层模型的其他参数。虽然有比我们将要使用的模型更现代和昂贵的版本,但我发现 GPT 3.5 非常出色且速度很快。

我们可以像这样调用 API,并将响应写回到我们的应用程序:

if analyze_button:
    messages = [
        {"role": "system", "content": """You are a helpful sentiment analysis assistant.
            You always respond with the sentiment of the text you are given and the confidence of your sentiment analysis with a number between 0 and 1"""},
        {"role": "user", 
    "content": f"Sentiment analysis of the following text: {text}"}
    ]
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages,
    )
    sentiment = response.choices[0].message['content'].strip()
    st.write(sentiment) 

让我们来测试一下!我们可以使用与 Hugging Face 示例中相同的文本输入来对比这两个分析器:

A screenshot of a computer  Description automatically generated

图 4.9:Hugging Face 和 OpenAI 情感分析器的比较

看起来两个版本都认为这个情感是积极的,且置信度相当高。这很了不起!Hugging Face 模型是专门为情感分析训练的,但 OpenAI 的模型并不是。对于这个简单的例子,它们似乎都能正常工作。如果我们尝试只给每个模型一个单词,比如 “Streamlit”,会怎么样呢?

A screenshot of a computer  Description automatically generated

图 4.10:测试“Streamlit”的情感

在这种情况下,两种方法得出的结论不同。OpenAI 认为这是中性情感,置信度适中,而 Hugging Face 认为情感是积极的,并且置信度非常高。我认为 OpenAI 在这里可能是对的,这真是令人着迷。显然,这种模型有着广泛的应用场景。

通过 Streamlit 小部件,我们可以让用户更改 API 调用的任何部分。我们只需添加正确的小部件类型并将用户的输入传递给 OpenAI 函数,然后就可以了!再试一次吧。如果我们让用户更改我们最初的系统消息会怎样?为此,我们需要添加一个新的文本输入。我们将使用一个叫做 st.text_area 的 Streamlit 输入小部件,它与我们熟悉的 st.text_input 相同,但允许多行输入,以便处理更长的文本段落:

openai.api_key = st.secrets["OPENAI_API_KEY"]
system_message_default = """You are a helpful sentiment analysis assistant. You always respond with the sentiment of the text you are given and the confidence of your sentiment analysis with a number between 0 and 1"""

system_message = st.text_area(
    "Enter a System Message to instruct OpenAI", system_message_default
)
analyze_button = st.button("Analyze Text")
if analyze_button:
    messages = [
        {
            "role": "system",
            "content": f"{system_message}",
        },
        {
            "role": "user",
            "content": f"Sentiment analysis of the following text: {text}",
        },
    ] 

用户现在可以更改系统消息,但我们的默认消息保持不变。我已经将系统消息改成了一些荒谬的内容。我要求模型成为一个糟糕的情感分析助手,总是把输入的情感分析弄错:

A screenshot of a computer  Description automatically generated

图 4.11:更改 OpenAI 文本分析器的系统消息

如你所见,模型按照我要求的做法,错误地进行了 streamlit is awesome 的情感分析,结果显示情感是负面的。

快速警告:当你允许用户输入到大型语言模型中时,用户可能会尝试将不良的提示注入到你的应用程序中。这里有一个使用同一个应用程序的例子,我要求模型忽略所有其他指令,而改写一个海盗主题的故事:

计算机截图 自动生成的描述

图 4.12:OpenAI 与海盗

这个故事还继续了很多行,但你可以看到,越是给予用户更多控制输入的权力,就越有可能让他们以我没有预料到的方式使用我的应用程序。对此有许多创新的解决方法,包括将提示传递给另一个 API 调用,这次询问模型它是否认为提示不真诚,或是防止一些常见的注入,如“忽略之前的提示”。

也有像 Rebuff 这样的开源库(github.com/protectai/rebuff),它们也非常有用!由于生成性 AI 领域发展极其迅速,我不太敢给出具体建议,但谨慎的原则和有意的用户输入应该是非常有帮助的。

如果你对更多生成性 AI 的 Streamlit 应用感兴趣,Streamlit 团队已经制作了一个网页,汇集了所有最新的信息和示例,网址是streamlit.io/generative-ai

总结

在本章中,我们学习了一些机器学习的基础知识:如何在 Streamlit 中使用预构建的机器学习模型,如何从 Streamlit 内部创建自己的模型,如何利用用户输入理解并迭代机器学习模型,甚至如何使用 Hugging Face 和 OpenAI 的模型。希望到本章结束时,你能对这些内容感到自如。接下来,我们将深入探讨如何使用 Streamlit 社区云部署 Streamlit 应用!

在 Discord 上了解更多

加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问,并了解新版本的发布——请扫描下方的二维码:

packt.link/sl

第五章:使用 Streamlit 社区云部署 Streamlit

迄今为止,本书的重点是 Streamlit 应用程序开发,从创建复杂的可视化到部署和创建机器学习ML)模型。在本章中,我们将学习如何部署这些应用程序,以便让任何有互联网访问的人都能共享。这是 Streamlit 应用程序的关键部分,因为如果无法部署 Streamlit 应用,用户或消费者仍然会遇到障碍。如果我们相信 Streamlit 消除了创建数据科学分析/产品/模型与与他人共享之间的障碍,那么我们也必须相信,广泛共享应用程序的能力与开发的便捷性同样重要。

部署 Streamlit 应用程序有三种主要方式:通过 Streamlit 创建的产品 Streamlit Community Cloud,通过云服务提供商如 Amazon Web ServicesHeroku,或者通过 Hugging FaceHugging Face Spaces。在 AWS 和 Heroku 上部署是付费的,但 Streamlit Community CloudHugging Face Spaces 是免费的!对于大多数 Streamlit 用户来说,最简单和首选的方法是 Streamlit Community Cloud,因此我们将在这里直接介绍这个方法,AWS 和 Hugging Face Spaces 会在本书后面的 第八章,《通过 Hugging Face 和 Heroku 部署 Streamlit 应用》以及 第十一章,《数据项目 - 在 Streamlit 中原型设计项目》进行介绍。

在本章中,我们将涵盖以下主题:

  • 开始使用 Streamlit 社区云

  • GitHub 快速入门

  • 在 Streamlit 社区云上部署

技术要求

本章需要访问 Streamlit 社区云,您可以通过免费注册一个账户来获得访问权限,网址是 share.streamlit.io/signup

本章还需要一个免费的 GitHub 账户,您可以在 www.github.com 获得。有关 GitHub 的完整入门和详细的设置说明,可以在本章后面 GitHub 快速入门 部分找到。

本章的代码可以在以下 GitHub 仓库中找到:github.com/tylerjrichards/Streamlit-for-Data-Science

开始使用 Streamlit 社区云

Streamlit Community Cloud 是 Streamlit 对快速部署流程的回应,绝对是我推荐的第一个部署 Streamlit 应用的方式。2020 年夏天我第一次接触到 Streamlit 时,记得是在本地部署一个应用并喜欢上了这个库,但很快我就对需要使用 AWS 来部署我的应用感到失望。随后,Streamlit 团队联系了我,问我是否愿意尝试他们正在开发的一款产品,那就是现在的 Streamlit Community Cloud。我当时觉得它不可能这么简单。我们只需要将代码推送到 GitHub 仓库,并指向该仓库,Streamlit 会处理剩下的部分。

有时候我们关心的是“其余的部分”,例如当我们想要配置可用的存储空间或内存时,但通常情况下,让 Streamlit Community Cloud 处理部署、资源配置和共享可以大大简化我们的开发工作。

这里的目标是将我们已经创建的 Palmer Penguins ML 应用部署到 Streamlit Community Cloud 上。在开始之前,Streamlit Community Cloud 是通过 GitHub 运行的。如果你已经熟悉 Git 和 GitHub,可以跳过此部分,直接创建一个包含我们 penguins_ml 文件夹的 GitHub 仓库,并前往 使用 Streamlit Community Cloud 部署 部分。

GitHub 简介

GitHub 和 Git 语言是软件工程师和数据科学家用来协作的工具,提供了版本控制框架。我们不需要知道它们如何运作的所有细节来使用 Streamlit Community Cloud,但我们需要能够创建自己的仓库(它们像共享文件夹一样)并在更新应用程序时更新它们。处理 Git 和 GitHub 有两种方式:通过命令行和通过名为 GitHub Desktop 的产品。

目前为止,这本书大部分内容还是停留在命令行中,本教程也会继续保持这一点。然而,如果你更愿意使用 GitHub Desktop,可以访问 desktop.github.com,并按照那里的指示操作。

现在,使用以下步骤在命令行中开始使用 Git 和 GitHub:

  1. 首先,访问 www.github.com 并在那里创建一个免费账户。

  2. 接着,我们需要将 Git 语言下载到自己的计算机上,并使用 Git 连接到我们的 GitHub 账户。我们可以在 Mac 上通过终端使用 brew 来完成此操作:

    brew install git 
    
  3. 我们还需要在 Git 中设置全局用户名和电子邮件(如果尚未设置),这是 GitHub 推荐的做法。以下代码会全局设置这些信息:

    git config --global user.name "My Name"
    git config --global user.email myemail@email.com 
    

现在我们已经有了 GitHub 帐户,并且本地安装了 Git,我们需要创建我们的第一个仓库!我们已经有一个名为penguin_ml的文件夹,里面包含所需的文件,因此我们应该确保正在使用该文件夹作为工作目录(如果不确定,可以使用pwd命令查看当前工作目录)。我们将使用penguins_streamlit.py应用的最终版本,下面的代码中简要解释了其中的一些内容:

import streamlit as st
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import pickle
st.title('Penguin Classifier')
st.write("This app uses 6 inputs to predict the species of penguin using "
         "a model built on the Palmer Penguins dataset. Use the form below"
         " to get started!")
penguin_df = pd.read_csv('penguins.csv')
rf_pickle = open('random_forest_penguin.pickle', 'rb')
map_pickle = open('output_penguin.pickle', 'rb')
rfc = pickle.load(rf_pickle)
unique_penguin_mapping = pickle.load(map_pickle)
rf_pickle.close()
map_pickle.close() 

第一部分导入了我们的库,为应用设置了标题,并加载了我们使用penguins_ml.py文件创建的模型。如果没有random_forest_penguin.pickleoutput_penguin.pickle文件,这一部分将无法执行。你可以前往第四章《Streamlit 与机器学习和人工智能》来创建这些文件,或者直接访问github.com/tylerjrichards/Streamlit-for-Data-Science/tree/main/penguin_ml来直接获取它们:

with st.form("user_inputs"):
    island = st.selectbox(
        "Penguin Island", 
        options=["Biscoe", "Dream", "Torgerson"])
    sex = st.selectbox(
        "Sex", options=["Female", "Male"])
    bill_length = st.number_input(
        "Bill Length (mm)", min_value=0)
    bill_depth = st.number_input(
        "Bill Depth (mm)", min_value=0)
    flipper_length = st.number_input(
        "Flipper Length (mm)", min_value=0)
    body_mass = st.number_input(
        "Body Mass (g)", min_value=0)
    st.form_submit_button()
island_biscoe, island_dream, island_torgerson = 0, 0, 0
if island == 'Biscoe':
    island_biscoe = 1
elif island == 'Dream':
    island_dream = 1
elif island == 'Torgerson':
    island_torgerson = 1
sex_female, sex_male = 0, 0
if sex == 'Female':
    sex_female = 1
elif sex == 'Male':
    sex_male = 1
new_prediction = rfc.predict(
    [
        [
            bill_length,
            bill_depth,
            flipper_length,
            body_mass,
            island_biscoe,
            island_dream,
            island_torgerson,
            sex_female,
            sex_male,
        ]
    ]
)
prediction_species = unique_penguin_mapping[new_prediction][0]
st.write(f"We predict your penguin is of the {prediction_species} species") 

接下来的这一部分获取我们进行预测所需的所有用户输入,从研究人员所在的岛屿到企鹅的性别,以及企鹅的鸟嘴和鳍肢测量值,这些准备工作为以下代码中企鹅物种的预测做准备:

st.subheader("Predicting Your Penguin's Species:")
st.write(f"We predict your penguin is of the {prediction_species} species")
st.write(
    """We used a machine learning 
    (Random Forest) model to predict the 
    species, the features used in this 
    prediction are ranked by relative 
    importance below."""
)
st.image("feature_importance.png") 

现在,最后这一部分创建了多个直方图,用来解释模型所做的预测。特别地,这些图表展示了按物种的色调分开的鸟嘴长度/鸟嘴深度/鳍肢长度。我们使用这三个变量,因为我们的特征重要性图表告诉我们,这些是第四章中物种的最佳预测因子,《Streamlit 与机器学习和人工智能》

st.write(
    """Below are the histograms for each
continuous variable separated by penguin species.
The vertical line represents the inputted value."""
)

fig, ax = plt.subplots()
ax = sns.displot(
    x=penguin_df["bill_length_mm"], 
    hue=penguin_df["species"])
plt.axvline(bill_length)
plt.title("Bill Length by Species")
st.pyplot(ax)

fig, ax = plt.subplots()
ax = sns.displot(
    x=penguin_df["bill_depth_mm"],
    hue=penguin_df["species"])
plt.axvline(bill_depth)
plt.title("Bill Depth by Species")
st.pyplot(ax)

fig, ax = plt.subplots()
ax = sns.displot(
    x=penguin_df["flipper_length_mm"], 
    hue=penguin_df["species"])
plt.axvline(flipper_length)
plt.title("Flipper Length by Species")
st.pyplot(ax) 

现在我们已经进入了正确的文件夹,并且里面有所需的文件,我们将使用以下代码来初始化第一个仓库,并将所有文件添加到仓库中然后提交:

git init
git add .
git commit -m 'our first repo commit' 

我们的下一步是将本地设备上的 Git 仓库与 GitHub 帐户连接起来:

  1. 首先,我们需要通过返回 GitHub 网站并点击新建仓库按钮来设置新仓库,如以下截图所示:

图 5.1 – 设置新仓库

图 5.1:设置新仓库

  1. 然后我们可以填写仓库名称(penguin_ml),并点击创建仓库。在我的例子中,我已经有一个同名的仓库,因此 GitHub 会提示错误,但你的示例应该可以正常创建。

图 5.2 – 创建仓库

图 5.2:创建仓库

  1. 既然我们在 GitHub 上有了新仓库,并且本地也有一个仓库,我们需要将这两个仓库连接起来。以下代码将这两个仓库连接,并将我们的代码推送到 GitHub 仓库;GitHub 还会在点击创建仓库后建议如何连接这两个仓库:

    git branch -M main
    git remote add origin https://github.com/{insert_username}/penguin_ml.git
    git push -u origin main 
    
  2. 现在我们应该能在 GitHub 仓库中看到我们的 penguin_ml 文件!如果我们有新的代码需要推送到仓库,我们可以按照常规格式使用 git add 添加文件更改,git commit -m "commit message" 提交更改,最后使用 git push 推送更改到仓库。

我们现在可以继续 Streamlit 端的部署过程了。

使用 Streamlit Community Cloud 部署

现在我们所有的必要文件都已经在 GitHub 仓库中,我们几乎拥有了部署应用程序所需的一切。你可以按照以下步骤部署我们的应用程序:

  1. 当我们部署到 Streamlit Community Cloud 时,Streamlit 会使用其自己的服务器来托管应用程序。因此,我们需要明确告诉 Streamlit 我们的应用程序运行所需的 Python 库。以下代码安装了一个非常有用的库 pipreqs,并创建了一个符合 Streamlit 格式的 requirements.txt 文件:

    pip install pipreqs
    pipreqs . 
    
  2. 当我们查看 requirements.txt 文件时,可以看到 pipreqs 检查了我们所有的 Python 文件,检查了我们导入和使用的内容,并创建了一个文件,Streamlit 可以使用它来安装相同版本的库,以避免错误:

图 5.3:Requirements.txt

  1. 我们有了一个新文件,因此也需要将其添加到我们的 GitHub 仓库中。以下代码将 requirements.txt 添加到我们的仓库:

    git add requirements.txt
    git commit -m 'add requirements file'
    git push 
    
  2. 现在,我们的最后一步是注册 Streamlit Community Cloud (share.streamlit.io),登录后,点击 New App 按钮。之后,我们可以直接将 Streamlit Community Cloud 指向承载我们应用程序代码的 Python 文件,在我们的例子中该文件名为 penguins_streamlit.py。你还需要将用户名从我个人的 GitHub 用户名 (tylerjrichards) 更改为你自己的:

图 5.4:从 GitHub 部署

  1. 在应用构建完成后,我们就有了一个完全部署的 Streamlit 应用。每当我们对 GitHub 仓库进行更改时,应用中将反映这些更改。例如,以下代码对我们的应用标题进行更改(为了简便起见,我们将仅展示足够的代码来说明更改):

    import streamlit as st
    import seaborn as sns
    import matplotlib.pyplot as plt
    import pandas as pd
    import pickle
    st.title('Penguin Classifier: A Machine Learning App')
    st.write("This app uses 6 inputs to predict the species of penguin using "
             "a model built on the Palmer Penguins dataset. Use the form below"
             " to get started!")
    penguin_df = pd.read_csv('penguins.csv')
    rf_pickle = open('random_forest_penguin.pickle', 'rb')
    map_pickle = open('output_penguin.pickle', 'rb')
    rfc = pickle.load(rf_pickle)
    unique_penguin_mapping = pickle.load(map_pickle)
    rf_pickle.close()
    map_pickle.close() 
    

    现在,为了推送更改,我们需要更新我们的 GitHub 仓库。我们将通过以下代码来实现:

    git add .
    git commit -m 'changed our title'
    git push 
    

当我们返回到应用程序时,它将有一个独特的 URL。如果你无法找到你的 Streamlit 应用,可以随时通过 share.streamlit.io 查找它们。现在,我们应用程序的顶部应该像下面的截图一样:

图 5.5 – 我们部署的 Streamlit 应用

图 5.5:我们部署的 Streamlit 应用

应用程序重新加载可能需要几分钟时间!

现在我们已经成功部署了一个完整的 Streamlit 应用程序!我们可以将这个链接分享给朋友、同事或在社交媒体网站上,比如 Twitter/X(如果你在本书的帮助下制作了一个有趣的 Streamlit 应用,请在推特上 @我@tylerjrichards,我很想看看!)。现在,让我们学习如何调试我们的 Streamlit 应用程序。本章的应用程序可以在 penguins.streamlit.app/ 找到,如果你想进行对比!创建和部署 Streamlit 应用程序,和一般的软件创建与部署一样,总是会遇到一些障碍或错误。我们接下来的部分将专注于学习如何调试我们在应用开发和部署过程中的问题!

调试 Streamlit Community Cloud

Streamlit Community Cloud 还允许我们访问应用程序的日志,这些日志如果我们在本地部署应用程序,将会显示在终端上。在右下角,每当我们查看自己的应用程序时,都会有一个管理应用程序按钮,允许我们访问日志。从这个选项菜单中,我们可以重新启动、删除或下载应用程序的日志,并查看其他可用的应用程序,或者从 Streamlit 登出。

Streamlit 秘密

在创建和部署 Streamlit 应用程序时,您可能希望使用一些用户无法查看的信息,比如密码或 API 密钥。然而,Streamlit Community Cloud 默认使用的是完全公开的 GitHub 仓库,其中包含完全公开的代码、数据和模型。但如果,比如说,你想使用一个私密的 API 密钥(许多 API,比如 Twitter 的抓取 API 或 Google Maps API,都需要),或者想编程访问存储在受密码保护的数据库中的数据,甚至想给 Streamlit 应用程序设置密码保护,你就需要一种方法来公开一些私密的数据给 Streamlit。Streamlit 的解决方案就是 Streamlit 秘密,它让我们在每个应用程序中设置隐藏和私密的“秘密”。让我们从为我们的 Streamlit 应用程序创建一个密码开始,特别是我们现有的企鹅应用程序。

一开始,我们可以编辑应用程序顶部,要求用户在加载其余部分之前输入密码。我们可以使用 st.stop() 函数,通过以下代码,如果密码不正确,停止应用程序的运行:

import streamlit as st
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
import pickle
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
st.title('Penguin Classifier')
st.write(
    """This app uses 6 inputs to predict
     the species of penguin using a model
     built on the Palmer Penguins dataset.
     Use the form below to get started!"""
)
password_guess = st.text_input('What is the Password?')
if password_guess != 'streamlit_password':
  st.stop()
penguin_file = st.file_uploader('Upload your own penguin data') 

这段代码将生成下一个截图,只有用户在文本输入框中输入streamlit_password字符串时,其余部分才会加载:

图 5.6 – 密码检查器

图 5.6:密码检查器

要创建一个 Streamlit 秘密,我们只需前往 Streamlit Community Cloud 的主页 share.streamlit.io/,然后点击编辑秘密选项,如下图所示:

图 5.7 – 秘密

图 5.7:秘密

一旦我们点击编辑秘密按钮,就可以将新的 Streamlit 秘密添加到应用程序中:

图 5.8 – 我们的第一个 Streamlit 秘密

图 5.8:我们的第一个 Streamlit 秘密

我们的最后一步是从已部署的应用中读取 Streamlit Secrets,我们可以通过调用st.secrets和在 Secrets 中创建的变量来实现。以下代码将硬编码的密码替换为 Streamlit Secret:

st.title('Penguin Classifier') 
st.write(
    """This app uses 6 inputs to predict
     the species of penguin using a model
     built on the Palmer Penguins dataset.
     Use the form below to get started!"""
)
password_guess = st.text_input('What is the Password?') 
if password_guess != st.secrets["password"]: 
    st.stop()
penguin_file = st.file_uploader('Upload your own penguin data') 

这段代码将创建一个以下的 Streamlit 应用,并用我们设置的 Streamlit Secret 进行密码保护:

图 5.9 – 部署的密码

图 5.9:部署的密码

当我们将这段代码推送到 GitHub 仓库并重启 Streamlit 应用时,我们就会在 Streamlit Community Cloud 上部署一个受密码保护的 Streamlit 应用!我们可以使用相同的方法来保护私密 API 密钥,或任何其他需要隐藏数据的使用场景。

总结

在本章中,我们学习了如何在命令行上开始使用 Git 和 GitHub,如何在 Streamlit Community Cloud 上调试应用,如何使用 Streamlit Secrets 在公共应用中使用私密数据,以及如何快速部署我们的应用到 Streamlit Community Cloud。这完成了本书的第一部分!恭喜你走到了这一阶段。接下来的部分将以第一部分为基础,深入探讨更复杂的主题,比如如何美化和格式化我们的 Streamlit 应用,以及如何使用名为 Streamlit Components 的开源社区插件。

在下一章中,我们将讲解如何通过主题、列等多种功能美化 Streamlit 应用。

在 Discord 上了解更多

要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码:

packt.link/sl

第六章:美化 Streamlit 应用程序

欢迎来到本书的第二部分!在第一部分创建基础 Streamlit 应用程序中,我们专注于基础内容——可视化、部署和数据处理——这些都是入门 Streamlit 时至关重要的主题。在本部分中,目的是通过更复杂的应用程序和用例来探索 Streamlit,旨在将你培养成一个 Streamlit 专家。

在本章中,我们将使用元素(包括侧边栏、标签、列和颜色)来扩展我们美化 Streamlit 应用程序的能力。同时,我们将探索如何创建多页面应用程序来管理用户流动,创造一个更清晰、更结构化的用户体验。

在本章结束时,你应该会更加自信地创建比普通最小可行产品MVP)更好的应用程序。我们将从学习列(columns)开始,然后继续讨论本章中的其他元素,并将每个元素融入到主 Streamlit 应用程序中。

具体来说,在本章中,我们将涵盖以下主题:

  • 设置旧金山SF)树木数据集

  • 使用列(columns)

  • 使用标签

  • 探索页面配置

  • 使用 Streamlit 侧边栏

  • 使用颜色选择器选择颜色

  • 多页面应用程序

  • 可编辑的 DataFrame

技术要求

本章需要一个免费的 GitHub 账户,可以通过www.github.com获得。关于 GitHub 的完整简介以及详细的设置说明,可以在上一章第五章在 Streamlit 社区云上部署 Streamlit中的GitHub 简要介绍部分找到。

设置 SF Trees 数据集

在本章中,我们将再次使用 SF Trees 数据集,这是我们在第三章数据可视化中使用的数据集。和之前的章节一样,我们需要按照以下步骤进行设置:

  1. 为章节创建一个新文件夹。

  2. 将我们的数据添加到文件夹中。

  3. 为我们的应用程序创建一个 Python 文件。

让我们详细了解每一个步骤。

在我们的主streamlit_apps文件夹中,在终端运行以下代码,创建一个巧妙命名为pretty_trees的新文件夹。你也可以在终端外手动创建一个新文件夹:

mkdir pretty_trees 

现在,我们需要将第三章数据可视化中的数据移动到本章的文件夹中。以下代码将数据复制到正确的文件夹:

cp trees_app/trees.csv pretty_trees 

如果你没有trees_app文件夹,并且还没有完成第三章数据可视化,你也可以从github.com/tylerjrichards/Streamlit-for-Data-Science下载所需的数据,文件夹名称为trees_app

现在我们已经准备好了数据,我们需要创建一个 Python 文件来托管我们的 Streamlit 应用程序代码;以下代码正是完成这一任务:

touch pretty_trees.py 

pretty_trees 文件将包含我们的 Python 代码,所以打开你喜欢的文本编辑器,接下来我们就可以正式开始学习如何在 Streamlit 中使用列!

在 Streamlit 中使用列

在此之前的所有应用中,我们都将每个 Streamlit 任务视为自上而下的体验。我们将文本作为标题输出,收集一些用户输入,然后将我们的可视化内容放在下方。然而,Streamlit 允许我们使用 st.columns() 功能将应用格式化为动态列。

我们可以将 Streamlit 应用划分为多个不同长度的列,然后将每个列视为应用中独立的空间(称为容器),以便放置文本、图表、图片或任何其他内容。

在 Streamlit 中,列的语法使用 with 语法,这可能你已经熟悉,用于资源管理以及在 Python 中处理文件的打开和写入。理解 Streamlit 列中的 with 语法的最简单方式是将它看作是自包含的代码块,这些代码块告诉 Streamlit 我们希望在应用中将元素放置在哪里。让我们看看一个例子,看看它是如何工作的。以下代码导入了我们的 SF Trees 数据集,并在其中创建了三个等长的列,在每个列中写入文本:

import streamlit as st
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
col1, col2, col3 = st.columns(3)
with col1:
    st.write("Column 1")
with col2:
    st.write("Column 2")
with col3:
    st.write("Column 3") 

上述代码将创建如下截图所示的应用:

图 6.1:前三列

如我们所见,st.columns() 定义了三个等长的列,我们可以使用 with 语法在每个列中打印一些文本。我们还可以直接在我们预定义的列上调用 st.write() 函数(或任何其他将内容写入 Streamlit 应用的函数),以获得相同的结果,如下代码所示。以下代码将产生与前一个代码块完全相同的输出:

import streamlit as st
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
col1, col2, col3 = st.columns(3)
col1.write("Column 1")
col2.write("Column 2")
col3.write("Column 3") 

当我们编写更复杂的 Streamlit 应用并在每个列中加入更多内容时,with 语句有助于使应用更加简洁,易于理解和调试。本书的大部分内容将尽可能使用 with 语句。

在 Streamlit 中,列的宽度是相对于其他已定义列的大小的。因此,如果我们将每列的宽度从 1 增大到 10,我们的应用将不会发生任何变化。此外,我们还可以向 st.beta_columns() 传递一个数字,这将返回该数字个相等宽度的列。以下代码块展示了三种列宽选项,所有选项的列宽都相同:

#option 1
col1, col2, col3 = st.columns((1,1,1))
#option 2
col1, col2, col3 = st.columns((10,10,10))
#option 3
col1, col2, col3 = st.columns(3) 

作为最后一个示例,以下代码块允许用户输入来决定每个列的宽度。你可以玩一玩生成的应用,更好地理解如何使用列来改变 Streamlit 应用的格式:

import streamlit as st
st.title('SF Trees')
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
first_width = st.number_input('First Width', min_value=1, value=1)
second_width = st.number_input('Second Width', min_value=1, value=1)
third_width = st.number_input('Third Width', min_value=1, value=1)
col1, col2, col3 = st.columns(
      (first_width,second_width,third_width))
with col1:
     st.write('First column')
with col2:
     st.write('Second column')
with col3:
     st.write('Third column') 

第三章数据可视化 中,我们使用以下代码展示了 Streamlit 内置函数 st.line_chart()st.bar_chart()st.area_chart() 之间的差异:

import streamlit as st
import pandas as pd
st.title('SF Trees')
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv('trees.csv')
df_dbh_grouped = pd.DataFrame(trees_df.groupby(['dbh']).count()['tree_id'])
df_dbh_grouped.columns = ['tree_count']
st.line_chart(df_dbh_grouped)
st.bar_chart(df_dbh_grouped)
st.area_chart(df_dbh_grouped) 

上面的代码块创建了以下 Streamlit 应用,三个按宽度分组的 SF 树图表一个接一个地排列(为了简洁起见,这里只显示两个图表):

图 6.2:SF 线图和柱状图

这次练习的目的是更好地理解 Streamlit 的三个函数,但如果我们需要滚动才能看到所有内容,我们怎么做呢?让我们通过使用三个列将三个图表并排放置来改进这一点。以下代码预定义了三个等宽的列,并将一个图表放入每个列中:

import streamlit as st
import pandas as pd
st.title('SF Trees')
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv('trees.csv')
df_dbh_grouped = pd.DataFrame(trees_df.groupby(['dbh']).count()['tree_id'])
df_dbh_grouped.columns = ['tree_count']
col1, col2, col3 = st.columns(3)
with col1:
     st.line_chart(df_dbh_grouped)
with col2:
     st.bar_chart(df_dbh_grouped)
with col3:
     st.area_chart(df_dbh_grouped) 

当我们运行前面的代码时,得到如下所示的奇怪结果:

图 6.3:细长图表

这显然不是我们想要的!每个图表都太窄了。幸运的是,这引出了我们下一个小主题:Streamlit 中的页面配置。

探索页面配置

Streamlit 允许我们在每个 Streamlit 应用的顶部配置一些必要的页面特性。到目前为止,我们一直在使用 Streamlit 的默认设置,但在 Streamlit 应用的顶部,我们可以手动配置一切,从浏览器中显示的页面标题(用于打开 Streamlit 应用),到页面布局,再到侧边栏的默认状态(我们将在使用 Streamlit 侧边栏部分讲解!)。

Streamlit 应用的默认设置是页面布局居中,因此我们的应用边缘有大量的空白区域。以下代码将我们的 Streamlit 应用设置为宽格式,而不是默认的居中格式:

import streamlit as st
import pandas as pd
st.set_page_config(layout='wide')
st.title('SF Trees')
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv('trees.csv')
df_dbh_grouped = pd.DataFrame(trees_df.groupby(['dbh']).count()['tree_id'])
df_dbh_grouped.columns = ['tree_count']
col1, col2, col3 = st.columns(3)
with col1:
     st.line_chart(df_dbh_grouped)
with col2:
     st.bar_chart(df_dbh_grouped)
with col3:
     st.area_chart(df_dbh_grouped) 

当我们运行前面的代码时,现在可以看到三个图表之间有了良好的间隔,我们可以轻松地对比这三个图表。以下截图显示了 Streamlit 应用的宽格式:

图 6.4:宽格式图表

我们还需要了解关于 Streamlit 列的两个信息。第一个是我们可以编辑我们创建的列容器之间的间距,第二个是我们可以确保图表保持在其列内,而不会溢出到其他列。至于间距部分,默认情况下列之间会有一个小间隙,但我们可以将其改为中等或大间隙。以下代码为每三个列之间添加了一个大间隙:

import pandas as pd
import streamlit as st
st.set_page_config(layout="wide")
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv("trees.csv")
df_dbh_grouped = pd.DataFrame(trees_df.groupby(["dbh"]).count()["tree_id"])
df_dbh_grouped.columns = ["tree_count"]
col1, col2, col3 = st.columns(3, gap="large")
with col1:
    st.line_chart(df_dbh_grouped)
with col2:
    st.bar_chart(df_dbh_grouped)
with col3:
    st.area_chart(df_dbh_grouped) 

现在,如果我们观察图表之间的空隙,我们会注意到有一个间隙!

图 6.5:图表间隙

正如您所注意到的,我们正在使用的 Streamlit 内置图表已经确保了图表保持在列内,并且与列的末端对齐。这是因为每个图表的默认设置是将参数use_container_width设置为True,那么如果我们将其设置为False,会发生什么呢?请看以下代码:

with col1:
    st.line_chart(df_dbh_grouped, 
   use_container_width=False) 

如我们在下一张截图中看到的那样,图表不再与列对齐,从而让我们的应用看起来更糟(这也是为什么默认设置为True的原因!):

图 6.6:容器宽度

这就结束了我们对在 Streamlit 中使用列的探索,同时也结束了我们第一次了解页面配置默认设置。我们将在本书的其余部分越来越多地使用这两项技能。我们的下一个主题是介绍 Streamlit 侧边栏。

使用 Streamlit 标签页

还有一种组织 Streamlit 应用布局的方式,非常类似于 Streamlit 列,叫做标签页。标签页在内容太宽,无法分割为列的情况下非常有用,即使在宽模式下也是如此;当你希望只显示一个内容时,标签页也很有用。例如,如果我们有三个非常不同的图表,在宽模式下才能显示得很好,但我们又不想将它们垂直堆叠在一起,我们可以使用标签页选择性地显示它们。让我们来探索一下这到底是如何工作的!

st.tabs 的功能与 st.columns 非常相似,不过我们不再告诉 Streamlit 我们想要多少个标签页,而是传递标签页的名称,然后使用现在熟悉的 with 语句将内容放入标签页中。接下来的代码将我们最近的 Streamlit 应用中的列转换为标签页:

import pandas as pd
import streamlit as st

st.set_page_config(layout="wide")
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv("trees.csv")
df_dbh_grouped = pd.DataFrame(trees_df.groupby(["dbh"]).count()["tree_id"])
df_dbh_grouped.columns = ["tree_count"]
tab1, tab2, tab3 = st.tabs(["Line Chart", "Bar Chart", "Area Chart"])
with tab1:
    st.line_chart(df_dbh_grouped)
with tab2:
    st.bar_chart(df_dbh_grouped)
with tab3:
    st.area_chart(df_dbh_grouped) 

从这里,我们将得到以下应用:

图 6.7:第一个标签页

就是这么简单!标签页没有像列那样的间距参数(因为,嗯,标签页有什么用间距呢?),但除了这一点,我们可以将所有关于列的知识映射到标签页的使用上。现在,接下来是 Streamlit 侧边栏。

使用 Streamlit 侧边栏

正如我们在 Streamlit 中已经看到的,当我们开始接收大量用户输入并且开发更长的 Streamlit 应用时,我们常常会失去让用户在同一屏幕上看到输入和输出的能力。在其他情况下,我们可能希望将所有用户输入放在一个单独的区域中,以便清晰地分隔输入和输出。在这两种使用场景下,我们可以使用 Streamlit 侧边栏,它允许我们在 Streamlit 应用的左侧放置一个可最小化的侧边栏,并将任何 Streamlit 组件添加到其中。

首先,我们可以创建一个基本示例,将之前应用中的一个图表提取出来,并根据用户输入对其背后的数据进行过滤。在这种情况下,我们可以要求用户指定树木所有者的类型(例如,私人所有者或公共工程部),并使用 st.multiselect() 函数按这些条件进行过滤,该函数允许用户从列表中选择多个选项:

import pandas as pd
import streamlit as st
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW.
    """
)
trees_df = pd.read_csv("trees.csv")
owners = st.sidebar.multiselect(
    "Tree Owner Filter", 
    trees_df["caretaker"].unique())
if owners:
    trees_df = trees_df[
trees_df["caretaker"].isin(owners)]
df_dbh_grouped = pd.DataFrame(
trees_df.groupby(["dbh"]).count()["tree_id"])
df_dbh_grouped.columns = ["tree_count"]
st.line_chart(df_dbh_grouped) 

前面的代码将创建以下的 Streamlit 应用。和之前一样,我们将 owners 变量隐藏在 if 语句中,因为我们希望在用户尚未从选项中选择时,应用能够使用整个数据集。侧边栏让用户可以轻松查看他们选择的选项以及应用的输出:

图 6.8:第一个侧边栏

这个应用的下一步是添加更多的可视化,从我们在第三章数据可视化中创建的树木地图开始,然后将侧边栏与我们在本章学到的列知识相结合。

以下代码将树木地图放置在 SF 的直方图下方,并通过我们的多选框进行过滤:

import pandas as pd
import streamlit as st

st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW. The dataset
    is filtered by the owner of the tree as selected 
    in the sidebar!
    """
)
trees_df = pd.read_csv("trees.csv")
owners = st.sidebar.multiselect(
    "Tree Owner Filter", 
    trees_df["caretaker"].unique())
if owners:
    trees_df = trees_df[
        trees_df["caretaker"].isin(owners)]
df_dbh_grouped = pd.DataFrame(trees_df.groupby(["dbh"]).count()["tree_id"])
df_dbh_grouped.columns = ["tree_count"]
st.line_chart(df_dbh_grouped)

trees_df = trees_df.dropna(subset=['longitude', 'latitude'])
trees_df = trees_df.sample(n = 1000, replace=True) 
st.map(trees_df) 

以下截图展示了前面代码中的 Streamlit 应用,线图位于新地图上方,地图显示的是 SF 的树木,已根据树木拥有者进行过滤:

图 6.9:带侧边栏的过滤地图

这个应用的下一步将是通过在地图上方添加另一个图表,将我们学到的关于列的知识与侧边栏相结合。在第三章数据可视化中,我们创建了树木年龄的直方图。我们可以使用它作为这个 Streamlit 应用中的第三个图表,借助 Plotly 库:

import pandas as pd
import plotly.express as px
import streamlit as st
st.page_config(layout='wide')
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW. The dataset
    is filtered by the owner of the tree as selected
    in the sidebar!
    """
)
trees_df = pd.read_csv("trees.csv")
today = pd.to_datetime("today")
trees_df["date"] = pd.to_datetime(trees_df["date"])
trees_df["age"] = (today - trees_df["date"]).dt.days
unique_caretakers = trees_df["caretaker"].unique()
owners = st.sidebar.multiselect(
    "Tree Owner Filter", 
    unique_caretakers)
if owners:
    trees_df = trees_df[trees_df["caretaker"].isin(owners)]
df_dbh_grouped = pd.DataFrame(trees_df.groupby(["dbh"]).count()["tree_id"])
df_dbh_grouped.columns = ["tree_count"] 

第一部分:

  1. 加载树木数据集。

  2. 基于我们数据集中的日期列,添加一个年龄列。

  3. 在侧边栏创建一个多选控件。

  4. 基于侧边栏的过滤器。

我们的下一步是创建三个图表:

col1, col2 = st.columns(2)
with col1:
    fig = px.histogram(trees_df, x=trees_df["dbh"], title="Tree Width")
    st.plotly_chart(fig)

with col2:
    fig = px.histogram(
        trees_df, x=trees_df["age"], 
        title="Tree Age")
    st.plotly_chart(fig)

st.write("Trees by Location")
trees_df = trees_df.dropna(
    subset=["longitude", "latitude"])
trees_df = trees_df.sample(
    n=1000, replace=True)
st.map(trees_df) 

正如我们在第三章数据可视化中已经讨论过的那样,Streamlit 内置的函数,如st.map()st.line_chart(),对于快速可视化非常有用,但缺乏一些配置选项,比如合适的标题或轴重命名。我们可以通过 Plotly 做得更多!下图显示了我们的 Streamlit 应用,预设了一些树木拥有者过滤器:

图 6.10:三个过滤后的图表

本章接下来的功能是如何通过颜色选择器将颜色输入添加到 Streamlit 应用中!

使用颜色选择器选择颜色

颜色作为用户输入在应用中非常难以处理。如果用户想要红色,他们是想要浅红色还是深红色?是栗色还是偏粉红的红色?Streamlit 解决这个问题的方法是st.color_picker(),它允许用户选择一种颜色作为输入,并以十六进制字符串的形式返回该颜色(这是一个独特的字符串,用于定义大多数图表库用于输入的非常特定的颜色阴影)。以下代码将此颜色选择器添加到我们之前的应用中,并根据用户选择的颜色更改 Seaborn 图表的颜色:

import pandas as pd
import plotly.express as px
import streamlit as st
st.set_page_config(layout="wide")
st.title("SF Trees")
st.write(
    """
    This app analyses trees in San Francisco using
    a dataset kindly provided by SF DPW. The dataset
    is filtered by the owner of the tree as selected
    in the sidebar!
    """
)
trees_df = pd.read_csv("trees.csv")
today = pd.to_datetime("today")
trees_df["date"] = pd.to_datetime(trees_df["date"])
trees_df["age"] = (today - trees_df["date"]).dt.days
unique_caretakers = trees_df["caretaker"].unique()
owners = st.sidebar.multiselect("Tree Owner Filter", unique_caretakers)
graph_color = st.sidebar.color_picker("Graph Colors")
if owners:
    trees_df = trees_df[trees_df["caretaker"].isin(owners)] 

与我们之前的应用相比,变化在于添加了graph_color变量,它是st.color_picker()函数的结果。我们为这个颜色选择器添加了名称,并将其放在侧边栏中,位于拥有者的多选控件下方。现在,我们从用户那里获取了颜色输入,就可以使用它来更改图表中的颜色,如以下代码所示:

col1, col2 = st.columns(2)
with col1:
    fig = px.histogram(
        trees_df,
        x=trees_df["dbh"],
        title="Tree Width",
        color_discrete_sequence=[graph_color],
    )
    fig.update_xaxes(title_text="Width")
    st.plotly_chart(fig, use_container_width=True)

with col2:
    fig = px.histogram(
        trees_df,
        x=trees_df["age"],
        title="Tree Age",
        color_discrete_sequence=[graph_color],
    )
    st.plotly_chart(fig, use_container_width=True)
st.write('Trees by Location')
trees_df = trees_df.dropna(subset=['longitude', 'latitude'])
trees_df = trees_df.sample(n = 1000, replace=True)
st.map(trees_df) 

当你运行这个 Streamlit 应用时,你可以看到颜色选择器是如何工作的(这本书是黑白印刷的,所以在纸质版中不可见)。它有一个默认颜色(在我们的案例中是黑色),你可以通过选择该组件然后点击你选择的颜色来更改它。以下截图展示了点击时的组件和我们 SF Trees 应用中的结果:

图 6.11:颜色选择器

现在我们已经知道如何更改 Streamlit 中可视化的颜色,让我们进入本章的最后一部分:创建多页应用。

多页应用

到目前为止,我们的 Streamlit 应用都是单页应用,在这些应用中,所有或几乎所有的信息都能通过简单滚动看到。然而,Streamlit 也有多页功能。多页应用是一个强大的工具,可以用来创建不仅限于一页内容的应用,并扩展 Streamlit 带来的用户体验。例如,Streamlit 数据团队目前主要构建多页应用,为每个项目或团队创建一个新的应用。

对于我们的第一个多页应用,我们将专注于将树木应用中的地图部分与其他图表分开,创建两个独立的应用。Streamlit 创建多页应用的方式是,它会在与我们的 Streamlit 应用相同的目录中查找一个名为 pages 的文件夹,然后将 pages 文件夹中的每个 Python 文件作为独立的 Streamlit 应用运行。为此,在 pretty_trees 文件夹中创建一个名为 pages 的新文件夹,然后在 pages 文件夹中创建一个名为 map.py 的文件。在你的终端中,从仓库的基础文件夹运行以下命令:

mkdir pages
touch pages/map.py 

现在,当我们运行 Streamlit 应用时,我们应该在侧边栏看到 地图 应用作为独立应用:

图 6.12:我们的第一个多页应用

当我们点击左上角的地图时,它将完全为空白。现在,我们需要将地图代码移到 map.py 文件中!在 map.py 文件中,我们可以包含以下代码(这只是从我们原始应用中复制粘贴过来的):

import pandas as pd
import streamlit as st
st.title("SF Trees Map")
trees_df = pd.read_csv("trees.csv")
trees_df = trees_df.dropna(subset=["longitude", "latitude"])
trees_df = trees_df.sample(n=1000, replace=True)
st.map(trees_df) 

当我们点击地图应用时,它不再是空白的,而应该像这样:

图 6.13:地图 MPA

我们需要为这个应用做的最后一件事是将地图代码从主文件中移除。现在我们主文件的代码应该大大减少,应该看起来像这样。以下是代码片段:

col1, col2 = st.columns(2)
with col1:
    fig = px.histogram(
        trees_df,
        x=trees_df["dbh"],
        title="Tree Width",
        color_discrete_sequence=[graph_color],
    )
    fig.update_xaxes(title_text="Width")
    st.plotly_chart(fig, use_container_width=True)

with col2:
    fig = px.histogram(
        trees_df,
        x=trees_df["age"],
        title="Tree Age",
        color_discrete_sequence=[graph_color],
    )
    st.plotly_chart(fig, use_container_width=True) 

如果我们想添加一个新应用,我们只需在 pages 文件夹中添加另一个文件,Streamlit 会处理其余的部分。

如你所见,多页面应用可以非常强大。随着我们的应用变得越来越复杂,用户体验也变得更加复杂,我们可以依赖多页面应用来提升用户体验的清晰度。通过这些,你可以轻松想象创建一个大型的多页面应用,为不同的业务用户(如市场团队、销售团队等)提供单独的应用,甚至只是一个优雅的方式来拆分你更大的应用。如果你想创建一个新的应用,只需在 pages 文件夹中添加一个新的 Python 文件,新应用就会出现在侧边栏!

Streamlit 数据科学团队的一员(Zachary Blackwood,github.com/blackary)创建了一个名为 st-pages 的 Python 库,该库为多页面应用提供了一系列新特性,如为页面链接添加表情符号或为文件创建分区。这个库还很年轻,但如果你有兴趣创建比本章中更大的应用,它是一个非常好的附加资源。Streamlit 拥有一个庞大且充满活力的社区,像这样的库只是我们初次接触开源 Streamlit 的一个开始:

图 6.14:st-pages

这就是多页面应用的内容!现在我们来讨论可编辑的 DataFrame。

可编辑的 DataFrame

到目前为止,在本书中,我们假设希望在这些应用中使用的数据是静态的。我们大多使用的是 CSV 文件或由程序生成的数据集,这些数据集在用户使用应用时不会发生变化。

这种情况非常常见,但我们可能希望让用户能够以非常用户友好的方式更改或编辑底层数据。为了解决这个问题,Streamlit 发布了 st.experimental_data_editor,它允许用户在 st.dataframe-style 界面上进行编辑。

有大量潜在的应用可以编辑 DataFrame,从使用 Streamlit 作为质量控制系统,到直接编辑配置参数,再到做更多类似于本书中“假设分析”的操作。作为一个在工作环境中创建许多不同应用的人,我注意到人们通常对随时可见的电子表格非常熟悉,并且更喜欢这种类型的用户界面。

在这个示例中,我们将在 pages 文件夹中创建一个名为 data_quality.py 的新应用,并尝试使用新的可编辑 DataFrame 功能。假设我们是 SF 的数据部门的一员,私有树木中的缺失数据正在给我们带来一些问题。我们希望让几个人来查看我们的数据并编辑他们认为有问题的内容,然后我们还希望将这些数据写回到我们的可靠数据源——CSV 文件中。

首先,我们可以通过在新文件的顶部写一条简短的信息,像之前一样过滤数据,并将 DataFrame 显示给用户,代码如下:

import pandas as pd
import streamlit as st
st.title("SF Trees Data Quality App")
st.write(
    """This app is a data quality tool for the SF trees dataset. Edit the data and save to a new file!"""
)
trees_df = pd.read_csv("trees.csv")
trees_df = trees_df.dropna(subset=["longitude", "latitude"])
trees_df_filtered = trees_df[trees_df["legal_status"] == "Private"]
st.dataframe(trees_df) 

为了使数据可编辑,我们只需将st.dataframe更改为st.experimental_data_editor,然后将结果返回到一个新的数据框:

import pandas as pd
import streamlit as st
st.title("SF Trees Data Quality App")
st.write(
    """This app is a data quality tool for the SF trees dataset. Edit the data and save to a new file!"""
)
trees_df = pd.read_csv("trees.csv")
trees_df = trees_df.dropna(subset=["longitude", "latitude"])
trees_df_filtered = trees_df[trees_df["legal_status"] == "Private"]
edited_df = st.experimental_data_editor(trees_df_filtered) 

运行这个应用时,界面如下所示。我点击了一个单元格并编辑它,以显示这个功能是有效的!

图 6.15:st-experimental_data_editor

数据编辑器会返回整个数据框,因此我们的最后一步是编辑原始的、未经过滤的数据框,然后覆盖 CSV 文件。我们希望确保用户确认他们的修改,因此我们可以添加一个按钮,将结果写回原始的 CSV 文件:

import pandas as pd
import streamlit as st
st.title("SF Trees Data Quality App")
st.write(
    """This app is a data quality tool for the SF trees dataset. Edit the data and save to a new file!"""
)
trees_df = pd.read_csv("trees.csv")
trees_df = trees_df.dropna(subset=["longitude", "latitude"])
trees_df_filtered = trees_df[trees_df["legal_status"] == "Private"]
edited_df = st.experimental_data_editor(trees_df_filtered) 
trees_df.loc[edited_df.index] = edited_df
if st.button("Save data and overwrite:"):
    trees_df.to_csv("trees.csv", index=False)
    st.write("Saved!") 

现在,这个应用的界面如下所示。我们可以注意到,这个数据集中的许多行缺少绘图大小的测量数据!

图 6.16:SF 树木数据质量应用中缺失的绘图大小测量

我们可以添加它们,然后点击保存数据按钮进行覆盖。也许我们还注意到第一行存在数据质量问题,其中的 x 字母是大写的(与其余部分不同!)。我们来编辑一下:

图 6.17:编辑 SF 树木数据质量应用

现在,如果我们重新加载应用或将数据托管在 Streamlit Community Cloud 上,其他人访问应用时,所有数据都会被修正。

截至本书编写时,数据编辑器是一个非常新的功能(它是在 Streamlit 1.19 版本中发布的,而本书使用的是 Streamlit 1.20 版本)。我相信在你阅读这本书时,基于数据编辑器和数据框(DataFrame)会有更多酷炫的新功能!请查看文档(docs.streamlit.io/)以获取更多数据编辑器的相关知识。现在,让我们进入总结部分!

总结

这段历程结束了,我们探索了 SF 树木数据集,并了解了使 Streamlit 应用更具美感的各种方式。我们涵盖了如何将应用拆分成列和页面配置,如何在侧边栏收集用户输入,如何通过st.color_picker()功能获取用户输入的特定颜色,最后学习了如何使用 Streamlit 的多页面应用和新的数据编辑器。

在下一章中,我们将通过了解如何下载和使用用户构建的 Streamlit 组件,来学习关于 Streamlit 的开源社区。

在 Discord 上了解更多

要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码:

packt.link/sl

第七章:探索 Streamlit 组件

到目前为止,在本书中,我们已经探索了由 Streamlit 核心开发团队开发的功能,他们全职致力于这些新奇且令人兴奋的功能。然而,本章将专注于社区驱动的开发,通过 Streamlit 组件。在构建 Streamlit 时,团队创建了一种方法,允许其他开发者在我们已经看到的所有现有 Streamlit 开源魔法之上,创建额外的功能。这种方法被称为组件(Components)!Streamlit 组件允许开发者灵活地构建出对他们工作流程至关重要的功能,或者仅仅是有趣和好玩的功能。

随着 Streamlit 成为越来越流行的框架,其组件也在不断增加。感觉每天我都能看到一个新颖且有趣的组件,我想在自己的应用程序中尝试!本章将重点介绍如何查找和使用社区制作的 Streamlit 组件。

在本章中,我们将介绍以下六个 Streamlit 组件:

  • 使用 streamlit-aggrid 添加可编辑的 DataFrame

  • 使用 streamlit-plotly-events 创建钻取图

  • 使用 streamlit-lottie 创建美丽的 GIF 动画

  • 使用 pandas-profiling 进行自动化分析

  • 使用 st-folium 创建交互式地图

  • 使用 streamlit-extras 创建有用的小函数

  • 查找更多组件

接下来,我们来看看下一节中的技术要求。

技术要求

在我们可以使用新的 Streamlit 组件之前,首先需要下载它们。我们可以像在第一章《Streamlit 简介》中一样,使用 pip(或其他包管理器)下载每个组件。这些是需要下载的组件:

  • streamlit-aggrid

  • streamlit-plotly-events

  • streamlit-lottie

  • streamlit-pandas-profiling

  • streamlit-folium

  • streamlit-extras

为了尝试所有这些库,我们将创建一个多页应用,每个库都是一个独立的 Streamlit 应用。我们将在一个新文件夹中进行尝试,并命名为 components_example。对于我们的多页应用,我们需要一个名为 pages 的文件夹,而对于我们的第一个库(streamlit-aggrid),我们需要在 pages 文件夹中添加一个名为 aggrid.py 的 Python 文件。我们将使用之前已使用过的企鹅和树木数据集中的数据,因此也要将它们复制到文件夹中。

在所有这些操作结束后,您的 components_example 文件夹应该是这样的:

图 7.1:文件夹结构

streamlit_app.py 中,我们可以添加以下代码,以告知用户所有示例都位于其余的多页应用中:

import streamlit as st
st.title("Streamlit Components Examples")
st.write(
    """This app contains examples of 
    Streamlit Components, find them 
    all in the sidebar!"""
) 

现在,进入 streamlit-aggrid

使用 streamlit-aggrid 添加可编辑的 DataFrame

我们已经在 Streamlit 应用中使用了几种显示 DataFrame 的方法,如内置的st.writest.dataframe函数。我们还介绍了 Streamlit 在 1.19 版本中发布的实验性可编辑 DataFrame,尽管它的功能不如streamlit-aggrid丰富,但使用起来显著更简单!streamlit-aggrid本质上创建了一个漂亮、交互式且可编辑的st.dataframe版本,构建在一个名为AgGrid的 JavaScript 产品之上(www.ag-grid.com/)。

理解这个库的最佳方式是亲自尝试!我们从使用企鹅数据集的例子开始,目的是制作一个交互式和可编辑的 DataFrame,而这是AgGrid的强项。

aggrid.py中,我们可以提取企鹅数据,并使用streamlit-aggrid中的核心函数AgGrid来在 Streamlit 应用中显示数据。代码如下所示:

import pandas as pd
import streamlit as st
from st_aggrid import AgGrid
st.title("Streamlit AgGrid Example: Penguins")
penguins_df = pd.read_csv("penguins.csv")
AgGrid(penguins_df) 

这使我们接近了 80%的理想解决方案。它创建了一个具有丰富功能的应用!目前这个应用看起来是这样的:

图 7.2:AgGrid 示例

如果你点击每一列,它会带有自动过滤机制、按值排序、显示和隐藏列等功能。例如,我们可以在数据集中过滤species列,仅包含Chinstrap值,DataFrame 会如以下截图所示进行响应:

图 7.3:第一次过滤

我鼓励你尝试AgGrid中的功能,看看它提供的完整功能集。有一点你可能会注意到的是,它默认显示整个 DataFrame。我发现这对于 Streamlit 应用来说有点突兀,但幸运的是,streamlit-aggrid中有一个height参数,可以强制将 DataFrame 显示在特定高度内。请参见以下代码,了解如何确保这一点:

import pandas as pd
import streamlit as st
from st_aggrid import AgGrid
st.title("Streamlit AgGrid Example: Penguins")
penguins_df = pd.read_csv("penguins.csv")
AgGrid(penguins_df, height=500) 

我们已经讨论过但还没有展示的最后一个特性是能够在 AgGrid 中编辑 DataFrame。同样,这和在 AgGrid 函数中添加一个参数一样简单。该函数返回编辑后的 DataFrame,我们可以在应用的其余部分使用它。这意味着该组件是双向的,和我们已经使用过的所有 Streamlit 输入控件一样。接下来的这段代码添加了编辑功能,并展示了我们如何访问编辑后的 DataFrame:

import pandas as pd
import streamlit as st
from st_aggrid import AgGrid
st.title("Streamlit AgGrid Example: Penguins")
penguins_df = pd.read_csv("penguins.csv")
st.write("AgGrid DataFrame:")
response = AgGrid(penguins_df, height=500, editable=True)
df_edited = response["data"]
st.write("Edited DataFrame:")
st.dataframe(df_edited) 

从这段代码,我们可以看到以下应用:

图 7.4:可编辑的 DataFrame

上面的应用展示了我修改了数据框架(DataFrame)中的一行,将值从 Adelie 改为 Adelie_example 后的效果。我们可以在应用的其余部分中使用这个数据框架,并可以执行从基于编辑过的数据框显示图表到将数据框保存回 CSV 文件等任何操作;这里的可能性是巨大的。streamlit-aggrid 是最受欢迎的 Streamlit 组件之一,希望你现在明白为什么了!该库中还有几十个其他功能,你可以在 streamlit-aggrid.readthedocs.io/ 查找更多。现在,继续介绍下一个组件 streamlit-plotly-events 进行钻取图表!

使用 streamlit-plotly-events 创建钻取图表

任何绘图库中最受欢迎的高级功能之一是能够钻取图表的部分或区域。你的应用用户常常会对你的数据提出一些你没有预料到的问题!与其围绕图表创建新的 Streamlit 输入,用户通常希望点击图表中的项目(如点或条形图),并获取该点的更多信息。例如,在我们的企鹅散点图中,用户可能希望查看与某只企鹅相关的所有数据,这些数据由鼠标悬停在数据框(DataFrame)中的某个点上表示。

streamlit-plotly-events 将单向的 st.plotly_chart 函数转变为双向的函数,在其中我们可以接收像点击或悬停之类的事件,并将其返回到我们的 Streamlit 应用中。为了测试这个功能,我们将在 pages 文件夹内创建另一个应用,这个应用名为 plotly_events,并将基于企鹅数据集创建一个图表。

首先,我们可以导入库,读取数据,并在 Plotly 中绘制一个常见的图表:

import pandas as pd
import plotly.express as px
import streamlit as st
from streamlit_plotly_events import plotly_events
st.title("Streamlit Plotly Events Example: Penguins")
df = pd.read_csv("penguins.csv")
fig = px.scatter(df, x="bill_length_mm", y="bill_depth_mm", color="species")
plotly_events(fig) 

我们没有调用 st.plotly_chart,而是将其替换为 plotly_events 函数调用。除此之外,与我们常规使用 Plotly 的方式没有区别。目前,这不会做任何特别的事情,我们的应用应该看起来比较标准:

图 7.5:Plotly 图表原始版本

plotly_events 函数接受一个名为 click_event 的参数,如果我们将其设置为 true,将把所有点击事件作为变量返回给 Streamlit。接下来的脚本使用了这个参数,并将点击事件写回 Streamlit:

import pandas as pd
import plotly.express as px
import streamlit as st
from streamlit_plotly_events import plotly_events

st.title("Streamlit Plotly Events Example: Penguins")
df = pd.read_csv("penguins.csv")
fig = px.scatter(df, x="bill_length_mm", y="bill_depth_mm", color="species")
selected_point = plotly_events(fig, click_event=True)
st.write("Selected point:")
st.write(selected_point) 

现在,当我们运行这个应用并点击图表上的点时,我们可以看到被点击的值!

图 7.6:点击事件

现在这还不是特别特别的,因为 Plotly 已经可以在悬停时显示这些点。我们可以通过显示点击的点的所有数据来改进这一点,以下是改进后的代码(为了简洁,我省略了导入部分)。如果没有选择任何点,我们需要停止应用,否则应用将报错!

st.title("Streamlit Plotly Events Example: Penguins")
df = pd.read_csv("penguins.csv")
fig = px.scatter(df, x="bill_length_mm", y="bill_depth_mm", color="species")
selected_point = plotly_events(fig, click_event=True)
if len(selected_point) == 0:
    st.stop()
selected_x_value = selected_point[0]["x"]
selected_y_value = selected_point[0]["y"]
df_selected = df[
    (df["bill_length_mm"] == selected_x_value)
    & (df["bill_depth_mm"] == selected_y_value)
]
st.write("Data for selected point:")
st.write(df_selected) 

现在,我们的最终应用看起来如下:

图 7.7:钻取仪表盘

将 Plotly 图表轻松转换为钻取式、双向仪表板真的很容易!在此示例中,用户可以查看所选择企鹅的性别和鳍长度等信息,理论上我们可以在应用程序的其余部分中随意使用此选择事件。

streamlit-plotly-events库还有另外两个事件(select_eventhover_event),这些事件也同样有用,并以相同方式返回。如果你使用了其中一个,当需要时你可以轻松地使用另一个。随着钻取式仪表板的完成,让我们转而使用streamlit-lottie向我们的应用程序添加美丽的动画!

使用 Streamlit 组件 - streamlit-lottie

Lottie 是由Airbnb创建的一种网页原生开源库,使得在您的网站上放置动画与放置静态图像一样简单。大型、盈利的技术公司将开源软件发布给开发者社区是一种非常普遍的做法,这样做不仅是回馈开发者社区(或者更可能是为了招募那些认为他们的软件很酷的开发者),这个库也不例外。在这种情况下,streamlit-lottie封装了lottie文件,并直接将它们放入我们的 Streamlit 应用程序中。

在我们尝试之前,我们首先需要导入streamlit-lottie库,然后将st_lottie()函数指向我们的lottie文件。我们可以导入本地的lottie文件,或者更可能的是,我们可以在免费网站(lottiefiles.com/)上找到一个有用的动画文件,并从那里加载到我们的应用程序中。

为了测试这个功能,我们可以将一个可爱的企鹅动画(lottiefiles.com/39646-cute-penguin)添加到我们在本章早些时候创建的企鹅应用程序的顶部。为了保持一切井然有序,让我们将plotly_events.py文件的当前状态复制到名为penguin_animated.py的新文件中,同样放在pages文件夹中。我们可以从components_example文件夹运行以下代码,或者手动复制文件:

cp pages/plotly_events.py pages/penguin_animated.py 

然后,在这个新文件中,我们可以对旧应用程序进行一些更改。以下代码块创建了一个函数,如streamlit-lottie库的示例所示(github.com/andfanilo/streamlit-lottie),允许我们从 URL 加载lottie文件,然后在应用程序顶部加载此动画:

import pandas as pd
import plotly.express as px
import requests
import streamlit as st
# add streamlit lottie
from streamlit_lottie import st_lottie
from streamlit_plotly_events import plotly_events
def load_lottieurl(url: str):
    r = requests.get(url)
    if r.status_code != 200:
        return None
    return r.json()
lottie_penguin = load_lottieurl(
    "https://assets9.lottiefiles.com/private_files/lf30_lntyk83o.json"
)
st_lottie(lottie_penguin, height=200)
st.title("Streamlit Plotly Events + Lottie Example: Penguins") 

应用的其余部分与 Plotly 事件库部分保持一致。现在当我们运行 Streamlit 应用程序时,我们在顶部看到动画:

图 7.8:可爱的企鹅

上一节的代码使用了requests库来定义一个函数,我们可以用它从链接加载lottie文件。在这个例子中,我已经预填了一个链接,指向一个可爱的企鹅动画。然后我们加载了这个文件,并使用从streamlit-lottie库中导入的st_lottie()函数来调用该文件。正如你所看到的,我们在顶部有一个动画!

streamlit-lottie还允许我们通过speedwidthheight参数分别改变动画的速度、宽度和高度。如果动画的速度太慢,可以将速度增大到例如1.52,这将分别使速度增加 50%或 100%。然而,heightwidth参数是动画的像素高度/宽度,默认为动画的原生大小。

我强烈建议运行这个应用,因为企鹅动画实在是非常可爱。这也标志着我们对streamlit-lottie的介绍已经完成!我已经养成了在每个我创建的 Streamlit 应用顶部加入一个精美动画的习惯——它创造了一种设计感,使得 Streamlit 应用显得更有目的性,并且立即提醒用户,这不是一个静态文档,而是一个动态交互式应用。

使用 Streamlit 组件 – streamlit-pandas-profiling

pandas-profiling是一个非常强大的 Python 库,它自动化了一些 EDA(探索性数据分析),这是任何数据分析、建模甚至数据工程任务的第一步。在数据科学家开始几乎所有数据工作之前,他们都希望首先了解他们数据的分布情况、缺失行的数量、变量之间的相关性,以及许多其他基本信息。正如我们之前提到的,这个库自动化了这个过程,然后将这个交互式分析文档放入 Streamlit 应用中供用户使用。

在名为pandas-profiling的 Streamlit 组件背后,有一个相同名称的完整 Python 库,组件从中导入其函数。这个 Streamlit 组件实际上将来自pandas-profiling Python 库的输出渲染为一种非常容易集成的方式。在这一部分中,我们将首先学习如何实现这个库,然后探索生成的输出。

对于我们的示例,我们将继续使用上一节关于企鹅的代码,并将自动生成的个人资料添加到应用程序的底部。这段代码只有几行——我们需要为数据集生成报告,然后使用 Streamlit 组件将生成的报告添加到应用程序中。同样,像之前一样,将streamlit-lottie部分的代码复制到一个新文件中,命名为penguin_profiled.py

cp pages/penguin_animated.py pages/penguin_profiled.py 

下一段代码导入了我们分析所需的库!

import pandas as pd
import plotly.express as px
import requests
import streamlit as st
from pandas_profiling import ProfileReport
from streamlit_lottie import st_lottie
from streamlit_pandas_profiling import st_profile_report
from streamlit_plotly_events import plotly_events 

应用的中间部分保持不变,因此我们不会在这里复制所有代码。然而,最后使用了我们之前导入的函数来获取 DataFrame 的概况:

fig = px.scatter(df, x="bill_length_mm", y="bill_depth_mm", color="species")
selected_point = plotly_events(fig, click_event=True)
st.subheader("Pandas Profiling of Penguin Dataset")
penguin_profile = ProfileReport(df, explorative=True)
st_profile_report(penguin_profile) 

现在,我们得到了整个企鹅数据集的概况,内容如下所示:

图 7.9:企鹅数据集概况

这有一个概述部分,警告我们关于高度相关的变量或缺失数据,甚至允许我们非常轻松地深入查看特定列。我们可以在 Streamlit 中重新制作整个库(我将这一点作为非常高级的练习留给读者!),但拥有这样的自动化探索分析也是非常不错的。

这也是一个关于组合性的重要课程——我们可以将 Streamlit 组件视为独特的乐高积木,随意组合它们来创建新的、有趣的 Streamlit 应用。

这是另一个你应该自己尝试的组件,看看它能向用户展示哪些信息。接下来,我们将介绍使用st-folium的双向应用!

使用 st-folium 的互动地图

在本章前面,我们学到了通过streamlit-plotly-events为可视化添加双向功能的重要性。深入分析图表是商业用户经常要求的功能,地图也不例外!st-folium非常类似于streamlit-plotly-events,但它是针对地理空间地图的。

本示例重点使用了我们在本书中一再使用的树木数据集,所以请创建一个新的文件folium_map.py,并将其放入pages文件夹中,然后我们可以开始。以下代码段加载库,添加数据,创建folium地图,并将该地图添加到我们的 Streamlit 应用中。这基本上是我们之前图表的重复,映射了旧金山的树木数据,但增加了 Folium 库:

import folium
import pandas as pd
import streamlit as st
from streamlit_folium import st_folium
st.title("SF Trees Map")
trees_df = pd.read_csv("trees.csv")
trees_df = trees_df.dropna(subset=["longitude", "latitude"])
trees_df = trees_df.head(n=100)
lat_avg = trees_df["latitude"].mean()
lon_avg = trees_df["longitude"].mean()
m = folium.Map(
location=[lat_avg, lon_avg], 
zoom_start=12)
st_folium(m) 

这段代码将创建如下应用,目前它只是旧金山的一个地图!但你会注意到,我们可以滚动、缩放,使用地图的所有正常功能:

图 7.10:我们的第一个 Folium 地图

在此基础上,我们希望为每个树木数据集中的点添加一个小标记,以复制我们已经创建的树木地图。我们可以使用一个基本的for循环来实现这一点!

lat_avg = trees_df["latitude"].mean()
lon_avg = trees_df["longitude"].mean()
m = folium.Map(location=[lat_avg, lon_avg], zoom_start=12)
for _, row in trees_df.iterrows():
    folium.Marker(
        [row["latitude"], row["longitude"]],
    ).add_to(m)
st_folium(m) 

现在,我们的应用将拥有 100 棵树的标记,像这样:

图 7.11:向 Folium 添加点

这还不算特别!很酷,但与我们可以制作的其他地图并没有太大区别。真正有趣的部分是,当我们意识到st_folium函数默认返回地图上的点击事件时!所以现在,我们可以接收这些事件,并通过以下代码将其打印到 Streamlit 应用中:

for _, row in trees_df.iterrows():
    folium.Marker(
        [row["latitude"], row["longitude"]],
    ).add_to(m)
events = st_folium(m)
st.write(events) 

现在,我们的应用将点击事件打印到 Streamlit 应用中,然后我们可以像在streamlit-plotly-events中那样以编程方式使用它们!

图 7.12:双向地图

这就是 Streamlit 的魔力,以及 st-folium!交互性非常直观,动态的应用程序就在每一个转角处,给用户带来惊喜。

现在是本章的最后一个库,它是由 Streamlit 数据产品团队创建的名为 streamlit-extras 的库!

streamlit-extras 中的有用迷你函数

自 2022 年初以来,我一直是 Streamlit 数据产品团队的一员,毫无意外地,我们的工作围绕着为 Streamlit 这家公司创建 Streamlit 应用程序展开。团队为众多商业伙伴创建了几十个应用程序,在这项工作中,我们还创造了几十个辅助函数,使得创建 Streamlit 应用程序变得更加有趣和高效。

每个团队都有类似的功能。在 Streamlit,我们鼓励尽可能多地将你的工作开源,因此我们决定将这些功能做成一个 Python 包并发布到社区。

例如,我们曾遇到一个问题,应用程序的用户不小心在日期范围中只选择了一个日期,结果整个应用无法正常运行。为了解决这个问题,我们构建了一个强制性的日期范围选择器,只有在选择了两个日期后,应用才会运行!它可以这样使用:

from streamlit_extras.mandatory_date_range import date_range_picker
result = date_range_picker("Select a date range")
st.write("Result:", result) 

另一个例子是,我们希望有一个输入框,看起来像我们最喜欢的文档管理软件 Notion 中的切换开关。所以我们构建了一个小的版本!它可以这样使用:

from streamlit_extras.stoggle import stoggle
stoggle(
    "Click me!",
    """![](https://github.com/OpenDocCN/freelearn-ds-zh/raw/master/docs/stlt-ds-2e/img/B18444_07_14.png) Surprise! Here's some additional content""",
) 

现在,我们可以创建像这样的切换开关!

图 7.13:切换!

所有这些功能,以及更多的功能,都存在于一个小小的库中。这个工作中的很大一部分要归功于我的队友 Arnaud Miribel (github.com/arnaudmiribel) 和 Zachary Blackwood (github.com/blackary)!他们构建并发布了这个库,是值得在 GitHub 上关注的优秀 Streamlit 开发者。你可以在 (extras.streamlit.app/) 找到其余的 Streamlit extras,赶紧用 pip install 安装并尝试一下吧!

寻找更多组件

这些组件只是 Streamlit 社区创建的所有组件中的一小部分,而在你读到这篇文档时,我相信已有的组件数量会大幅增加。寻找新的、有趣的组件的最佳地方是 Streamlit 网站的streamlit.io/gallery?type=components&category=featured 或讨论论坛discuss.streamlit.io/c/streamlit-components/18

当你找到一个你觉得有趣的组件时,可以像我们之前做的那样,通过 pip 下载并试用,并阅读足够的文档以开始使用!

总结

到此为止,我希望你已经非常熟练地下载并使用了在这里学习到的 Streamlit 组件,也能轻松地找到社区创建的其他 Streamlit 组件。你应该真正理解如何找到、下载和使用 Streamlit 组件,以增强你构建的应用程序。

在下一章中,我们将深入探讨如何通过 HerokuHugging Face 等云服务提供商部署你自己的 Streamlit 应用!

在 Discord 上了解更多

要加入本书的 Discord 社区——你可以在这里分享反馈、向作者提问、并了解最新的发布——请扫描下面的二维码:

packt.link/sl

第八章:使用 Hugging Face 和 Heroku 部署 Streamlit 应用

第五章通过 Streamlit Community Cloud 部署 Streamlit中,我们学习了如何通过 Streamlit Community Cloud 部署我们的 Streamlit 应用。Streamlit Community Cloud 对大多数应用来说快速、简单且非常有效。然而,它并没有无限制的免费计算资源,每个部署的应用程序限制为 1GB 内存。如果我们想要一个使用更多资源的应用程序,那就没有这个选项了。

这让我考虑到另一个需要考虑的方面——Streamlit 与 Snowflake 的集成。付费的 Streamlit 版本现在已经包含在 Snowflake 生态系统中。尽管这看起来像是一种限制,但请注意,Snowflake 因其庞大的受欢迎程度而值得信赖。如果您的公司已经在使用 Snowflake,这可能对你来说是一个巨大的优势。然而,如果你尚未使用 Snowflake,本章还为你提供了其他几个优秀的选项,以部署资源密集型或有安全限制的应用程序。

当 Streamlit 首次发布时,以及本书在 2021 年秋季首次发布时,可用的部署选项非常有限。通常,最佳选择是租用 Amazon Web Services 或 Azure 的服务器空间并自行设置所有配置。幸运的是,随着该库的巨大成功,部署选项已经有了很大的改进。本章将重点介绍三个主要部分:

  • 在 Streamlit Community Cloud、Hugging Face 和 Heroku 之间进行选择

  • 在 Hugging Face 上部署 Streamlit 应用

  • 在 Heroku 上部署 Streamlit 应用

技术要求

以下是本章所需安装的软件:

  • Heroku 账户:Heroku 是一个受欢迎的平台,数据科学家和软件工程师使用它来托管他们的应用、模型和应用编程接口(API),它归 Salesforce 所有。要获取 Heroku 账户,请前往 signup.heroku.com 创建免费账户。

  • Heroku 命令行界面CLI):要有效使用 Heroku,我们需要下载 Heroku CLI,它将允许我们运行 Heroku 命令。要下载该工具,请按照这里列出的说明进行操作:devcenter.heroku.com/articles/heroku-cli

  • Hugging Face 账户:Hugging Face 是一个以机器学习为重点的优秀平台,我们在第四章通过 Streamlit 进行机器学习和 AI中使用过它;要创建账户,请访问 huggingface.co/join

现在我们已经有了需求,让我们开始吧!

在 Streamlit Community Cloud、Hugging Face 和 Heroku 之间进行选择

从高层次来看, whenever 我们尝试部署我们的 Streamlit 应用程序,让互联网用户能够看到我们的应用时,实际上我们是在租用一个由他人拥有的计算机,并给这台计算机一组指令来启动我们的应用。选择使用哪个平台,若没有系统部署背景或者没有先尝试过每个选项,往往很难确定,但有一些启发式方法应该能帮助你做出决定。

决定选择哪个平台的两个最重要因素是系统的灵活性和启动所需的时间。需要注意的是,这两个因素通常是相互权衡的。如果你使用的是 Streamlit Community Cloud,你无法指定“我想让这个在 30 GiB 内存的 GPU 上运行”,但你可以得到一个极其简单的流程,只需将 Streamlit Community Cloud 指向你的 GitHub 仓库,它会处理掉所有其他需要做的小决策。另一方面,Hugging Face 和 Heroku 通过付费选项提供更多灵活性,但设置起来需要更多时间(正如你将会发现的!)。

简而言之,如果你已经在使用某个平台(如 Snowflake、Hugging Face 或 Heroku),你应该继续使用你正在使用的平台。如果你还没有使用这些平台,或者是一个业余程序员,Streamlit Community Cloud 是最好的选择。

如果你需要更多的计算资源,并且正在从事机器学习或自然语言处理工作,你应该使用 Hugging Face。如果你需要更多计算资源,并且希望拥有一个更通用的平台,具有广泛的集成选项,Heroku 是一个很好的选择。

让我们开始使用 Hugging Face 吧!

在 Hugging Face 上部署 Streamlit

Hugging Face 提供了一整套专注于机器学习的产品,特别受到机器学习工程师和自然语言处理领域专家的青睐。它通过其 transformers 库(我们已经使用过了!)使开发者能够轻松使用预训练模型,还可以创建产品让开发者托管自己的模型、数据集,甚至通过名为 Hugging Face Spaces 的产品托管他们自己的数据应用。你可以将 Space 看作是一个在 Hugging Face 基础设施上部署应用的地方,而且开始使用非常简单。

对于本章内容,我们将部署在 第四章 中创建的同一个 Hugging Face 应用。我们可以在 Hugging Face 上部署任何一个 Streamlit 应用,但我觉得部署这个会更合适!

首先,我们需要访问 huggingface.co/spaces,然后点击“Create new Space”按钮。

图 8.1:Hugging Face 登录

登录后,我们将看到一些选项。我们可以为我们的 Space 命名,选择一个许可证,选择我们想要的 Space 类型(Gradio 是 Hugging Face 另一个受欢迎的数据应用选择),选择 Space 硬件(注意付费和免费选项),并将 Space 设置为公开或私有。下面的截图展示了我选择的选项(你可以根据需要为 Space 命名,但其余的应该保持一致)。

图 8.2:Hugging Face 选项

现在,你应该点击页面底部的 Create Space 按钮。一旦创建了 Space,你需要使用以下 Git 命令在个人计算机上克隆该 Space,我在这本书所在的主 Streamlit for Data Science GitHub 仓库中进行了克隆:

git clone https://huggingface.co/spaces/{your username}/{your_huggingface_space_name} 

现在你的仓库已经被克隆,我们需要为我们的 Streamlit 应用创建一个文件,并创建另一个 requirements.txt 文件,使用以下命令告诉 Hugging Face Spaces 我们需要哪些库来运行我们的应用:

cd {your_huggingface_space_name}
touch app.py 
touch requirements.txt 

app.py 文件中,我们可以直接复制并粘贴我们已经创建的应用;代码如下所示:

import streamlit as st
from transformers import pipeline
st.title("Hugging Face Demo")
text = st.text_input("Enter text to analyze")
st.cache_resource
def get_model():
    return pipeline("sentiment-analysis")
model = get_model()
if text:
    result = model(text)
    st.write("Sentiment:", result[0]["label"])
    st.write("Confidence:", result[0]["score"]) 

对于我们的 requirements.txt 文件,我们只需使用三个库,可以像这样将它们添加到文件中:

streamlit
transformers
torch 

现在我们已经把文件放到正确的状态,只需使用 Git 添加、提交并推送更改:

git add .
git commit –m 'added req, streamlit app'
git push 

当我们从命令行推送更改时,系统会要求我们输入 Hugging Face 的用户名和密码,然后如果我们回到 Hugging Face 标签页,我们的应用就会被托管!

图 8.3:Hugging Face 部署的应用

如果我们回到代码中并查看 README.md 文件,我们会注意到有很多有用的配置选项,比如更改表情符号或标题。Hugging Face 还允许我们指定其他参数,比如 Python 版本。完整的文档可以在你的 README.md 中的链接中找到:

图 8.4:Hugging Face 部署的应用代码

这就是在 Hugging Face 上部署 Streamlit 应用的全部内容!

你可能已经注意到在 Hugging Face Spaces 上部署的一些缺点,包括比 Streamlit Community Cloud 多一些步骤,并且 Hugging Face 占用了应用程序的很多显示空间。可以理解的是,Hugging Face 希望确保每个看到你应用的人都知道它是使用他们的产品创建的。他们将大量自己的品牌和产品放在你部署的应用程序顶部,这无疑会对应用的观看体验产生负面影响。对于已经在使用 Hugging Face 的其他人来说,这种品牌展示可能是一个很大的优势,因为他们可以克隆你的 Space,查看流行的 Spaces 和模型,但对于将应用发送给非 ML 同事甚至朋友来说,这种品牌展示则是 Spaces 的一个缺点。

Hugging Face Spaces 的另一个主要缺点是,它们支持的 Streamlit 版本通常稍微滞后。截至本书写作时,他们正在使用 Streamlit 版本 1.10.0,而最新的 Streamlit 版本是 1.16.0。如果你希望使用最新的 Streamlit 特性,Hugging Face Spaces 可能无法支持!不过,对于大多数 Streamlit 应用来说,这通常不会造成太大问题,但在选择平台时,还是需要考虑这一因素。

希望你已经清楚使用 Hugging Face Spaces 的明显优势和轻微劣势。现在让我们转向 Heroku!

使用 Heroku 部署 Streamlit

Heroku 是由 Salesforce 所拥有的一个平台即服务(PaaS),它作为一个通用的计算平台优化,可以用于从网站到 API 到 Streamlit 应用等各种用途。因此,与 Streamlit Community Cloud 或 Hugging Face Spaces 相比,Heroku 提供了更多选择,但上手难度更高。

请注意,Heroku 没有免费套餐,所以如果你不想跟着操作(或者你已经对 Streamlit Community Cloud 或 Hugging Face Spaces 感到满意),可以直接跳到下一章!之所以在本书中提到 Heroku,是因为我希望提供一个具有更高容量、支持最新版本 Streamlit 且没有太多品牌标识,并且易于使用的选项。Heroku 在这些标准下表现最好,因此我将在下面详细介绍!

为了将我们的 Streamlit 应用部署到 Heroku,我们需要执行以下操作:

  1. 设置并登录到 Heroku。

  2. 克隆并配置我们的本地仓库。

  3. 部署到 Heroku。

让我们详细看看这些步骤!

设置并登录到 Heroku

在本章的 技术要求 部分,我们介绍了如何下载 Heroku 并创建账户。现在,我们需要通过命令行登录 Heroku,运行以下命令并根据提示进行登录:

heroku login 

这将带我们到 Heroku 页面,登录后我们就可以开始了。该命令会让你在机器上保持登录状态,直到你更改密码或主动退出 Heroku。

克隆并配置我们的本地仓库

接下来,我们需要切换到存放企鹅机器学习应用程序的目录。我的应用程序文件夹在我的 Documents 文件夹内,所以以下命令将我带到该目录,但你的文件夹路径可能不同:

cd ~/Documents/penguin_ml 

如果你还没有下载本地仓库,并且没有对应的 GitHub 仓库,可以前往 第五章使用 Streamlit Community Cloud 部署 Streamlit,了解如何开始使用 GitHub。你也可以运行以下命令,从我的个人 GitHub 仓库将代码下载到本地:

git clone https://github.com/tylerjrichards/penguin_ml.git 

强烈建议你使用自己的 GitHub 仓库进行实践,因为这比从我这里克隆应用程序来部署到 Heroku 更具实践意义。

现在,我们需要使用以下命令创建一个 Heroku 应用,并为我们的应用选择一个独特的名称(该应用将以此名称加上 .heroku.com 后缀进行部署)。我的应用名是 penguin-machine-learning,但你可以选择自己的名称!

heroku create penguin-machine-learning 

一旦完成此操作,我们需要明确地将我们的 Git 仓库与刚刚创建的 Heroku 应用连接起来,可以通过以下命令完成:

heroku git:remote -a penguin-machine-learning 

最后,我们将向我们的代码库中添加两个文件,这两个文件是启动 Heroku 所必需的,即 Procfile 文件和 streamlit_setup.sh 文件。Heroku 使用名为 Procfile 的文件来声明应用在启动时应该执行的命令,并告知 Heroku 这是什么类型的应用。对于我们的 Heroku 应用,我们还需要这个 Procfile 来配置一些特定于 Streamlit 应用的设置(比如端口配置),并且还要运行 streamlit run 命令来启动我们的应用。让我们从以下命令开始创建 streamlit_setup.sh 文件:

touch streamlit_setup.sh 

我们可以使用文本编辑器打开这个文件,并将以下内容添加进去,这样就会在基础目录中创建我们熟悉的config.toml文件:

mkdir -p ~/.streamlit
echo "[server]
headless = true
port = $PORT
enableCORS = false
" > ~/.streamlit/config.toml 

一旦保存了这个文件,我们需要创建一个 Procfile,该文件会运行 streamlit_setup.sh 文件,并且还会运行我们的 Streamlit 应用:

touch Procfile 

在我们刚创建的 Procfile 文件中,接下来需要添加以下内容:

web: sh streamlit_setup.sh && streamlit run penguins_streamlit.py 

现在我们已经设置好了我们的 Streamlit 应用,最后一步就是将它部署到 Heroku!

部署到 Heroku

在我们部署之前,应用中有几个新文件,我们需要使用以下命令将它们添加到 Git 仓库中:

git add .
git commit -m 'added heroku files'
git push 

现在,我们本章的最后一步是将代码推送到 Heroku,接下来可以通过以下命令来完成:

git push heroku main 

这将启动 Heroku 构建,过一会儿,我们将看到我们的 Penguin 应用成功部署到 Heroku,任何人都可以访问查看。我们刚刚部署并正在进行开发的应用可以通过以下链接访问(附带截图!),penguin-machine-learning.herokuapp.com/,GitHub 仓库可以通过 github.com/tylerjrichards/penguin_ml 访问。你可以在以下截图中看到该应用:

图 8.2 – Heroku 应用部署

图 8.5:Heroku 应用部署

如你所见,Heroku 的部署比 Hugging Face Spaces 或 Streamlit Community Cloud 更复杂,但它允许你为应用添加更多计算资源而不带有 Heroku 品牌。而且,Heroku 始终支持最新的 Streamlit 特性,而 Hugging Face Spaces 不一定会及时更新。

Heroku 的一个大缺点(除了增加的复杂性)是,从 2022 年 11 月 28 日起,Heroku 不再提供免费层,而 Streamlit Community Cloud 和 Hugging Face Spaces 都提供免费层。如果你想要使用这些功能,就必须为它们付费!

这就涵盖了如何通过 Heroku 部署 Streamlit!如你所见,Streamlit Community Cloud 开箱即用,处理了大部分这些困难,因此,我建议在可能的情况下尽量使用 Streamlit Community Cloud。然而,本节内容应该让你意识到,当我们使用 Hugging Face Spaces 和 Heroku 时,面前有多种选项和配置控制,这在未来可能会派上用场。

总结

到目前为止,这一章是最具技术性的,恭喜你成功完成!部署应用程序通常是非常困难且耗时的,需要软件工程和 DevOps 技能,还通常需要掌握版本控制软件(如 Git)和 UNIX 风格的命令及系统。这也是为什么 Streamlit Community Cloud 是如此关键的创新之一,而在本章中,我们学习了通过租用自己的虚拟机并在 Hugging Face Spaces 和 Heroku 上部署应用来推动 Streamlit 部署的极限。我们还学会了在开始之前确定正确的部署策略,这将节省数小时或数天的工作(没有什么比完成应用部署后发现需要使用其他平台更糟糕的事情了!)。

接下来,我们将学习如何在我们的 Streamlit 应用中从数据库查询数据。

在 Discord 上了解更多

加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问,并了解新版本的发布——请扫描下方二维码:

packt.link/sl

第九章:连接到数据库

在前几章中,我们完全专注于存储在单个文件中的数据,但大多数真实世界的基于工作的应用程序集中在存储在数据库中的数据上。公司倾向于将其数据存储在云中,因此能够对这些数据进行分析是一项关键技能。在本章中,我们将探讨如何访问和使用存储在流行数据库(如 Snowflake 和 BigQuery)中的数据。对于每个数据库,我们将连接到数据库,编写 SQL 查询,然后创建一个示例应用程序。

无论您是希望对大型数据集执行即席分析,还是构建数据驱动的应用程序,高效地从数据库中检索和操作数据的能力都是必不可少的。通过本章结束时,您将对如何使用 Streamlit 连接到数据库并与之交互有深入理解,从而使您能够自信地提取见解并做出数据驱动的决策。

在本章中,我们将涵盖以下主题:

  • 使用 Streamlit 连接到 Snowflake

  • 使用 Streamlit 连接到 BigQuery

  • 添加用户输入到查询中

  • 组织查询

技术要求

以下是本章所需的软件和硬件安装列表:

  • Snowflake 账户:要获得 Snowflake 账户,请访问(signup.snowflake.com/),并开始免费试用。

  • Snowflake Python 连接器:Snowflake Python 连接器允许您从 Python 运行查询。如果您已安装了本书的要求,则已经拥有该库。如果没有,请运行pip install snowflake-connector-python开始安装。

  • BigQuery 账户:要获取 BigQuery 账户,请访问(console.cloud.google.com/bigquery),并开始免费试用。

  • BigQuery Python 连接器:BigQuery 也有一个 Python 连接器,其工作方式与 Snowflake Python 连接器相同!它也包含在您在本书开始时安装的要求文件中,但如果您尚未安装该库,您也可以运行pip install google-cloud-bigquery来安装。

现在我们已经准备好了一切,让我们开始吧!

使用 Streamlit 连接到 Snowflake

要在 Streamlit 中连接到任何数据库,我们主要需要考虑如何在 Python 中连接到该服务,然后添加一些特定于 Streamlit 的功能(如缓存!)来提高用户体验。幸运的是,Snowflake 花费了大量时间使从 Python 连接到 Snowflake 变得非常简单;您只需指定您的账户信息,Snowflake Python 连接器会完成其余操作。

在本章中,我们将创建并在一个名为database_examples的新文件夹中工作,并添加一个名为streamlit_app.py的文件,以及一个 Streamlit secrets文件来开始:

mkdir database_examples
cd database_examples
touch streamlit_app.py
mkdir .streamlit
touch .streamlit/secrets.toml 

secrets.toml 文件中,我们需要添加用户名、密码、账户和仓库信息。我们的用户名和密码是在注册 Snowflake 账户时添加的,仓库是 Snowflake 用来执行查询的虚拟计算机(默认的仓库叫做 COMPUTE_WH),而账户标识符是最后一个需要填写的!要查找你的账户标识符,最简单的办法是通过这个链接查看最新信息(docs.snowflake.com/en/user-guide/admin-account-identifier)。现在我们有了所有需要的信息,可以将它们添加到我们的 secrets 文件中!我们的文件应该如下所示,内容是你的信息而不是我的。

现在我们从上面的 SQL 查询结果中获取了账户信息,我们有了所有需要的信息,可以将它们添加到 secrets 文件中!我们的文件应该如下所示,内容是你的信息而不是我的:

[snowflake]
user = "streamlitfordatascience"
password = "my_password"
account = "gfa95012"
warehouse = "COMPUTE_WH" 

现在我们可以开始创建我们的 Streamlit 应用了。我们的第一步是创建 Snowflake 连接,运行一个基本的 SQL 查询,然后将结果输出到 Streamlit 应用中:

import snowflake.connector
import streamlit as st
session = snowflake.connector.connect(
    **st.secrets["snowflake"], client_session_keep_alive=True
)

sql_query = "select 1"
st.write("Snowflake Query Result")
df = session.cursor().execute(sql_query).fetch_pandas_all()
st.write(df) 

这段代码做了几件事:首先,它使用 Snowflake Python 连接器,通过 secrets 文件中的秘密信息编程连接到我们的 Snowflake 账户,然后它运行 SQL 查询,仅返回 1,最后它将在我们的应用中显示该输出。

现在我们的应用应该如下所示:

图 9.1:Snowflake 查询结果

每次运行这个应用时,它都会重新连接到 Snowflake。这不是一个理想的用户体验,因为它会使应用变得更慢。过去我们会通过将其包装在函数中并使用 st.cache_data 来缓存,但在这里这样做不起作用,因为连接不是数据。相反,我们应该使用 st.cache_resource 来缓存它,类似于我们在本书前面处理 HuggingFace 模型的方式。我们的会话初始化代码现在应该像这样:

@st.cache_resource
def initialize_snowflake_connection():
    session = snowflake.connector.connect(
        **st.secrets["snowflake"], client_session_keep_alive=True
    )
    return session

session = initialize_snowflake_connection()
sql_query = "select 1" 
personal project or for your company! A sample query for us to use looks like this:
sql_query = """
    SELECT
    l_returnflag,
    sum(l_quantity) as sum_qty,
    sum(l_extendedprice) as sum_base_price
    FROM
    snowflake_sample_data.tpch_sf1.lineitem
    WHERE
    l_shipdate <= dateadd(day, -90, to_date('1998-12-01'))
    GROUP BY 1
""" 

现在,我们的应用应该是这样的:

图 9.2:SQL GROUPBY

现在,我们还想缓存数据结果,以加快应用速度并降低成本。这是我们之前做过的事情;我们可以将查询调用包装在一个函数中,并使用 st.cache_data 来缓存它!它应该像这样:

@st.cache_data
def run_query(session, sql_query):
    df = session.cursor().execute(sql_query).fetch_pandas_all()
    return df
df = run_query(session, sql_query) 

我们为这个应用的最后一步是稍微打扮一下外观。现在它比较基础,因此我们可以添加一个图表、一个标题,并且让用户选择用于作图的列。另外,我们还会确保结果是 float 类型(大致是非整数的数字),这是一个好的通用实践:

df = run_query(session, sql_query)

st.title("Snowflake TPC-H Explorer")
col_to_graph = st.selectbox(
    "Select a column to graph", ["Order Quantity", "Base Price"]
)
df["SUM_QTY"] = df["SUM_QTY"].astype(float)
df["SUM_BASE_PRICE"] = df["SUM_BASE_PRICE"].astype(float)

if col_to_graph == "Order Quantity":
    st.bar_chart(data=df, 
                 x="L_RETURNFLAG", 
                 y="SUM_QTY")
else:
    st.bar_chart(data=df,
                 x="L_RETURNFLAG", 
                 y="SUM_BASE_PRICE") 

现在我们的应用程序是互动式的,并且显示了一个很棒的图表!它将如下所示:

图 9.3:TCP-H 最终应用

以上就是我们关于使用 Streamlit 连接 Snowflake 的章节内容!目前,Snowflake 有一些预览版产品可以让你直接在 Snowflake 内创建 Streamlit 应用。如果你想使用这些产品,可以联系你的 Snowflake 管理员,他们应该能帮你获取访问权限!

现在,开始使用 BigQuery!

使用 Streamlit 连接 BigQuery

将 BigQuery 连接到 Streamlit 应用的第一步是获取从 BigQuery 所需的认证信息。Google 提供了一份非常棒的快速入门文档,你应该按照文档操作,文档链接如下:cloud.google.com/bigquery/docs/quickstarts/quickstart-client-libraries。这个链接将帮助你注册免费账户,并创建一个项目。创建项目后,你需要创建一个服务账号(console.cloud.google.com/apis/credentials),并将凭证下载为 JSON 文件。一旦你获得了这个文件,你就拥有了所有需要的数据,可以回到本章继续操作。

在这一部分中,我们将在 database_example 文件夹内创建一个新的文件,命名为 bigquery_app.py,并且我们将向已创建的 secrets.toml 文件中添加一个新部分。首先,我们可以编辑 secrets.toml 文件,最后,你可以通过这个链接创建和查看你的服务账号凭证(console.cloud.google.com/apis/credentials)。请将你的服务账号凭证粘贴到 secrets.toml 文件的新部分,格式如下:

[bigquery_service_account]
type = "service_account"
project_id = "xxx"
private_key_id = "xxx"
private_key = "xxx"
client_email = "xxx"
client_id = "xxx"
auth_uri = "https://accounts.google.com/o/oauth2/auth"
token_uri = "https://oauth2.googleapis.com/token"
auth_provider_x509_cert_url = "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url = "xxx" 

现在我们需要创建并打开一个新的应用文件,命名为 bigquery_app.py,并从那里连接到 BigQuery:

import streamlit as st
from google.oauth2 import service_account 
from google.cloud import bigquery 

credentials = service_account.Credentials.from_service_account_info( 
    st.secrets["bigquery_service_account"] 
) 
client = bigquery.Client(credentials=credentials) 

现在,当我们想要运行查询时,可以使用我们通过认证创建的客户端变量来执行它!为了展示一个例子,Google 慷慨地提供了一个免费数据集,记录了人们下载 Python 库的频率。我们可以编写一个查询,计算我们应用中过去 5 天的 Streamlit 下载量,查询代码如下:

import streamlit as st
from google.cloud import bigquery
from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_info(
    st.secrets["bigquery_service_account"]
)
client = bigquery.Client(credentials=credentials)

st.title("BigQuery App")
my_first_query = """
    SELECT
    CAST(file_downloads.timestamp  AS DATE) AS file_downloads_timestamp_date,
    file_downloads.file.project AS file_downloads_file__project,
    COUNT(*) AS file_downloads_count
    FROM 'bigquery-public-data.pypi.file_downloads'
    	    AS file_downloads
    WHERE (file_downloads.file.project = 'streamlit')
AND (file_downloads.timestamp >= timestamp_add(current_timestamp(), INTERVAL -(5) DAY))
    GROUP BY 1,2
    """

downloads_df = client.query(my_first_query).to_dataframe()
st.write(downloads_df) 

当我们运行这个应用时,得到的结果如下:

图 9.4:BigQuery 查询结果

在这种情况下,我大约在 3 月 29 日的太平洋标准时间 8 点运行了查询,这意味着世界某些地方已经进入了 3 月 30 日,并开始下载库。这就是 30 日下载量大幅下降的原因!接下来,作为改进,我们可以通过 st.line_chart() 来绘制下载量随时间变化的图表,就像我们在本书中做过的几次一样:

图 9.5:BigQuery 图表

如你所见,运行这些查询需要一些时间。这是因为我们既没有缓存结果,也没有缓存连接。让我们向应用中添加一些功能来实现这个目的:

from google.oauth2 import service_account 
@st.cache_resource 
def get_bigquery_client(): 
credentials = service_account.Credentials.from_service_account_info(st.secrets["bigquery_service_account"])
return bigquery.Client(credentials=credentials) 
client = get_bigquery_client() 
@st.cache_data  
def get_dataframe_from_sql(query):  
df = client.query(query).to_dataframe() 
    return df 

我们应用的底部将使用我们刚刚创建的 get_dataframe_from_sql

Downloads_df = get_dataframe_from_sql(my_first_query)
st.line_chart(downloads_df,
x="file_downloads_timestamp_date",
y="file_downloads_count) 

就是这样!现在你知道如何从 BigQuery 获取数据并缓存结果以及认证过程了。随着你开始在工作环境中使用 Streamlit,这将非常有用,因为数据很少完全存储在.csv文件中,而是存在于云数据库中。接下来的部分将介绍更多的策略,以便在 Streamlit 中处理查询和数据库。

向查询添加用户输入

使用 Streamlit 的一个主要好处是使用户交互变得极为简单,我们希望在编写连接数据库的应用程序时启用这一功能。到目前为止,我们编写了将查询转换为 DataFrame 的代码,并且在这些 DataFrame 上,我们可以添加典型的 Streamlit 小部件来进一步过滤、分组和绘制数据。然而,这种方法仅适用于相对较小的数据集,通常我们必须更改底层查询,以便在应用程序中获得更好的性能。让我们通过一个例子来证明这一点。

让我们回到bigquery_app.py中的 Streamlit 应用程序。我们为应用程序设置了一个相对任意的回溯期,在查询中仅提取了过去 5 天的数据。如果我们想让用户定义回溯期怎么办?如果我们坚持不改变查询,而是在查询执行后进行过滤,那么我们就不得不从bigquery-public-data.pypi.file_downloads表中提取所有数据,这将非常慢并且花费大量的金钱。相反,我们可以通过以下方法添加一个滑块来更改底层查询:

st.title("BigQuery App")
days_lookback = st.slider('How many days of data do you want to see?', min_value=1, max_value=30, value=5)
my_first_query = f"""
    SELECT
    CAST(file_downloads.timestamp  AS DATE) AS file_downloads_timestamp_date,
    file_downloads.file.project AS file_downloads_file__project,
    COUNT(*) AS file_downloads_count
    FROM 'bigquery-public-data.pypi.file_downloads'
    AS file_downloads
    WHERE (file_downloads.file.project = 'streamlit')
        AND (file_downloads.timestamp >=
        timestamp_add(current_timestamp(), 
INTERVAL -({days_lookback}) DAY))
    GROUP BY 1,2
    """ 

在这种情况下,我们添加了一个滑块,设定了适当的最小值和最大值,并将滑块的结果输入到查询中。每当滑块移动时,查询都会重新执行,但这比提取整个数据集要高效得多。现在我们的应用程序应该是这样的:

图 9.6:动态 SQL

我们也可以通过同样的方法,轻松地在 Snowflake 查询中添加动态 SQL,但这展示了在 BigQuery 中使用它的一个很好的例子。

这里有一点警告,绝对不要将文本输入用作数据库查询的输入。如果你允许用户输入自由格式的文本并将其放入查询中,你实际上就赋予了用户与你一样的数据库访问权限。你可以使用 Streamlit 的其他小部件,而不必担心同样的后果,因为像st.slider这样的部件的输出是有保证的,它始终返回数字而非恶意查询。

现在我们已经学习了如何将用户输入添加到查询中,我们可以进入最后一部分,组织 Streamlit 应用程序中的查询。

组织查询

随着你创建越来越多依赖数据库查询的 Streamlit 应用,你的应用往往会变得非常长,并且会包含作为字符串存储的长查询。这会使应用变得更难阅读,并且在与他人协作时也会更加难以理解。对于 Streamlit 数据团队来说,常常会有半打 30 行的查询来支撑一个我们创建的 Streamlit 应用!有两种策略可以改善这种设置:

  • 使用像 dbt 这样的工具创建下游表格

  • 将查询存储在独立的文件中

我们将简要地只介绍其中的第一个,创建下游表格。如我们在上一个例子中所注意到的,每当用户更改滑块时,查询都会在应用中重新运行。这可能会变得相当低效!我们可以使用像 dbt 这样的工具,它是一个非常流行的工具,可以让我们安排 SQL 查询,来创建一个较小的表格,这个表格已经把较大的表格筛选到只包含 bigquery-public-data.pypi.file_downloads 中最后 30 天的 Streamlit 数据。这样,我们的查询行数会减少,也不会让应用变得拥挤,同时也更加经济高效!我们在 Streamlit 数据团队中非常常用这个技巧,我们经常在 dbt 中创建较小的下游表格来支持我们的 Streamlit 应用。

第二个选项是将我们的查询存储在完全独立的文件中,然后将它们导入到我们的应用中。为此,在与我们的 Streamlit 应用相同的目录中创建一个名为 queries.py 的新文件。在这个文件中,我们需要创建一个函数,返回我们已经创建的 pypi 数据查询,函数的输入是我们应用所需的日期筛选。它应该是这样的:

def get_streamlit_pypi_data(day_filter):
    streamlit_pypy_query = f"""
    SELECT
    CAST(file_downloads.timestamp  AS DATE) 
        AS file_downloads_timestamp_date,
    file_downloads.file.project AS
   file_downloads_file__project,
    COUNT(*) AS file_downloads_count
    FROM 'bigquery-public-data.pypi.file_downloads'
    AS file_downloads
    WHERE (file_downloads.file.project = 'streamlit')
        AND (file_downloads.timestamp >=
        timestamp_add(current_timestamp(), 
        INTERVAL -({day_filter}) DAY))
    GROUP BY 1,2
    """
    return streamlit_pypy_query 

现在,在我们的 Streamlit 应用文件中,我们可以从文件中导入这个函数,并像这样使用它(为了简便,我省略了两个缓存函数):

import streamlit as st
from google.cloud import bigquery
from google.oauth2 import service_account
from queries import get_streamlit_pypi_data
...
st.title("BigQuery App")
days_lookback = st.slider('How many days of data do you want to see?', min_value=1, max_value=30, value=5)
pypi_query = get_streamlit_pypi_data(days_lookback)

downloads_df = get_dataframe_from_sql(pypi_query)
st.line_chart(downloads_df, x="file_downloads_timestamp_date", y="file_downloads_count") 

完美!现在我们的应用变得更小了,Streamlit 部分与查询部分在应用中逻辑分离。我们在 Streamlit 数据团队中始终使用这样的策略,并且我们向开发生产环境中 Streamlit 应用的人推荐这样的策略。

总结

本章结束了第九章连接数据库。在本章中,我们学习了很多内容,从在 Streamlit 中连接 Snowflake 和 BigQuery 数据,到如何缓存我们的查询和数据库连接,帮助我们节省成本并改善用户体验。在下一章中,我们将重点讨论如何在 Streamlit 中优化工作应用程序。

在 Discord 上了解更多

要加入这本书的 Discord 社区——你可以在这里分享反馈、向作者提问,并了解新版本的发布——请扫描下面的二维码:

packt.link/sl

第十章:使用 Streamlit 改进求职申请

到本书这一章时,您应该已经是一个经验丰富的 Streamlit 用户。您已经对一切有了充分的掌握——从 Streamlit 设计到部署,再到数据可视化,以及其中的所有内容。本章重点关注应用,它将向您展示一些 Streamlit 应用的优秀用例,激发您创造自己的应用!我们将从演示如何使用 Streamlit 进行技能证明数据项目开始。然后,我们将讨论如何在求职申请的自述部分使用 Streamlit。

在本章中,我们将涵盖以下主题:

  • 使用 Streamlit 进行技能证明数据项目

  • 在 Streamlit 中改进求职申请

技术要求

以下是本章所需的软件和硬件安装列表:

  • streamlit-lottie:我们已经在组件章节中安装了这个库,但如果您还没有安装,现在是个好时机!要下载这个库,请在终端中运行以下代码:

    pip install streamlit-lottie 
    

    有趣的是,streamlit-lottie 使用了 lottie 开源库,允许我们将 Web 原生动画(例如 GIF)添加到我们的 Streamlit 应用程序中。坦率地说,这是一个极好的库,您可以用它来美化 Streamlit 应用程序,它是由富有创造力的 Streamlit 应用程序开发者 Fanilo Andrianasolo 创建的。

  • 求职申请示例文件夹:本书的中央仓库可以在 github.com/tylerjrichards/Streamlit-for-Data-Science 找到。在这个仓库中,job_application_example 文件夹将包含您在本章第二部分关于求职申请所需的一些文件。如果您还没有下载这个主仓库,请在终端中运行以下代码以克隆它:

    https://github.com/tylerjrichards/Streamlit-for-Data-Science 
    

现在我们已经完成了所有的设置,开始吧!

使用 Streamlit 进行技能证明数据项目

向他人证明自己是一个熟练的数据科学家是出了名的困难。任何人都可以在简历上写上 Python 或机器学习,甚至可以在一个可能涉及一些机器学习的大学研究小组工作。但通常,招聘人员、你想合作的教授以及数据科学经理们依赖于简历中的一些替代品来衡量你的能力,例如是否毕业于“名校”,或者是否已经有了一个体面的数据科学实习或工作。

在 Streamlit 出现之前,并没有很多有效的方法可以快速且轻松地展示你的作品。如果你将一个 Python 文件或 Jupyter notebook 放在你的 GitHub 个人资料上,那么让别人理解这项工作是否令人印象深刻的时间就变得太长,风险太大。如果招聘者必须点击你 GitHub 个人资料中的正确仓库,然后再点击多个文件,直到他们找到一个没有注释的、无法读取的 Jupyter notebook 代码,你就已经失去了他们。如果招聘者在你的简历上看到“机器学习”,但需要点击五次才能看到你编写的任何机器学习产品或代码,你也已经失去了他们。大多数感兴趣的人员在简历上的停留时间非常短;例如,访问我个人作品集网站的访客(www.tylerjrichards.com)平均在该站点停留约 2 分钟后就会离开。如果这个人是招聘者,我需要确保他们能快速理解我是谁,以及为什么我可能是一个合适的候选人!

解决这个问题的一种方法是尝试创建并分享 Streamlit 应用,这些应用专门展示你最想展示的技能。例如,如果你在基础统计学方面有很多经验,你可以创建一个 Streamlit 应用,证明或阐明一个基础统计定理,例如中心极限定理——就像我们在本书中早些时候所做的那样。

如果你有自然语言处理方面的经验,你可以创建一个展示你所开发的文本生成神经网络的应用。这里的重点是尽量减少用户需要点击的次数,直到他们能够看到你在某一领域的能力。

我们已经创建的许多 Streamlit 应用程序确实达到了这个目的。让我们看几个例子。

机器学习 - Penguins 应用

第四章使用 Streamlit 进行机器学习与人工智能中,我们创建了一个随机森林模型,该模型在我们的 Palmer Penguins 数据集上进行训练,根据诸如体重、栖息岛屿和喙长等特征预测企鹅的物种。然后,我们保存了该模型,以便在我们的 Streamlit 应用中使用。

在开始创建 Streamlit 应用之前,我们需要(在第一次迭代中)运行以下代码,这将创建部署的模型:

import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import pickle
penguin_df = pd.read_csv('penguins.csv')
penguin_df.dropna(inplace=True)
output = penguin_df['species']
features = penguin_df[['island', 'bill_length_mm', 'bill_depth_mm',
                       'flipper_length_mm', 'body_mass_g', 'sex']]
features = pd.get_dummies(features)
output, uniques = pd.factorize(output)
x_train, x_test, y_train, y_test = train_test_split(
    features, output, test_size=.8)
rfc = RandomForestClassifier(random_state=15)
rfc.fit(x_train, y_train)
y_pred = rfc.predict(x_test)
score = accuracy_score(y_pred, y_test)
print('Our accuracy score for this model is {}'.format(score)) 

在这一部分,我们导入库,加载数据,并训练/评估我们的模型,同时打印出评估结果。然后,我们使用以下代码将模型结果保存为pickle文件:

rf_pickle = open('random_forest_penguin.pickle', 'wb')
pickle.dump(rfc, rf_pickle)
rf_pickle.close()
output_pickle = open('output_penguin.pickle', 'wb')
pickle.dump(uniques, output_pickle)
output_pickle.close() 

回想一下,在本章结束时,我们增加了一个新功能,以便如果用户上传自己的数据集,他们可以使用我们的模型训练脚本在他们的数据上完全训练一个模型(前提是数据格式相同;这带有一些先决条件)。

这个应用的最终形式展示了我们至少对数据清洗有所了解,知道如何对变量进行独热编码,如何评估模型在测试数据上的表现,最后,如何将我们训练好的模型部署到应用中。单单这一点,相较于在简历上仅仅写上“机器学习”,它看起来要好得多,并且能证明我们具备一些实际技能。如果没有这一技能的证明,招聘人员或招聘经理在查看我们的申请时,要么只能完全信任我们简历上的内容(而从多年阅读数百份简历的经验来看,这是一个很差的假设),要么只能依赖如大学学位这样的替代指标(这也是一个评估能力的糟糕替代标准)。

除此之外,当我们将这个应用部署到 Streamlit Community Cloud 并使用公共 GitHub 仓库(即像我们在第五章通过 Streamlit Community Cloud 部署 Streamlit 中所做的那样),我们将自动获得一个免费的功能,那就是 GitHub 仓库按钮。如以下截图所示,当我们将应用部署到 Streamlit Community Cloud 时,应用右上方会添加一个按钮,允许用户查看背后的源代码。如果你是该应用的所有者,你还会看到一个分享按钮,可以让你与他人分享应用!

图 10.1:查看应用源选项

通过这种方式,用户可以随时检查,确保没有恶意代码(例如,研究人员的企鹅数据是否被应用存储)被 Streamlit Community Cloud 部署。作为一个附加功能,用户还可以查看你为构建应用所写的代码,这提高了我们将 Streamlit 作为技能证明工具的能力。

可视化 – 美丽的树应用

第六章美化 Streamlit 应用中,我们曾经开发了一个 Streamlit 应用,它能够创建关于旧金山树木的美丽动态可视化,最终产生了以下这个应用:

图 9.2 – 映射一个网页应用

图 10.2:映射一个网页应用

在这个应用中,我们需要创建多个不同的可视化(即两个直方图和一张地图),这些可视化会根据右侧的用户输入动态更新。通过这样的应用,我们展示了数据处理能力,熟悉 pandasmatplotlibseaborn 库的能力,甚至证明了我们理解如何在 Python 中处理日期时间。让我们来看看专注于可视化的应用代码部分:

#define multiple columns, add two graphs
col1, col2 = st.columns(2)
with col1:
     st.write('Trees by Width')
     fig_1, ax_1 = plt.subplots()
     ax_1 = sns.histplot(trees_df['dbh'],
          color=graph_color)
     plt.xlabel('Tree Width')
     st.pyplot(fig_1)
with col2:
     st.write('Trees by Age')
     fig_2, ax_2 = plt.subplots()
     ax_2 = sns.histplot(trees_df['age'],
          color=graph_color)
     plt.xlabel('Age (Days)')
     st.pyplot(fig_2)
st.write('Trees by Location')
trees_df = trees_df.dropna(subset=['longitude', 'latitude'])
trees_df = trees_df.sample(n = 1000, replace=True)
st.map(trees_df) 

这段代码对于任何熟悉 Python 或其他脚本语言的人来说都相当容易阅读,而且它比简单地在简历上写“数据可视化”或“pandas”要好得多。

到目前为止,我希望你已经信服了。Streamlit 应用是向招聘人员、潜在的招聘经理或任何需要证明你技能的人展示你工作的绝佳方式。在接下来的部分,我们将更详细地介绍这个过程,并演示如何使用 Streamlit 增强你向心仪公司提交的申请。

在 Streamlit 中改进职位申请

数据科学和机器学习职位申请通常依赖于带回家的数据科学挑战来评估候选人。坦率地说,这是一个残酷且令人烦恼的过程,因为在申请人与雇主之间存在动态关系。例如,候选人可能需要花费 5 到 10 个小时来完成一个数据科学挑战,但雇主可能只需要 10 分钟就能评估完成情况。此外,个人的虚拟或电话面试可能需要雇主 30 到 45 分钟的时间,再加上额外的 15 分钟来写反馈,而候选人同样需要花费 30 到 45 分钟。因为获得 5 到 10 小时的工作为雇主提供了每分钟非常高的员工时间信号,雇主倾向于在职位申请中加入这些挑战。

你可以利用这个机会通过创建一个完全功能的应用来脱颖而出,而不是发送公司一个 Jupyter 笔记本、Word 文档或 PowerPoint 演示文稿。

问题

让我们通过一个虚构的例子来讲解一个正在申请美国主要航空公司职位的求职者。他们被给出了两个主要问题来解决——其中一个包含数据集:

  • 问题 1:机场距离

    第一个练习问道:“鉴于包含的机场和位置数据集(纬度和经度),编写一个函数,该函数接受一个机场代码作为输入,并返回从输入机场最近到最远的机场列表。

  • 问题 2:表示法

    第二个问题问道:“你会如何将一组搜索转换为表示一次旅行的数值向量?假设我们有数十万用户,我们希望以这种方式表示他们的所有旅行。理想情况下,我们希望这是一个通用表示,可以在多个不同的建模项目中使用,但我们显然关心的是找到相似的旅行。你具体会如何比较两次旅行,看它们有多相似?你认为前述数据中缺少什么信息会帮助改进你的表示方式?

既然我们已经有了所需的问题,我们就可以启动一个新的 Streamlit 应用。为此,我按照每一章到目前为止我们使用的相同流程操作。我们在我们的中央文件夹(streamlit_apps)中创建一个新的文件夹,命名为job_application_example

在这个文件夹中,我们可以创建一个 Python 文件,命名为job_streamlit.py,并在终端中使用以下命令:

touch job_streamlit.py 

回答问题 1

对您来说,理解如何解答当前问题(计算机场距离)并不至关重要,但创建 Streamlit 应用程序的整体框架非常重要。我们创建的 Streamlit 应用程序应该像一份极具动态性的文档,以独特的方式回答问题,取决于 Streamlit 能够创建一个无法轻易通过 Word 文档复制的应用程序。

首先,我们可以创建一个标题来介绍我们自己,并启动整个应用程序的格式。这里有一个改进,可以使用我们在第七章《探索 Streamlit 组件》中学习到的streamlit-lottie库,在应用程序的顶部添加一个可选动画,如以下代码所示:

import streamlit as st
from streamlit_lottie import st_lottie
import pandas as pd
import requests
def load_lottieurl(url: str):
    r = requests.get(url)
    if r.status_code != 200:
        return None
    return r.json()
lottie_airplane = load_lottieurl('https://assets4.lottiefiles.com/packages/lf20_jhu1lqdz.json')
st_lottie(lottie_airplane, speed=1, height=200, key="initial")
st.title('Major US Airline Job Application')
st.write('by Tyler Richards')
st.subheader('Question 1: Airport Distance') 

上述代码将创建一个包含美丽飞机动画的应用程序,如以下截图所示:

图 9.3 – 一架飞机 GIF

图 10.3:飞机动画

接下来,我们需要将问题复制并粘贴到子标题下方。Streamlit 提供了许多将文本插入应用程序的选项。我们还没有使用的一个选项是将文本包裹在三个撇号符号内,这告诉 Streamlit 使用 Markdown 语言来写入这些文本。对于大段的文本,这是非常有用的,比如下面的这个,它开始回答第一个问题:

"""
The first exercise asks us 'Given the table of airports and
locations (in latitude and longitude) below,
write a function that takes an airport code as input and
returns the airports listed from nearest to furthest from
the input airport.' There are three steps here:
1\. Load the data
2\. Implement a distance algorithm
3\. Apply the distance formula across all airports other than the input
4\. Return a sorted list of the airports' distances
""" 

如本章的技术要求部分所述,完成此应用程序需要两个文件。第一个是机场位置的数据集(名为airport_location.csv),第二个是展示哈弗辛距离的图片(即球面上两点之间的距离;该文件恰当地命名为haversine.png)。请将这些文件复制到与 Streamlit 应用程序 Python 文件相同的文件夹中。

现在,我们需要完成第一步:加载数据。我们既需要在 Streamlit 中完成此步骤,还需要将代码展示给用户。这与其他 Streamlit 应用程序不同,后者通常将代码隐藏在后台。然而,因为用户肯定希望看到我们的代码,毕竟他们会基于此进行评估,我们需要同时完成这两个任务。我们可以使用之前使用过的st.echo()函数,将代码块打印到我们的应用程序中。我们可以使用以下代码来实现:

airport_distance_df = pd.read_csv('airport_location.csv')
with st.echo():
     #load necessary data
     airport_distance_df = pd.read_csv('airport_location.csv') 

在这里,我想指出,我们在此代码的顶部添加了一个注释。这不是为了注解代码以便您理解,而是为了应用程序的读者。偶尔在代码块前后加上关于代码目的的注释是一种好习惯,这样读者就能理解我们所采用的方法。这在求职申请中尤为重要,但对于协作开发的 Streamlit 应用程序同样也是一种好做法。

下一步是解释 Haversine 公式,并在我们的 Streamlit 应用中展示该图片,这在以下代码块中已完成。你完全可以用叙述性的方式编写文本块。只需想象自己是招聘经理,想看到什么样的内容,然后尽量模拟出来:

"""
From some quick googling, I found that the Haversine distance is
a good approximation for distance. At least good enough to get the
distance between airports! Haversine distances can be off by up to .5%
because the Earth is not actually a sphere. It looks like the latitudes
and longitudes are in degrees, so I'll make sure to have a way to account
for that as well. The Haversine distance formula is labeled below,
followed by an implementation in Python
"""
st.image('haversine.png') 

现在,我们的应用应该类似于以下截图:

图 9.4 – 加载问题 1 的数据

图 10.4:加载问题 1 的数据

我们已经列出了要处理的项目,包括动画、Haversine 距离公式,以及读取数据的基础代码。此时,我们需要在 Python 中实现 Haversine 距离公式,并展示我们的实现:

with st.echo():
    from math import atan2, cos, radians, sin, sqrt
    def haversine_distance(long1, lat1, 
  long2, lat2,    degrees=False):
        # degrees vs radians
        if degrees == True:
            long1 = radians(long1)
            lat1 = radians(lat1)
            long2 = radians(long2)
            lat2 = radians(lat2)
        # implementing haversine
        a = (
            sin((lat2 - lat1) / 2) ** 2
            + cos(lat1) * cos(lat2) * sin((long2 - long1) / 2) ** 2
        )
        c = 2 * atan2(sqrt(a), sqrt(1 - a))
        distance = 6371 * c  # radius of earth in kilometers
        return distance 

我们代码的第一部分并没有创建函数,而是打印出我们将在 Streamlit 应用中创建的函数。这样做是为了让应用的读者能够查看我们编写的两段重要代码并与代码本身进行互动。如果我们只是创建了一个实现 Haversine 距离的函数,应用的读者就无法真正了解我们是如何解决当前问题的!以下代码块创建了这个函数:

#execute haversine function definition
from math import radians, sin, cos, atan2, sqrt
def haversine_distance(long1, lat1, 
                       long2, lat2, 
                       degrees=False):
    # degrees vs radians
    if degrees == True:
        long1 = radians(long1)
        lat1 = radians(lat1)
        long2 = radians(long2)
        lat2 = radians(lat2)

    # implementing haversine
    a = (
        sin((lat2 - lat1) / 2) ** 2
        + cos(lat1) * cos(lat2) * sin((long2 - long1) / 2) ** 2
    )
    c = 2 * atan2(sqrt(a), sqrt(1 - a))
    distance = 6371 * c  
# radius of earth in kilometers
    return distance 

我们已经完成了 Haversine 实现!每当我们想要计算两地之间的距离时,只需调用我们的公式,输入经度和纬度,即可得到以公里为单位的距离。这个应用很有用,但目前它与 Word 文档差别不大。我们的下一步是允许用户输入自己的坐标,检查 Haversine 距离是否正常工作。几乎没有人知道地球上两点之间的具体公里数,因此我提供了默认的坐标并验证了它们之间的实际距离:

"""
Now, we need to test out our function! The
distance between the default points is
18,986 kilometers, but feel free to try out
your own points of interest.
"""
long1 = st.number_input('Longitude 1', value = 2.55)
long2 = st.number_input('Longitude 2', value = 172.00)
lat1 = st.number_input('Latitude 1', value = 49.01)
lat2 = st.number_input('Latitude 2', value = -43.48)
test_distance = haversine_distance(long1 = long1, long2 = long2,
          lat1 = lat1, lat2 = lat2, degrees=True)
st.write('Your distance is: {} kilometers'.format(int(test_distance))) 

当我们输入默认值时,应用返回的距离大约偏差 2 公里,如下截图所示:

图 9.5 – 实现 Haversine 距离

图 10.5:实现 Haversine 距离

在这一点上,我们的下一步是通过使用已实现的 Haversine 距离计算器,结合给定的数据集。以下截图简要展示了这一过程:

图 10.6:已给出的机场距离

这个数据集包含了机场代码及其对应的 latlong 值。以下代码块提供了解决方案,结合了两种距离并省略了完整的 get_distance_list 函数,因为它只是我们已经实现过的函数的副本:

"""
We have the Haversine distance implemented, and we also have
proven to ourselves that it works reasonably well.
Our next step is to implement this in a function!
""" 
def get_distance_list(airport_dataframe,
                      airport_code):
    df = airport_dataframe.copy()
    row = df[df.loc[:, "Airport Code"] == airport_code]
    lat = row["Lat"]
    long = row["Long"]
    df = df[df["Airport Code"] != airport_code]
    df["Distance"] = df.apply(
        lambda x: haversine_distance(
            lat1=lat, long1=long, lat2=x.Lat, long2=x.Long, degrees=True
        ),
        axis=1,
    )
    df_to_return = df.sort_values(by="Distance").reset_index()
    return df_to_return
with st.echo():
     def get_distance_list(airport_dataframe, airport_code):
          *copy of function above with comments* 

最后,我们可以在我们获得的 DataFrame 上实现这个距离公式。我们可以允许用户从我们提供数据的机场代码选项中输入自己的代码,并返回正确的值:

"""
To use this function, select an airport from the airports provided in the dataframe
and this application will find the distance between each one, and
return a list of the airports ordered from closest to furthest.
"""
selected_airport = st.selectbox('Airport Code', airport_distance_df['Airport Code'])
distance_airports = get_distance_list(
     airport_dataframe=airport_distance_df, airport_code=selected_airport)
st.write('Your closest airports in order are {}'.format(list(distance_airports))) 

这是第一个问题的结尾。我们可以在最后添加一个可选部分,讲述如果我们有更多时间来解决这个问题,我们会如何改变我们的实现。如果你知道自己总共只打算在应用程序上花费几个小时,但又希望展示如果有更多时间,你知道如何改进它,这通常是个不错的主意。

下面的代码块展示了一个示例,应该紧接着前面的代码块放置:

"""
This all seems to work just fine! There are a few ways I would improve this if I was working on
this for a longer period of time.  
1\. I would implement the [Vincenty Distance](https://en.wikipedia.org/wiki/Vincenty%27s_formulae)
instead of the Haversine distance, which is much more accurate but cumbersome to implement.  
2\. I would vectorize this function and make it more efficient overall.
Because this dataset is only 7 rows long, it wasn't particularly important,
but if this was a crucial function that was run in production, we would want to vectorize it for speed.
""" 

另外,你也可以选择直接结束前面的代码说明,转而继续处理第二个问题。在这一点上,我们对问题 1的回答已经完成,并且应该类似于以下截图:

图 9.7 – 获取用户输入

图 10.7:获取用户输入

我们现在成功地回答了问题 1!我们总是可以手动检查这些机场之间的距离,得到相同的结果。但让我们继续处理应用程序中的第二个问题。

回答问题 2

第二个问题要简单得多,仅要求文本回答。这里的技巧是尽量加入一些列表或 Python 对象,以便将大段的文本分隔开。首先,我们将解释我们尝试回答这个问题的方法,然后展示它在 DataFrame 中的样子:

"""
For this transformation, there are a few things
that I would start with. First, I would have to define
what a unique trip actually was. In order to do this, I would
group by the origin, the destination, and the departure date
(for the departure date, often customers will change around
this departure date, so we should group by the date plus or
minus at least 1 buffer day to capture all the correct dates).
Additionally, we can see that often users search from an entire city,
and then shrink the results down to a specific airport. So we should also
consider a group of individual queries from cities and airports in the
same city, as the same search, and do the same for the destination.
From that point, we should add these important columns to each unique search.
""" 

现在,我们可以考虑一些在用户搜索航班时会用到的列。我们可以将它们放入一个示例 DataFrame,如下所示:

example_df = pd.DataFrame(columns=['userid', 'number_of_queries', 'round_trip', 'distance', 'number_unique_destinations',
                     'number_unique_origins', 'datetime_first_searched','average_length_of_stay',
                     'length_of_search'])
example_row = {'userid':98593, 'number_of_queries':5, 'round_trip':1,
                   'distance':893, 'number_unique_destinations':5,
                     'number_unique_origins':1, 'datetime_first_searched':'2015-01-09',
                   'average_length_of_stay':5, 'length_of_search':4}
st.write(example_df.append(example_row, ignore_index=True)) 

对于剩下的问题,我们可以加入一些关于如何使用不同方法找到两点之间距离的知识,然后就可以结束了:

"""
To answer the second part of the question, we should take the Euclidian distance
on two normalized vectors. There are two solid options for comparing two
entirely numeric rows, the euclidian distance (which is just the straight line
difference between two values), and the Manhattan distance (think of this as the
distance traveled if you had to use city blocks to travel diagonally across Manhattan).
Because we have normalized data, and the data is not high-dimensional or sparse, I
would recommend using the Euclidian distance to start off. This distance would tell
us how similar two trips were.
""" 

第二个问题的答案应类似于以下截图:

图 9.8 – 回答问题 2

图 10.8:回答问题 2

如你所见,这个示例展示了如何借助 Streamlit 库处理家庭作业数据任务,以便制作更具吸引力的应用程序。这项工作的最终步骤是部署这个 Streamlit 应用,并将链接分享给招聘人员。我强烈建议你将它部署到 Heroku,以确保其他人无法查看公司提供的问题或数据。你还可以采取进一步的预防措施,比如在应用程序的开头放置一个文本框,作为密码保护(尽管显然这不是一个好的密码保护措施),如下所示的代码块:

password_attempt = st.text_input('Please Enter The Password')
if password_attempt != 'example_password':
     st.write('Incorrect Password!')
     st.stop() 

现在,除非用户在文本框中输入 example_password,否则整个应用程序不会运行。这显然不安全,但对于相对不重要(至少在保密方面)的应用程序,比如家庭作业类应用程序,它是有用的:

图 9.9 – 输入密码

图 10.9:输入密码

如你所见,只有输入正确的密码,应用程序才能加载。否则,用户将看到一个空白页面。

总结

本章是我们迄今为止创作的最侧重于实际应用的一章。我们主要关注求职申请以及数据科学和机器学习面试中的申请流程。此外,我们还学习了如何为我们的申请设置密码保护,如何创建能够向招聘人员和数据科学招聘经理证明我们是熟练的数据科学家的申请,以及如何通过创建 Streamlit 应用程序在数据科学的家庭作业面试中脱颖而出。下一章将重点介绍 Streamlit 作为一个玩具,你将学习如何为社区创建面向公众的 Streamlit 项目。

在 Discord 上了解更多

若要加入本书的 Discord 社区——在这里你可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码:

packt.link/sl

第十一章:数据项目——在 Streamlit 中制作原型项目

在上一章中,我们讨论了如何创建特定于工作申请的 Streamlit 应用程序。另一个有趣的 Streamlit 应用是尝试新的、有趣的数据科学想法,并为他人创建互动应用。一些示例包括将新的机器学习模型应用到现有数据集、分析用户上传的数据,或创建一个基于私有数据集的互动分析。制作这样的项目有很多原因,比如个人教育或社区贡献。

就个人教育而言,通常学习新主题的最佳方式是通过将其应用于你周围的世界或你熟悉的数据集来观察它是如何实际工作的。例如,如果你尝试了解主成分分析是如何工作的,你总是可以通过教科书学习,或者观看别人将其应用到数据集上。然而,我发现,当我自己亲自实践时,我对一个主题的理解会迅速提高。Streamlit 非常适合这种方式。它允许你在一个响应式、充满乐趣的环境中尝试新想法,并且可以轻松地与他人分享。学习数据科学是可以协作的,这也引出了我在 Streamlit 中创建数据项目的下一个原因。

就社区贡献而言,Streamlit 的一个最佳特点——坦白说,数据科学的一个最佳特点——是围绕我们日常使用的工具和玩具形成的日益壮大的社区。通过与他人共同学习,并在 Twitter(twitter.com/tylerjrichards)、LinkedIn 以及 Streamlit 论坛(discuss.streamlit.io/)分享 Streamlit 应用程序,我们可以摆脱大多数学校和大学教授的零和博弈的经验(在这种情况下,如果你的同学得了好成绩,通常会相对伤害到你),转而迎接一种正和博弈的体验(在这种情况下,你能直接从他人学到的经验中受益)。

以之前的示例为例,如果你创建了一个帮助你理解主成分分析背后统计学的应用,分享给他人可能也会让他们学到一些东西。

在本章中,我们将从头到尾完整地进行一个数据项目,开始于一个想法,最终以最终产品结束。具体来说,我们将涵盖以下主题:

  • 数据科学创意

  • 数据收集与清洗

  • 创建最小可行产品MVP

  • 迭代改进

  • 托管与推广

技术要求

在本节中,我们将使用Goodreads.com,这是一个由亚马逊拥有的流行网站,用于跟踪用户的阅读习惯,从开始和结束的时间到他们下一本想读的书。建议你首先访问www.goodreads.com/,注册一个账户,并稍作探索(也许你可以添加自己的书单!)。

数据科学创意

经常,想出一个新的数据科学项目的创意是最令人生畏的部分。你可能会有许多疑虑。假如我开始一个没人喜欢的项目怎么办?假如我的数据根本不好用怎么办?假如我想不出任何点子怎么办?好消息是,如果你创建的是你真正关心且愿意使用的项目,那么最糟的情况就是你的观众只有一个!如果你把项目发给我(tylerjrichards@gmail.com),我保证会阅读。所以最少的观众人数是两个。

我自己创建或观察到的一些示例包括:

尽管只有这两个数据项目使用了 Streamlit,因为其余的项目是在该库发布之前完成的,但所有这些项目都可以通过将它们部署到 Streamlit 上来改进,而不仅仅是上传到 Jupyter 笔记本(列表中的第一个项目)或 Word 文档/HTML 文件(第二个和第三个项目)。

有许多不同的方法可以帮助你想出自己的数据项目创意,但最流行的方法通常可以归为三类:

  • 寻找只有你能够收集到的数据(例如,你朋友的乒乓球比赛)

  • 寻找你关心的数据(例如,Spotify 的阅读数据)

  • 想到一个你希望存在的分析/应用程序来解决你的问题,并付诸实践(例如,宿舍 Wi-Fi 分析或在纽约市找到最佳披萨)

你可以尝试这些方法,或从你已有的其他想法开始。最好的方法是最适合你的方法!在本章中,我们将深入演示并重建 Goodreads Streamlit 应用,作为数据项目的示例。你可以通过 goodreads.streamlit.app/ 再次访问该应用。

这个应用程序旨在抓取用户的 Goodreads 历史记录,并生成一组图表,向用户展示自开始使用 Goodreads 以来的阅读习惯。生成的图表应与以下截图相似:

图 11.1:Goodreads 图表示例

我是通过对我的书籍历史进行个人分析得出了这个想法,然后我想,其他人可能也会对这种分析感兴趣!通过这个项目,我意识到我想阅读更老的书籍(或者说有更长“书龄”的书籍)。其实没有比这个更好的理由了,而且,往往最有趣的项目就是这样开始的。首先,我们将收集并清洗存在于 Goodreads 上的用户数据。

数据收集与清洗

获取 Goodreads 数据有两种方式:通过 应用程序编程接口 (API),该接口允许开发者以编程方式访问书籍数据,和通过其手动导出功能。遗憾的是,Goodreads 即将弃用其 API,并且从 2020 年 12 月起,不再向新开发者提供访问权限。

原版的 Goodreads 应用使用了 API,但我们的版本将依赖于 Goodreads 网站提供的手动导出功能来获取数据。要获取你的数据,请前往 www.goodreads.com/review/import 并下载你的数据。如果你没有 Goodreads 账户,可以随意使用我的个人数据,数据可以在 github.com/tylerjrichards/goodreads_book_demo 找到。我已经将我的 Goodreads 数据保存为一个名为 goodreads_history.csv 的文件,放在一个新文件夹 streamlit_goodreads_book 中。要创建你自己的文件夹并进行相应设置,请在终端中运行以下命令:

mkdir streamlit_goodreads_book
cd streamlit_goodreads_book
touch goodreads_app.py 

现在我们已经准备好开始了。我们实际上不知道这些数据是什么样子,也不知道数据集中包含了什么,所以我们的第一步是做以下几件事:

  1. 在应用程序顶部添加标题和说明。

  2. 允许用户上传自己的数据,或者如果他们没有自己的数据,则使用我们的默认数据。

  3. 将数据的前几行写入应用程序,以便我们查看。

以下代码块完成了所有这些功能。你可以随意更改文本,使应用显示你的名字,还可以添加人们可以查看的个人资料链接!在撰写本文时,大约 10% 的访问流量来自我制作的 Streamlit 应用:

import streamlit as st
import pandas as pd
st.title('Analyzing Your Goodreads Reading Habits')
st.subheader('A Web App by [Tyler Richards](http://www.tylerjrichards.com)')
"""
Hey there! Welcome to Tyler's Goodreads Analysis App. This app analyzes (and never stores!)
the books you've read using the popular service Goodreads, including looking at the distribution
of the age and length of books you've read. Give it a go by uploading your data below!
"""
goodreads_file = st.file_uploader('Please Import Your Goodreads Data')
if goodreads_file is None:
     books_df = pd.read_csv('goodreads_history.csv')
     st.write("Analyzing Tyler's Goodreads history")
else:
     books_df = pd.read_csv(goodreads_file)
     st.write('Analyzing your Goodreads history')
st.write(books_df.head()) 

现在,当我们运行这个 Streamlit 应用时,应该得到一个类似于以下截图的界面:

图 11.2:前五行

如你所见,我们得到了一个数据集,每本书都是一个独立的行。此外,我们还获得了关于每本书的大量数据,包括书名和作者、书籍的平均评分、你给这本书的评分、页数,甚至是你是否读过这本书、是否计划阅读这本书,或是正在阅读这本书。数据看起来大部分是干净的,但也有一些奇怪的地方——例如,数据中既有出版年份,又有原出版年份;以及ISBN国际标准书号)以="1400067820"的格式出现,这真是有点奇怪。现在我们了解了手头的数据,接下来可以尝试为用户构建一些有趣的图表。

制作 MVP

观察我们的数据,我们可以从一个基本问题开始:我可以用这些数据回答哪些最有趣的问题?在查看数据并思考我想从我的 Goodreads 阅读历史中获得哪些信息后,以下是我想到的一些问题:

  • 我每年读多少本书?

  • 我开始读一本书后,需要多长时间才能完成它?

  • 我读过的书有多长?

  • 我读过的书有多老?

  • 我是如何给书评分的,与其他 Goodreads 用户相比如何?

我们可以提出这些问题,弄清楚如何修改数据以便更好地可视化它们,然后尝试通过打印出所有图表来创建我们的第一个产品。

我每年读多少本书?

关于每年读书数量的第一个问题,我们有阅读日期列,数据格式为yyyy/mm/dd。以下代码块将执行以下操作:

  1. 将我们的列转换为日期时间格式。

  2. 阅读日期列中提取年份。

  3. 按此列分组并计算每年书籍的数量。

  4. 使用 Plotly 绘制图形。

以下代码块执行此操作,从日期时间转换开始。这里需要特别注意的是,和所有事情一样,我并没有在第一次尝试时就做对。事实上,我花了一些时间才弄清楚如何管理和转换这些数据。当你自己做项目时,如果发现数据清理和转换需要很长时间,不要感到沮丧!这通常是最难的一步:

 goodreads_file = st.file_uploader('Please Import Your Goodreads Data')
if goodreads_file is None:
     books_df = pd.read_csv('goodreads_history.csv')
     st.write("Analyzing Tyler's Goodreads history")
else:
     books_df = pd.read_csv(goodreads_file)
     st.write('Analyzing your Goodreads history')
books_df['Year Finished'] = pd.to_datetime(books_df['Date Read']).dt.year
books_per_year = books_df.groupby('Year Finished')['Book Id'].count().reset_index()
books_per_year.columns = ['Year Finished', 'Count']
fig_year_finished = px.bar(books_per_year, x='Year Finished', y='Count', title='Books Finished per Year')
st.plotly_chart(fig_year_finished) 

上述代码块将创建以下图表:

图 11.3:完成年份的条形图

事实上,我们在这里做出了一个假设——也就是,我们假设阅读日期列中的年份代表我们阅读这本书的年份。但如果我们在 12 月中旬开始一本书,并在 1 月 2 日完成它呢?或者,如果我们在 2019 年开始读一本书,但只读了几页,然后在 2021 年重新拿起来继续读呢?我们知道这不会完美地估算每年读的书籍数量,但将其表示为每年完成的书籍数量会更好。

我需要多长时间才能读完一本已经开始的书?

我们下一个问题是关于一旦开始读书后,我们需要多长时间才能读完它。为了回答这个问题,我们需要找出两列的差异:读书日期列和添加日期列。再次提醒,这将是一个近似值,因为我们没有用户开始读书的日期,只知道他们将书添加到 Goodreads 上的日期。考虑到这一点,我们的下一步包括以下内容:

  1. 将这两列转换为日期时间格式。

  2. 找出两列之间的天数差异。

  3. 将这个差异绘制为直方图。

以下代码块首先进行转换,就像我们之前做的那样,然后继续进行我们的一系列任务:

books_df['days_to_finish'] = (pd.to_datetime(
            books_df['Date Read']) - pd.to_datetime(books_df['Date Added'])).dt.days
fig_days_finished = px.histogram(books_df, x='days_to_finish')
st.plotly_chart(fig_days_finished) 

上面的代码块可以添加到你当前 Streamlit 应用的底部,运行后应该会显示一个新的图表:

图 11.4:完成天数图

这张图表对我的数据帮助不大。看起来,在某个时候,我把以前读过的书添加到了 Goodreads 上,这些书在这个图表中显示了出来。我们还有一组未读完或在待读书架上的书,它们在这个数据集中显示为null值。我们可以做一些调整,例如过滤数据集,只包含那些天数为正的书,或者仅过滤出已读完的书,这段代码块就是这样做的:

books_df['days_to_finish'] = (pd.to_datetime(
            books_df['Date Read']) - pd.to_datetime(books_df['Date Added'])).dt.days
books_finished_filtered = books_df[(books_df['Exclusive Shelf'] == 'read') & (books_df['days_to_finish'] >= 0)]
fig_days_finished = px.histogram(books_finished_filtered,
x='days_to_finish', title='Time Between Date Added And Date Finished',
     labels={'days_to_finish':'days'})
st.plotly_chart(fig_days_finished) 

我们代码中的这个改动使得图表大大改善。它做了一些假设,但也提供了更准确的分析。完成的图表可以在以下截图中查看:

图 11.5:改进后的完成天数图

这样好多了!现在,让我们继续下一个问题。

我读过的书有多长?

这个问题的数据已经处于相当不错的状态。我们有一列叫做页面数,顾名思义,它记录了每本书的页数。我们只需要将这一列传递给另一个直方图,就能搞定:

fig_num_pages = px.histogram(books_df, x='Number of Pages', title='Book Length Histogram')
st.plotly_chart(fig_num_pages) 

这段代码将生成类似于以下屏幕截图的结果,显示按页面数衡量的书籍长度直方图:

图 11.6:页面数直方图

这对我来说有道理;很多书的页数在 300 到 400 页之间,还有一些巨型书籍有 1,000 页以上。现在,让我们继续看这些书的出版年限吧!

我读过的书有多老?

我们接下来的图表应该是直接的。我们怎么知道我们读的书有多旧?我们是倾向于选择最近出版的书籍,还是更喜欢阅读经典书籍?我们可以从两个列中获取这些信息:出版年份和原始出版年份。这个数据集的文档非常少,但我认为我们可以放心假设原始出版年份是我们需要的,而出版年份则显示的是出版商重新出版书籍的时间。

以下代码块通过打印出所有原始出版年份晚于出版年份的书籍来验证这一假设:

st.write('Assumption check')
st.write(len(books_df[books_df['Original Publication Year'] > books_df['Year Published']])) 

当我们运行此代码时,应用程序应该返回没有原始出版年份大于出版年份的书籍。现在我们已经验证了这一假设,我们可以执行以下操作:

  1. 按原始出版年份对书籍进行分组。

  2. 在柱状图上绘制这个数据。

以下代码块执行了两个步骤:

books_publication_year = books_df.groupby('Original Publication Year')['Book Id'].count().reset_index()
books_publication_year.columns = ['Year Published', 'Count']
fig_year_published = px.bar(books_publication_year, x='Year Published', y='Count', title='Book Age Plot')
st.plotly_chart(fig_year_published) 

当我们运行这个应用程序时,我们应该得到以下图表:

图 11.7:书籍年龄图

初看这个图表,它似乎不太有用,因为有很多书籍的写作时间距离现在非常久远(例如,柏拉图的著作是在公元前 375 年),导致整个图表难以阅读。然而,Plotly 默认是交互式的,它允许我们缩放到我们关心的历史时期。例如,以下截图展示了当我们缩放到 1850 年至今的时期时的情况,而我读过的大多数书籍正好位于这个时间段:

图 11.8:缩放出版年份

这是一个更好的图表!接下来有几个选项可以选择。我们可以从不太有用的图表开始,并告诉用户可以放大,也可以只筛选出较新的书籍数据集(这会违背图表的主要目的),或者我们可以为图表设置默认的缩放状态,并在底部提醒用户可以根据需要放大。我认为第三个选项是最好的。以下代码实现了这个选项:

Books_publication_year = books_df.groupby('Original Publication Year')['Book Id'].count().reset_index()
books_publication_year.columns = ['Year Published', 'Count']
st.write(books_df.sort_values(by='Original Publication Year').head())
fig_year_published = px.bar(books_publication_year, x='Year Published', y='Count', title='Book Age Plot')
fig_year_published.update_xaxes(range=[1850, 2021])
st.plotly_chart(fig_year_published)
st.write('This chart is zoomed into the period of 1850-2021, but is interactive so try zooming in/out on interesting periods!') 

当我们运行这段代码时,我们应该得到最终的图表:

图 11.9:带有帮助文本的默认缩放

四个问题已经完成——剩下一个!

我如何与其他 Goodreads 用户相比对书籍评分?

对于这个最后一个问题,我们实际上需要两个独立的图表。首先,我们需要绘制我们给书籍打的分。其次,我们需要绘制其他用户给我们也评分的书籍打的分。这并不是一个完美的分析,因为 Goodreads 只给我们显示了书籍的平均评分——我们没有读取分布情况。例如,如果我们读过《雪球》,沃伦·巴菲特的传记,并给它打了 3 星,而一半的 Goodreads 读者给它打了 1 星,另一半给了 5 星,那么我们打的分就和平均评分完全一样,但我们并没有和任何单个评分者的评分一致!不过,我们只能基于现有数据做我们能做的事。因此,我们可以这样做:

  1. 根据我们评分过的书籍(即我们已阅读的书籍)来过滤图书。

  2. 为我们的第一个图表创建一本书的平均评分的直方图。

  3. 为你自己的评分创建另一个直方图。

接下来的代码块正是做到了这一点:

books_rated = books_df[books_df['My Rating'] != 0]
fig_my_rating = px.histogram(books_rated, x='My Rating', title='User Rating')
st.plotly_chart(fig_my_rating)
fig_avg_rating = px.histogram(books_rated, x='Average Rating', title='Average Goodreads Rating')
st.plotly_chart(fig_avg_rating) 

正如你在下面的截图中看到的,带有用户评分分布的第一个图表看起来非常好。看起来我主要给书籍打了 4 星或 5 星,这总体来说是相对宽松的评分:

图 11.10:用户评分分布

当我们查看第二个图表时,看到的分布相当干净。然而,我们遇到了之前提到的问题——所有的评分平均值比用户评分的分布要集中得多:

图 11.11:Goodreads 平均评分

我们可以始终将两个图表的 x 轴范围设置为 1 到 5,但这并不能解决我们真正的问题。相反,我们可以保留这两个图表,但还要计算出我们平均评分是否高于或低于 Goodreads 的平均评分。以下代码块将计算此差异,并将其添加到 Goodreads 平均评分图表下方:

Fig_avg_rating = px.histogram(books_rated, x='Average Rating', title='Average Goodreads Rating')
st.plotly_chart(fig_avg_rating)
import numpy as np
avg_difference = np.round(np.mean(books_rated['My Rating'] – books_rated['Average Rating']), 2)
if avg_difference >= 0:
     sign = 'higher'
else:
     sign = 'lower'
st.write(f"You rate books {sign} than the average Goodreads user by {abs(avg_difference)}!") 

这个代码块计算了我们的平均值,并创建了一个动态字符串,显示 Goodreads 用户给书籍的评分高于或低于 Goodreads 的平均用户评分。以下是我的数据结果:

图 11.12:添加平均差异

这样更好,完成了我们的 MVP。我们的应用已经处于一个不错的状态,难度较大的操作和可视化步骤基本完成。然而,我们的应用看起来并不出色,只是一堆排成一行的图表。这可能适合 MVP,但我们需要添加一些样式来真正改善状态。这就引出了我们下一部分内容:对这个想法进行迭代,使其更好。

迭代改进

到目前为止,我们几乎完全处于生产模式中。迭代改进就是在我们已经完成的工作基础上进行编辑和组织,以使应用更加易用,坦率地说,也更美观。这里有一些我们可以努力改进的地方:

  • 通过动画美化

  • 使用列和宽度组织

  • 通过文本和额外统计数据构建叙事

让我们通过使用动画让应用变得更加美观吧!

通过动画进行美化

第七章探索 Streamlit 组件 中,我们探讨了各种 Streamlit 组件的使用;其中有一个组件叫做streamlit-lottie,它使我们能够在 Streamlit 应用中添加动画。我们可以通过以下代码,在当前 Streamlit 应用的顶部添加一个动画来改进我们的应用。如果你想了解更多关于 Streamlit 组件的信息,请回到第七章探索 Streamlit 组件

import streamlit as st
import pandas as pd
import plotly.express as px
import numpy as np
from streamlit_lottie import st_lottie
import requests
def load_lottieurl(url: str):
    r = requests.get(url)
    if r.status_code != 200:
        return None
    return r.json()
file_url = 'https://assets4.lottiefiles.com/temp/lf20_aKAfIn.json'
lottie_book = load_lottieurl(file_url)
st_lottie(lottie_book, speed=1, height=200, key="initial") 

这个 Lottie 文件是一本书翻页的动画,如下图所示。这些动画总是能为较长的 Streamlit 应用程序增添一丝生动感:

图 12.13 – Goodreads 动画

图 11.13:Goodreads 动画

现在我们添加了动画,可以继续改进应用的布局。

使用列和宽度进行组织

正如我们之前讨论的,我们的应用看起来并不太好,每个图表都是依次显示的。另一个改进是,我们可以让应用以宽格式而不是窄格式显示,然后将应用并排放置在每一列中。

首先,在应用程序顶部,我们需要第一个 Streamlit 调用来将 Streamlit 应用的配置设置为宽格式,而不是窄格式,代码块如下所示:

import requests
st.set_page_config(layout="wide")
def load_lottieurl(url: str):
    r = requests.get(url)
    if r.status_code != 200:
        return None
    return r.json() 

这将把我们的 Streamlit 应用设置为宽格式。到目前为止,我们在应用中为每个图表都命名了一个独特的名称(例如fig_year_finished),以便让接下来的步骤更加轻松。现在我们可以删除所有的st.plotly_chart()调用,并创建一个由两列三行组成的布局,在哪里可以放置我们的六个图表。以下代码创建了这些布局。我们首先为每个位置命名,然后用一个图表填充它们:

row1_col1, row1_col2 = st.columns(2)
row2_col1, row2_col2 = st.columns(2)
row3_col1, row3_col2 = st.columns(2)
with row1_col1:
     st.plotly_chart(fig_year_finished)
with row1_col2:
     st.plotly_chart(fig_days_finished)
with row2_col1:
     st.plotly_chart(fig_num_pages)
with row2_col2:
     st.plotly_chart(fig_year_published)
     st.write('This chart is zoomed into the period of 1850-2021, but is interactive so try zooming in/out on interesting periods!')
with row3_col1:
     st.plotly_chart(fig_my_rating)
with row3_col2:
     st.plotly_chart(fig_avg_rating)
     st.write(f"You rate books {sign} than the average Goodreads user by {abs(avg_difference)}!") 

这段代码将创建如下截图中的应用,为简洁起见,已经裁剪为前两个图表:

图 11.14:宽格式示例

这使我们的图表更加易于阅读,并且可以轻松地进行比较。我们特意根据评分将两个图表配对,其余的图表也都能很好地并排显示。我们的最后一步是添加一些文字,使整个应用程序更加易读。

通过文本和附加统计数据进行叙事构建

这些图表已经很有帮助,可以理解用户的阅读方式,但我们可以通过在每个图表下方以及应用的开头添加一些有用的统计数据和文字来提高应用的可读性。

在我们开始定义列的位置上方,我们可以添加一个初步的部分,展示我们读过的独特书籍数量、独特作者数量和最喜欢的作者,将这些基本统计数据集中展示。我们可以利用这些基本数据来启动应用,并告知用户每个图表都是交互式的:

if goodreads_file is None:
     st.subheader("Tyler's Analysis Results:")
else:
     st.subheader('Your Analysis Results:')
books_finished = books_df[books_df['Exclusive Shelf'] == 'read']
u_books = len(books_finished['Book Id'].unique())
u_authors = len(books_finished['Author'].unique())
mode_author = books_finished['Author'].mode()[0]
st.write(f'It looks like you have finished {u_books} books with a total of {u_authors} unique authors. Your most read author is {mode_author}!')
st.write(f'Your app results can be found below, we have analyzed everything from your book length distribution to how you rate books. Take a look around, all the graphs are interactive!')
row1_col1, row1_col2 = st.columns(2) 

现在我们需要在四个图表下方添加四个新的文本部分,这些图表目前没有任何注释文本。对于前面三个图表,以下代码将为每个图表添加一些统计数据和文本:

row1_col1, row1_col2 = st.columns(2)
row2_col1, row2_col2 = st.columns(2)
row3_col1, row3_col2 = st.columns(2)
with row1_col1:
     mode_year_finished = int(books_df['Year Finished'].mode()[0])
     st.plotly_chart(fig_year_finished)
     st.write(f'You finished the most books in {mode_year_finished}. Awesome job!')
with row1_col2:
     st.plotly_chart(fig_days_finished)
     mean_days_to_finish = int(books_finished_filtered['days_to_finish'].mean())
     st.write(f'It took you an average of {mean_days_to_finish} days between when the book was added to Goodreads and when you finished the book. This is not a perfect metric, as you may have added this book to a to-read list!')
with row2_col1:
     st.plotly_chart(fig_num_pages)
     avg_pages = int(books_df['Number of Pages'].mean())
     st.write(f'Your books are an average of {avg_pages} pages long, check out the distribution above!') 

这里有一个示例图表是关于书籍长度的直方图。前面的代码为图表下方添加了平均长度和一些文本,具体如下面的截图所示:

图 11.15:平均页数文本

对于最后一组图表,我们可以为那些没有上下文的图表添加文本:

with row2_col2:
     st.plotly_chart(fig_year_published)
     st.write('This chart is zoomed into the period of 1850-2021, but is interactive so try zooming in/out on interesting periods!')
with row3_col1:
     st.plotly_chart(fig_my_rating)
     avg_my_rating = round(books_rated['My Rating'].mean(), 2)
     st.write(f'You rate books an average of {avg_my_rating} stars on Goodreads.')
with row3_col2:
     st.plotly_chart(fig_avg_rating)
     st.write(f"You rate books {sign} than the average Goodreads user by {abs(avg_difference)}!") 

这完成了我们关于添加文本和额外统计数据的部分!

托管与推广

我们的最后一步是将这个应用托管到 Streamlit Community Cloud 上。为此,我们需要执行以下步骤:

  1. 为这项工作创建一个 GitHub 仓库。

  2. 添加一个requirements.txt文件。

  3. 使用 Streamlit Community Cloud 上的一键部署功能来部署应用。

我们在第五章《使用 Streamlit 部署到 Streamlit Community Cloud》中已经详细讲解过了这个内容,现在可以没有指引地尝试一下。

总结

这一章真是太有趣了!我们在这里学到了很多内容——从如何提出我们自己的数据科学项目,到如何创建初步的 MVP,再到我们应用程序的迭代改进。我们通过我们自己的 Goodreads 数据集来实现这一切,并且把这个应用从一个想法发展成了一个在 Streamlit Community Cloud 上托管的完全功能应用。我期待看到你们创造的各种不同类型的 Streamlit 应用。请创造一些有趣的应用,并在 Twitter 上发送给我,用户名是@tylerjrichards。在下一章中,我们将重点采访 Streamlit 的重度用户和创作者,了解他们的技巧与窍门,为什么他们如此广泛使用 Streamlit,并且他们认为这个库的未来将会怎样发展。到时见!

在 Discord 上了解更多信息

要加入这本书的 Discord 社区——你可以在这里分享反馈、向作者提问、了解新版本的发布——请扫描下面的二维码:

packt.link/sl

第十二章:Streamlit 资深用户

欢迎来到本书的最后一章!在这一章中,我们将向最顶尖的人士学习——Streamlit 的创作者,他们有着创建了数十个应用和组件的经验,Streamlit 的资深用户转变为 Streamlit 的员工,甚至还有 Streamlit 库的创始人,他如今在 Snowflake 内部领导公司。我采访了六位不同的用户,了解了他们的背景、使用 Streamlit 的经验,以及他们对不同经验层次的用户的建议。从这些访谈中,我们将了解到他们是如何在日常工作中使用 Streamlit 的,如何在教学中使用 Streamlit,并且我们还会探讨 Streamlit 的未来发展方向。

本章分为五个访谈:

  • Fanilo Andrianasolo,Streamlit 创作者以及 Worldline 的技术负责人

  • Adrien Treuille,Streamlit 创始人兼 CEO

  • Gerard Bentley,Streamlit 创作者和软件工程师

  • Arnaud Miribel 和 Zachary Blackwood,Streamlit 数据团队成员

  • Yuichiro Tachibana,stlite 的创作者以及 Streamlit 创作者

我们先从 Fanilo 开始!

Fanilo Andrianasolo

Tyler:嘿,Fanilo!我想我们可以开始了,你想先向大家介绍一下自己吗?你是做什么的?现在在忙些什么?

Fanilo:大家好,我叫 Fanilo。我在 WorldLine 工作了 10 年,最开始是做大数据工程师,后来转到业务拓展,再到产品管理,最后回到了管理数据科学团队。现在我主要作为开发者倡导者工作,负责内部和外部的数据科学与商业智能相关事务。

主要是与内部团队讨论他们如何在支付产品中利用数据分析,然后向外部做演讲,推广我们的数据分析和数据科学技能。

此外,我还是一名大学讲师。我讲授大数据和 SQL 课程,并且在讲课时使用 Streamlit 做演示。

最后,我还做了一些内容创作,主要是在 YouTube(www.youtube.com/@andfanilo/videos)和 Twitter(twitter.com/andfanilo)上,以及进行外部演讲。

Tyler:真是做了好多事!我意思是,你已经是这个 Streamlit 社区的一部分很长时间了。你是第一版书中的首批访谈对象之一,长期活跃于 Streamlit 论坛,并且现在你还是这个领域的大型 YouTuber。

Streamlit 作为公司以及开源库有哪些大的变化?

Fanilo:我刚刚意识到,自己已经用了 Streamlit 构建应用 3 年了!当我刚开始使用 Streamlit 时,团队只有 9 个人,社区也非常小。现在它已经真正爆发了,社交媒体上有那么多的帖子,真的是很难跟上所有的动态。

我觉得即使在如此庞大的用户基础和受欢迎程度下,Streamlit 依然非常适合新手或者非开发人员,作为创建数据应用的一种方式。

Streamlit 一直非常小心地确保它易于使用,同时也能让开发者构建更高级的应用,同时能够倾听庞大社区的声音。

一个变化是他们稍微放宽了一些限制,例如,我们两年前在 Markdown 中无法进行样式设置,但现在可以了。

Tyler:这真是太棒了。那么,当谈到 Streamlit 时,你的内容获取方式是什么样的?如果我想进入这个领域,了解发生了什么,看到人们在做什么,Twitter 是不是最好的发现平台?

Fanilo:我其实更喜欢使用 Streamlit 论坛上的每周总结(discuss.streamlit.io/),由 Jessica 管理。我不知道她是怎么把所有内容整合在一起的,但它一直是我最可靠的保持受 Streamlit 应用启发的方式之一。

我还在 Hugging Face Spaces(huggingface.co/spaces)看到了很多应用。我经常看到强大的应用程序,通常这两个来源都会出现在 Twitter 或 YouTube 上,所以等我看到的时候,已经是重复内容了。

Tyler:说到信息获取方式,你有没有一些最近创建的或者看到的你喜欢的应用程序?

Fanilo:我和多个想要与我分享自己应用的人交谈过。例如,有一位市场营销人员,他刚开始学习 Python,想创建一个小型 Python 应用来生成发票,并将其发送给客户。他还让同事们也能在这个应用上生成发票,所以这类小型应用是每个人都可以构建的。

我知道另外一个用例,一位用户围绕一个 Streamlit 多页面应用建立了自己的创业公司,他将这个应用提供给一些医疗机构,帮助他们在各个建筑之间跟踪药品库存。

看到这些人创造应用程序,即使这不是他们作为开发者的初衷,甚至他们不是开发者,我觉得非常有趣。我认为这些应用给我留下的印象最深刻。

Tyler:我完全理解;你真的会遇到很多有趣的人,他们会来和你谈论 Streamlit 或者他们遇到的问题。基于这个背景,构建 Streamlit 应用最困难的部分是什么?你觉得新的开发者常在哪些方面走错方向?

Fanilo:我经常看到很多人尝试构建实时应用程序,这是可能的,但也非常难以处理,因为他们会突然生成新的 Python 线程,或者线程与主线程发生冲突。管理这些线程很麻烦。

我还看到有些人想要将 Streamlit 与公司内部的设计模式整合,涉及所有视觉定制,或者将所有数据放入缓存或会话状态中。这些设计模式对于初学者来说,不太容易整合到 Streamlit 应用中。

我还看到有些人试图构建非常大的应用程序,而这会变得非常混乱,因为他们很难理解缓存是如何随着应用从头到尾的重新运行而工作的。

Tyler:你说的大应用是指什么呢?是指一个非常大的多页面应用,还是一个非常长的应用?

Fanilo:两者都有,但大多数情况下,我看到的是有些人想做的,例如在同一页面上对 40 个不同的图表进行交叉筛选。也许这样一个页面上的内容有点过多,你应该考虑把它拆分成多个应用,让每个应用有单一的责任,这样反而更有利于目标达成。把所有内容塞进一个页面,可能不如分开做多个页面好。

Tyler:相反,我提到过你看到了很多人学习 Streamlit,而且你在 YouTube 频道上有一些非常不错的教程。我觉得你目前最受欢迎的视频是那部史诗般的 Streamlit 教程视频(www.youtube.com/watch?v=vIQQR_yq-8I)。我该如何从观看你精彩的视频内容到构建酷炫的应用呢?

Fanilo:我认为做一件事一百次是成为它的高手的最佳方式。我做法是几乎每有一个随机的想法,我就做一个 Streamlit 应用。例如,最近我做了一个应用来追踪我何时开始参加羽毛球比赛。我做了另一个应用来查询 MongoDB 数据库。我还做了一个应用来查询我的 Outlook 邮箱,检查最近附加的文件,看看我是否需要下载它们。

所以,每次我有了一个创意,我就会做一个应用,做得越多,我就越熟悉 Streamlit 的生命周期。我理解 Streamlit 讲究的是创意表达性,我可以有一个想法,做个草稿,然后一点一点地迭代。我觉得这就是最好的学习方式。

Tyler:每次我看你的视频时,我既感到娱乐又感到受到了启发,我相信其他人也有类似的感受。说到 YouTube,你最喜欢自己制作的关于 Streamlit 的视频是哪些?

Fanilo:当你开始做 YouTube 时,你会发现你根本不知道人们到底喜欢什么。我自己最喜欢的视频反而是那些表现最差的视频,而我最不喜欢的视频正是那些表现最好的。

我最喜欢的视频是关于创建组件的,因为说实话,和 Python 开发者讨论 JavaScript 和 React 真的有点棘手。我有点为自己编辑这个视频的方式感到骄傲,它也让我想起了我在教程制作上的进步。

Tyler:你对那些有兴趣成为 Streamlit 内容创作者或教育者的人有什么建议吗?我知道你最近发布了一个关于这个的视频,我会把链接放在这里(www.youtube.com/watch?v=pT6lNKtGyP8&t=163s)。但我也想给你一个机会提到一些事情。

Fanilo:首先,我只能鼓励你去做,因为这为我带来了很多机会,认识了许多伟大的人,他们愿意与我分享应用,如果我只是停留在 Twitter 上,或者只是待在论坛上回答问题,我是不会有这些机会的。所以,这对我来说是一个巨大的好处,甚至在我自己的公司也是如此。

关于开始创作内容的不幸事实是,每个创作者都会有这种感受。每个内容创作者都会这么说。总是会有一种担心,也许人们不会喜欢你,或者人们会评判你。我也有这些对话,关于人们会评判我。如果我犯错了怎么办?或者如果人们取笑我怎么办?

我给的建议通常是,从小做起,并保持一致的进步,因为这才是创造事物的方式,慢慢做。而且,也不要把现在的自己或当前的旅程与那些已经做了 5 年的创作者作比较,因为没有什么可以比较的,那样做会是最快让你筋疲力尽的方式。

Tyler:我觉得这是一个很棒的建议。在职业生涯中也是一样的,你要确保不要把自己的第一年与别人第十年的情况作比较。

这真是太棒了,我只是想再次感谢你接受了两次采访!在我们结束之前,你还有什么其他想说的或者想聊的吗?

Fanilo:我很容易在网上找到,所以可以去看看我的 YouTube 频道——其他的一切都可以通过我的用户名andfanilo找到。

Tyler:Fanilo 可以在 GitHub(github.com/andfanilo)、YouTube(www.youtube.com/@andfanilo)和 Twitter(twitter.com/andfanilo)找到。

Adrien Treuille

Tyler:嘿,Adrien!很高兴再次和你聊天,谢谢你同意接受采访。你想先做个简短的介绍吗?

Adrien:是的!我曾是 Streamlit 的创始人兼 CEO 大约四年半,可能五年,之后在收购后,我成为了 Snowflake 内部 Streamlit 的负责人。

我们这里有两个主要目标,一个是将 Streamlit 的优秀第一方版本融入到 Snowflake 中,影响整个 Snowflake 产品线,另一个是维护和运营这个快速增长的精彩开源项目,它已经被全球最大的公司广泛使用。

从独立公司转变为 Snowflake,真的让我的工作职责得到了极大的提升,并且为其增加了许多维度,这让人非常兴奋。

Tyler:很好,我想我们可以直接开始,首先谈谈收购。这是我们第一次采访时最大的变化,当时这本书的第一版刚刚发布。正如你提到的,在 2022 年春天,Streamlit 被 Snowflake 收购。这个过程是怎样的?

Adrien:Streamlit 是一家不寻常的公司,它最初是一个个人项目,即使它成为了商业公司,它仍然是一个社区和开源项目,与作为商业公司的身份一样重要。显然,我们认为这个商业模式会有长期的成功,但我们始终是一个以使命为驱动的组织,因为我们真心相信我们带给世界的这个产品将推动技术的进步,并使人们的生活变得更好。

作为其中的一部分,我们从未一次性看 Streamlit 并问自己为何要做这个,或者我们如何尽可能榨取更多的利润。我们创建了一个平台,能够让尽可能多的人接触到这个产品,并与有才华的设计师、工程师和数据科学家一起继续改进它,造福每个人。

然后,当 Snowflake 接触我们并提出收购时,我们其实并没有真正考虑过这个选项。我们的预期是通过原来的公司来实现这一切。改变并且完全震撼我们的事情是,我们意识到 Snowflake 想要并且需要一个 Streamlit,正如我们当初试图为社区创造的那样。而这一点真的很酷。

这其实并不是我们从未想过的一个新商业计划,而是整个生态层级上激励机制的一种契合。我们有非常互补的激励和目标,这就像是,等一下,这真是太棒了。让我们与那些对成功有着和我们一样理解的人一起工作,而这不仅在收购时成立,现在依然成立。

我觉得 Streamlit 和 Snowflake 是一个绝妙的组合,并且随着时间的推移,它们的合作变得更加契合,而不是减少,这真的很棒,很酷。

Tyler:完全正确。正如你提到的,Streamlit 周围有一个庞大的社区,而你显然很在乎这个社区。在 Streamlit 还是一个新兴库的时候,故意去发展这个社区可能会稍微容易一些;那么随着它的规模不断扩大,这种情况有什么变化吗?

Adrien:当你经历这些快速扩展的变化时(如果你不想摧毁一切),关键是要时刻关注那些帮助你到达现在的东西,以及最重要的原则。

从 Streamlit 的早期,我们并没有把社区看作是产品的附属部分,而是把产品、论坛、网站、创作者计划以及我们在沟通上所做的努力,都视为社区的输入。

所以,退一步说,我们喜欢大声清晰地说出我们的原则,并在扩展过程中坚持这些原则。其中之一就是社区是一个有生命的东西,它是我们一切的核心。这一点在今天依然成立。另一个原则是我们如何谈论 Streamlit,以及我们对产品本身的热情。你会在我们的所有文档和论坛中注意到这一点,我们使用大量的感叹号和表情符号。我们写作的方式就像是一个对自己所讲内容充满热情的人,这对我们来说是真的!

社区是一个有生命的东西,它被培养并赋予了情感特征,比如对数据的真正热情、对探索的兴奋以及分享自己工作的热忱。我认为,这也是为什么人们即使在我们不断壮大的过程中,仍然能在 Streamlit 中发现持续的主题之一。

Tyler:在加入 Streamlit 之前,我注意到的一件事,并且作为员工仍然在注意到的,是对善意的强调。无论是与社区的互动,还是与其他员工的交流,这与我过去见过的其他团队明显不同。

Adrien:是的,即使在我年轻的时候,每当我领导一个小组时,它总是一个很友好的团队。我认为这点常常在 Streamlit 中得到体现,从联合创始人到员工,甚至是我们吸引到的社区成员和产品的使用者。多年来,这些特点已经形成了自己的生命力,且这些特征也发展成了各自独立的生命。例如,当我们开始招聘第一批员工时,我们有意识地筛选并选拔那些具有善意的人。后来我有点惊讶,发现很多员工在面试时也会筛选善意!某些情感维度已经以一种最酷的方式,变得自我推动。

Tyler:接下来,在我们之前版本的这本书的第一次访谈中,当我们讨论 Streamlit 随着时间的变化时,你提到 Streamlit 对 Python 编程的意义比对机器学习或数据科学的意义更为基础,而且 Streamlit 那时候是如此“玩具化”。你觉得这个愿景现在实现了吗?

Adrien:我完全同意。Streamlit 正在超越仅仅构建丰富有趣的应用程序的能力,开始着手实现作为 Python 之上视觉层的目标。

另一个方面是,Python 有很多细分领域,比如它是计算机科学教育的主要语言,也是数据工程工作的绝佳语言。它也是一种脚本语言,用于在成千上万种不同的 API 和文件格式之间进行协调。所以,我们看到了像 GPTZero 应用(gptzero.me/)这样的应用,它源自 Streamlit,并且做着硬核的机器学习工作,或者是其他用于全球打击人口贩卖的应用。

现在甚至有一些由 GPT-3 自己创建的 Streamlit 应用实例。如果有任何语言或框架能作为像 GPT-3 这样的大型语言模型的产物,Streamlit 就是最合适的选择,因为它具有独特且简单的数据流。它非常适合这一类模型,并且简化了它们能创建的应用程序的复杂性。我们现在只是看到这一切的开始,我认为它有着极其丰富和令人惊叹的未来。我迫不及待了。

Tyler:Adrien,非常感谢你来和我们分享。你有没有其他想聊的事情?

Adrien:我认为 Streamlit 非常酷的一点是,我们关于数据科学趋势的许多假设都通过我们创建产品的经验得到了验证。核心想法非常简单;这不是火箭科学!这是一种全新的数据工作方式,原生支持 Python,连接了 API、pandas、机器学习和所有其他 Python 概念。在所有不同的方向中,我认为仍然有大量绿地待开发的是机器学习,而且有许多不同的角度可以探讨。我们已经讨论过的一个角度是 AI 生成应用程序,我认为这是非常现实的,未来几年我们将会看到越来越多这样的应用。另一个角度是将 Streamlit 更深入地融入机器学习开发过程中,特别是在构建模型和探索大型数据集时。机器学习工程师的工作体验中几乎对可视化层有无限的需求,所以看到这一发展并为之助力是非常令人兴奋的。

Tyler:再次感谢你来参加这个访谈,Adrien!你可以在 Twitter 上找到 Adrien,网址是 twitter.com/myelbows

Gerard Bentley

Tyler:嘿,Gerard!我们开始吧,我想知道你能不能简单介绍一下你的背景。你做什么工作?是什么让你进入数据领域的?

Gerard:现在,我在一家叫 Sensible Weather 的初创公司从事后台 Web 服务工作。我们销售一种针对恶劣天气的保险担保,这是气候科技领域的新产品。我在这里没有太多时间使用 Streamlit,但我在一些内部使用的工具上用到了它,虽然这些工具还没有完全部署。

之前,我在一家抵押贷款公司工作,主要做批量 ETL 工作,那时我接触到了一些金融领域的数据科学和预测模型。在那里,使用 Streamlit 创建一个内部工具来可视化如果一个人的信用评分更高,或者如果他们支付更多的本金等情况,感觉很有趣。在 Streamlit 之前,我们没有工具能快速回答这些问题。

Tyler:那么,你主要是专注于数据科学团队创建的模型,并制作互动应用程序来展示这些模型,对吧?是什么让你进入数据领域的?

Gerard:在本科毕业后,我与 Pomona College 的 Osborn 教授一起做了一年的 AI 研究,我们研究的是经典电子游戏中的计算机视觉。我们训练了卷积神经网络,并构建了数据管道来记录游戏截图并标注训练数据。

那时 Streamlit 还不存在。我当时构建的是一个加载图像的界面,要求用户标注图像特征,然后保存新的训练数据。但我花了好几个月的时间才学会足够的 JavaScript、Nginx 和 Docker,才得以部署一个有用的 Flask 应用。

Tyler:我在尝试使用 Flask 和 Django 创建项目时有很多经验,它们确实非常难以使用。那么,在这些经历之后,你是怎么开始接触 Streamlit 的呢?

Gerard:我是在为 AI Camp 做远程教学工作时听说 Streamlit 的。有一位老师建议我用它来和学生们一起做项目。有一个 Python 初学者仅仅花了一天时间就做了一个个人网站,然后在接下来的一周里,自己添加了图片和交互性。他们构建了一个完整的计算机视觉应用,这让他们充满了信心,在我看来也非常令人印象深刻。所以,我就是从这里开始接触 Streamlit 的,之后我开始做一些小项目,尝试理解工作中的新东西,也用它来做自己的项目。

Tyler:那真是太酷了。很多应用程序都有教学性质,你的目标是教会别人某些东西,可能是学生,或者是工作中的不同合作伙伴。那么你在使用 Streamlit 时,是什么时刻让你有了“啊哈”时刻?在什么情况下你意识到你真的喜欢这个库?

Gerard:我想我很早就看到了这一点。当一个学生只用了大约一天的时间就运行了一个带有摄像头支持的 YOLO 计算机视觉 Web 应用时,我就知道了。我清楚地记得,当时我花了好几周才用 HTML 和 JavaScript 构建一个这样的应用。从那时起,我就明白了,Streamlit 可以用来与任何机器学习模型进行交互。

Tyler:从那时起,你做了很多不同的 Streamlit 应用。你学习 Streamlit 的曲线是怎样的?你觉得它在一开始进展很快,然后进入了瓶颈期吗?是容易学习,难以精通,还是难学,易精通?或者是持续不断的学习过程?

Gerard:Streamlit 确实很容易上手。为了学习,我浏览了它的画廊,并复制了我想要模仿的应用的源代码。然后,我会进行一些修改,让它们成为我自己的。我已经对 Python 很熟悉了,所以这个过程进行得很顺利。

很容易学习,但要精通则更难。当我试图将传统 web 应用中的一些功能实现出来,比如客户端状态和异步函数时,我不得不在论坛和网上搜索解决方案。

Tyler:我也是这么觉得的。那么,为什么你会创建像 Fidelity 应用这样的 Streamlit 应用(github.com/gerardrbentley/fidelity-account-overview)?这是你掌握 Streamlit 过程中的一部分吗?

图 12.1:Gerard 的 Streamlit 应用

Gerard:部分原因是为了建立一个我可以展示给可能雇用我的公司的作品集。部分原因是我自己的兴趣;这些是我主要为自己构建的应用程序,看看它们是否能工作。对于我来说,Streamlit 目前是最快的方式。

Tyler:在 Fidelity 应用上,是更多地偏向于“我开始寻找新工作,并且想要开发一个作品集”,还是更多地偏向于“我有一个个人问题,我正在试图解决”?

Gerard:这个例子是两者的强烈结合。我想为自己的数据构建一个仪表盘,所以这是一个个人问题。同时,我也在考虑一个可以展示数据分析技能的项目,以便申请数据科学职位。其他应用程序则更多地集中于后端概念,这在面试中非常有用。

Tyler:Fidelity 应用花了多长时间?你是怎么得到这个想法的?你能讲讲构建和思考过程吗?

Gerard:我想第一版可能花了大约 4 小时的编码和研究时间。我对自己从电脑中的 CSV 文件加载 DataFrame 的技能很有信心,而且我已经构建了几个带有文件上传输入的应用,Streamlit 让这一过程非常简单。加载数据很容易,接下来我需要清理数据,展示数据并添加过滤器。

构建它最耗时的部分是制作视觉效果。根据我的经验,Plotly 拥有最漂亮的默认图表,所以这帮助我在没有大量代码的情况下尝试了多种图表的组合。

Tyler:第一版 Fidelity 应用花了四个小时,之后发生了什么?

Gerard:之后,我基本上想,哦,我怎么分享这个呢?我怎么才能让别人轻松使用这个,而不必在他们的电脑上运行 Streamlit 呢?所以,这就包括了一个可用的描述,然后是一些关于添加颜色和使用 Streamlit AG Grid 的细节。

那大约又是 2 小时的工作,用来清理并美化它。然后,我对它能够发布出去感到满意。这是在我读完你书的第一版后,在 Twitter 上私信你讨论过之后。然后,你分享了这个应用,我立刻想,哦,我应该把它做得更漂亮一点。

Tyler:我很喜欢这个应用!你有没有考虑过为一个应用收费?也许考虑如何收费超出基本功能?

Gerard:我有想过一点。比如我构建的一些应用,比如 QR 码生成器(github.com/gerardrbentley/flash_qr),与现有的 SaaS 产品类似,但我只是做了一个有趣的版本。我肯定认为一些应用可以采用分层或免费增值结构,但我从未付出过努力去实现这一点。

Tyler:你发布应用之后,第一次与 Streamlit 社区的互动是什么样的?大致来说,之后发生了什么?人们是通过 GitHub 或 Twitter 联系你吗,还是有什么回应?

Gerard:在那之后,我发布了一些关于应用的内容,大家开始在 GitHub 上关注我,并且 fork 了代码。这是我看到的主要互动。

然后,Streamlit Community Cloud 推出了观众数据功能,我看到大家每个月都会使用这个应用,或者至少查看过它!

Tyler:在你们开发第一个应用集之后,有没有一些你们希望早点知道的大事?比如关于 Streamlit 的流程、存储、状态或缓存之类的?

Gerard:绝对的,使用表单来防止重新运行是非常重要的。我做过几个时间序列预测的应用,每当你调整一个滑块时,它会触发到处的变化。但如果你将这些放在一个表单中,就能更好地控制执行过程。

当我刚开始时,像 st.stop()st.experimental_rerun() 这样的控制流函数还很少。但现在,我使用它们来防止代码在一堆 if 语句中嵌套得太深。而且,在应用停止时显示一个警告说明原因也是很好的做法,因为用户并不如你了解你的应用。

Tyler:我只是想感谢你们进行了一场精彩的采访。我真的很喜欢你们的所有应用!如果你正在阅读这篇内容并且想找 Gerard,你可以在 LinkedIn 上找到他:www.linkedin.com/in/gerardrbentley 或者访问他的网站:home.gerardbentley.com/

Arnaud Miribel 和 Zachary Blackwood

Tyler:嘿,Zachary 和 Arnaud!首先,你们每个人能跟大家简单介绍一下自己吗?

Zachary:正如你提到的,我是 Zachary Blackwood。我最开始是做教师的,但当这份工作无法支付账单时,我转行做了网页开发,后来因为他们使用 Python,我被吸引到数据团队,感觉这很有趣。

我之前在一个小型农业科技创业公司工作,后来被一个更大的公司收购,在那里我学到了很多关于基础设施和数据工程的知识。期间,我为数据科学团队开发了多个仪表板,我们尝试了好几种不同的框架。我的一个朋友向我展示了 Streamlit,这也许是我有些超前了,但我很喜欢它。

后来,那个朋友告诉我,他有个朋友在 Streamlit 工作,正在找一位数据工程师。我立刻申请了,现在,我在 Streamlit 的数据团队工作。

Arnaud: 大家好,我是 Arnaud Miribel。我从事数据科学已经超过五年了。我开始在几家不同领域的公司工作,这也是我一直喜欢数据科学的原因:你可以涉足任何领域。第一个公司是在法律领域,做的是法院判决的自然语言处理NLP)。另一个是在医院,我做的是医学报告上的机器学习。还有一个是在继续教育领域,教授机器学习给很多人。

在过去的两家公司,我使用了 Streamlit,并且非常喜欢它。我认为它是有史以来最棒的东西;它突然让我的工作变得轻松多了。这与我之前在学生项目中使用 Plotly、Jupyter 或 Flask 时的困境完全不同。所以,当我发现它时,我非常高兴。

我在 Twitter 上看到 Johannes,他和我有类似的背景,开始在柏林的 Streamlit 工作。这让我感到非常惊讶,因为我一直以为 Streamlit 只存在于旧金山湾区!我在 Twitter 上给他发了私信,他让我和 Adrien 会面,然后……我开始为 Streamlit 工作了。那是大约两年前,这就是我开始的方式。

Tyler: 有时候事情就是这么奇妙地运作;我知道我也是通过 Twitter 发现了 Streamlit。那么,既然你们俩也和我在 Streamlit 的数据团队工作,而你们在过去大约六个月的时间里一起花了很多时间建立了一个名为 streamlit-extras 的库(extras.streamlitapp.com/),其中包含了我们多年来在 Streamlit 数据团队上构建的许多小工具。你们能谈谈是什么中心问题激发了你们的灵感吗?

Arnaud: 从某种程度上来说,这是有些自私的,因为我们希望有某种方式分享我们所有的发现。然后我们意识到这是一个开源产品,我们应该将其分享给每个人,这是我们展示自己作为重度 Streamlit 用户的经验和教学的一种方式。我们也希望将它作为一种实验发布,看看社区对此做出什么反应,并从中汲取灵感,进一步发展。

Tyler: 所以,当你说到经验和教学时,你指的是什么?

Arnaud: 在数据团队中,我们有一个大型的多页面应用程序正在开发。在这个应用中,我们做了很多基于 Streamlit 的视觉功能或技巧,作为增强视觉效果和提升观看体验的方法。因此,我们希望将这些功能隔离出来,并打包成一个单独的包。

还有一些功能是为了让数据科学家更高效,比如系统地让图表始终显示底层的 DataFrame,以及一个将数据导出为 CSV 文件的按钮。我们希望将这些功能打包在一起,因为它们作为组件非常契合,我们认为将一组小组件称为“extras”是有意义的!

Zachary:除了我们内部使用的功能外,另一个额外功能的来源是社区论坛!我们定期花时间关注论坛,看到人们遇到的各种问题及其解决方法非常有趣。经常会有一些非常常见的问题,虽然需要一些设置代码,但一旦设置好了就能很好地工作。一个简单的例子是,有人希望在多页面应用中把应用的 logo 放在页面列表上方的左上角。实际上,这并不难做到,但我们确实看到很多人询问过这个问题。所以,我们将其做成了一个功能,并把这个功能放到了streamlit-extras中(extras.streamlit.app/App%20logo)。许多额外功能直接来自论坛上有人提出的需求。我们要么自己弄明白了,要么其他人弄明白了,然后我们将其加入 extras 中,便于让大家发现并避免重复造轮子。

Tyler:我最喜欢的额外功能之一是那个让你轻松添加带有实心底线的标题(extras.streamlit.app/Color%20ya%20Headers)和数据资源管理器功能(extras.streamlit.app/Dataframe%20explorer%20UI)。你们两个有最喜欢的吗?

图 12.2:资源管理器

Arnaud:我最喜欢的是最近添加的一个叫做“图表容器”(extras.streamlit.app/Chart%20container),它是一个非常高效的 API,用于快速创建 BI 组件。这样,无论你在处理 DataFrame 时,都可以创建图表、显示 DataFrame,并且通过一个函数将其导出为 CSV 文件。

图 12.3:图表容器

Zachary:我也喜欢这个!我有一个功能我们内部不常用,但它是一个经常会遇到的问题。人们会对点击按钮后,做完其他操作再返回时,按钮“不保持点击状态”感到困惑或沮丧。这是因为 Streamlit 的模型是每次交互时都从头到尾重新运行脚本。人们有时期望它像复选框一样工作,点击一次后会记住已被点击。

我们制作的额外功能叫做 Stateful Button(extras.streamlit.app/Stateful%20Button),它在重新运行时依然保持被点击状态。虽然它很简单,但我认为它非常好,并且解决了人们在实际应用中常遇到的问题。

Tyler:我将所有这些额外功能看作是一个途径,如果你是团队中的一名独立开发者,可以通过这些功能了解最佳实践,并在使用 Streamlit 时获得优势。到目前为止,尚未提到的一个功能叫做图表注释(extras.streamlit.app/Chart%20annotations),它允许你在图表上添加可点击的注释。我认为这是在工作中使用的一个极好的附加功能,尤其是在你希望能够在图表本身上解释图表中大幅波动时:

图 12.4:注释

就库本身的开发而言,开发这样的东西遇到了哪些困难?

Zachary:它最初其实是作为一个叫做 st-hub 的项目开始的!我们希望能够让组件和函数都能轻松地在一个地方发现。这个项目变成了一个独立的项目,但其中一个重要部分就是建立了我们现在在 extras.streamlit.app 上的这个画廊。对我们俩来说,这个过程非常有趣。目前,画廊是基于每个附加功能的源代码和一些你设置的魔法变量(www.pythonmorsels.com/dunder-variables/)动态生成的。然后,它构建了你在主画廊页面上看到的所有页面,包括代码、文档字符串、使用示例等。这是我最喜欢的 streamlit-extras 部分之一——技术挑战以及为生成这种动态页面所做的巧妙解决方案。我们在这上面花了很多时间,因为我们希望尽可能降低门槛,让社区中的其他人也能参与进来。

Arnaud:我同意,画廊可能占了 75% 的工作量。有趣的是,最开始挑战并不那么明确,我们做这件事的动机也不完全清晰。但我们知道我们确实想让 Streamlit 团队中的项目变得更容易被发现,也想让组件更易于被发现和创建,所以我们认为这是一个很好的社区起点。

Tyler:写这本书时,我遇到的一个困难是,实际上我可以写出很多关于组件的章节。而且很可能,我必须教大家一定量的 CSS、HTML 或 JavaScript,或者在 Python 中,我必须展示如何创建和上传包到 PyPI。在创建美丽的 Streamlit 应用和创建组件之间,存在着一个非常大的跨度。

这也是我想邀请你们俩的一个重要原因,鼓励大家从 streamlit-extrasextras.streamlit.app/Contribute)开始他们的组件创建之旅。

我总是在这些访谈的最后问嘉宾是否还有其他想说的,或者是否还有其他想聊的话题。现在是你的时间了!

Zachary:在加入 Streamlit 之前,我在公司开始第一次创建开源包,虽然这些包在我的团队之外没有被广泛使用,但我真的很享受成为开源生态系统的一部分。现在能够参与一个被广泛使用、拥有强大用户基础并且正在积极开发的项目真的很有趣。我认为 Streamlit 有很多方法可以让贡献变得更加容易,但我真的很高兴能够成为这个社区的一部分,并将这些工作传递下去。为我的团队创造解决他们问题的工具真的很有趣,然后把它发布到论坛供其他人使用也很有趣。

Arnaud:正如他所说的那样。我鼓励每个人尝试 streamlit-extras,玩得开心,并告诉我们他们觉得好的或不好的地方。更一般地说,这是我第一次做开源贡献,非常有成就感,因为你会看到有人在使用它,甚至在 YouTube 视频中引用它。最终,这也是一种承诺,因为你会受到那些提交 PR 或问题的人施加的压力,你会意识到某些东西看起来不好或没有按预期工作。我鼓励大家尝试一下,并尝试构建自己的开源包,因为这确实是一次美好的冒险。

Tyler:非常感谢你们两位过来和我们聊天!你可以在 GitHub 上找到 Zachary,网址是 github.com/blackary,在 Twitter 上是 twitter.com/blackaryz,你可以在 GitHub 上找到 Arnaud,网址是 github.com/arnaudmiribel,在 Twitter 上是 twitter.com/arnaudmiribel

Yuichiro Tachibana

Tyler:嘿,Yuichiro,和你一起做这个真的太棒了!你想先向大家介绍一下自己吗?谈谈你的背景吧。

Yuichiro:好的,我是 Yuichiro Tachibana,目前在日本。我也是 Streamlit Creators 计划的成员,正在开发和维护一些 Streamlit 库。我的个人开源项目托管在 GitHub 上!

就我的背景而言,我在大学学习了计算机视觉和机器学习的应用。我与做机器人研究的人合作,软件对于帮助建立机器人智能,尤其是在计算机视觉方面是必要的。这就是为什么我对计算机视觉和机器学习的知识更侧重于实践而非理论的原因。

之后,我休学了一年,开始了自己的个人项目,并获得了日本政府的部分财政支持,在这个项目中我构建了一个基于现场可编程门阵列FPGA)的深度学习加速器。当时我使用的是非常底层的技术;大约是 2014 年。

然后,我去了一个商业公司,在那里我参与了从自然语言处理到计算机视觉的各种项目,重点是商业产品。那时,我的主要技能逐渐转向更侧重软件开发,而不仅仅是计算机视觉或机器学习。

在这家公司,我开始开发一个基于网页的视频流录制系统,其中一些部分与我最终构建的Streamlit-webrtc组件类似。

然后,我加入了我朋友创办的公司,开始处理更结构化的数据。我是软件工程师,负责构建内部应用程序,用来展示一些基于日志的数据或分析结果。那时,我开始大量使用 Streamlit。

现在,我没有工作。我在休息,这就是我长篇的自我介绍!

泰勒:真酷,真是一段有趣的旅程。你是在做网页视频流工作时了解 Streamlit 的,还是主要是在最后一家公司才接触的?

裕一郎:实际上,我是在我第一家公司作为软件开发人员时开始接触 Streamlit 的。我与一些计算机视觉领域的研究人员合作,他们通常通过 OpenCV 展示他们的演示。他们会把笔记本电脑拿到我座位旁边,展示他们工作的酷炫!我最初的动机是寻找一些替代工具,帮助他们创建一些便携和可分享的演示。我想我最初是通过 Twitter 发现 Streamlit 的。

之后,我在 Streamlit 中找不到任何能帮助我进行实时视频流的工具,于是我构建了Streamlit-webrtcgithub.com/whitphx/streamlit-webrtc),它是一个组件,允许你在 Streamlit 中处理实时视频和音频流。

泰勒:我觉得Streamlit-webrtc真的很酷,不仅因为它有用,而且因为它看起来非常非常难以制作。你想谈谈这个组件的开发过程吗?人们是怎么使用它的?

图 12.5:WebRTC

裕一郎:首先,非常感谢。我认为创建和开发这个库的最初动机是帮助计算机视觉研究人员或软件开发者,他们希望在自己的模型和实时视频流的基础上创建演示。因此,从我的角度来看,它替代了 OpenCV 的一部分功能。

泰勒:我觉得这也引出了你与 stlite(github.com/whitphx/stlite)的工作,stlite 是 Streamlit 的无服务器版本,基本上意味着你可以与某人分享一个 Streamlit 应用,他们可以在自己的浏览器中本地运行它,而不需要为该应用做任何 Python 设置。你能谈谈构建 stlite 时的动机吗?当时的核心问题是什么?

图 12.6:stlite

Yuichiro:嗯,我该从哪里开始呢?一开始,我先从 Jupyter 开始看,Jupyter 和 Streamlit 类似,因为它将一些前端代码抽象出来,隐藏了开发者的视线,让开发者主要只需编写 Python 代码。然后我看到 Jupyter 可以转换为 WebAssembly,这意味着它可以在浏览器中运行并完全本地运行。那时,我就知道我也可以用 Streamlit 做同样的事情。

我也有这样的感受,认为离线体验很重要。有些人会希望拥有一个以隐私为中心的体验,所有内容都在本地运行。

Tyler:没错,有些事情你希望集中管理,而有些事情则真心希望使用自己的电脑来处理。到处都是薄客户端和厚客户端!

Yuichiro:没错。在我与深度学习加速器合作的过程中,我有一个假设,认为离线、本地化的能力对计算机视觉或机器学习领域尤其有利,主要是因为隐私的考虑。

Tyler:然后,你对隐私和本地开发的重要性有了更深的看法,并且你也有了 WebAssembly 解决方案的技术想法。从那之后,事情是如何发展的呢?

Yuichiro:在那个时候,我已经有了这个想法,但我知道这会非常困难。大概过了半年,我才真正开始动手,尽管那时 Streamlit 论坛上有一些讨论,但我无法找到一个从实现角度来解决技术问题的清晰方法。

这一切在 2022 年 4 月下旬发生了变化,PyScript(pyscript.net/)的发布使得你可以在浏览器中运行 Python。这是一个潜在的竞争者,在看到它之后,我决定将我所有的精力投入到这个项目中。于是,我在下周就开始了开发。

Tyler:那太棒了;我推荐 stlite 给许多刚开始接触 Streamlit 的人,因为他们不需要自己做开发环境的设置。他们可以直接开始编程,立刻就能看到结果,简直像魔法一样。

在你看来,接下来会怎么发展?

Yuichiro:我在这方面有很多待办事项。但是我仍然没有完全清晰的未来发展蓝图。所以,首先,我希望你和这次采访的观众能告诉我关于 stlite 的有趣应用。如果你们有任何想法,请告诉我!

其他一些应用可能与边缘计算相关,比如 Cloudflare 及其基于 WebAssembly 的边缘计算服务,但它们不支持 Python。也许我们可以在那方面做些集成。

工业公司也可以使用 stlite 创建商业级产品,将 Streamlit 应用程序运行在客户端,以降低服务器成本,或者如果他们的大部分客户非常重视数据隐私,就像你说的那样。

Tyler:换个话题,你显然已经投入了大量时间参与 Streamlit 组件生态系统和论坛;是什么让你一直留在这个社区和库中?

Yuichiro:我会说我有三个理由。

首先,Streamlit 的技术设计。这个库的设计使它恰好位于简洁性和可扩展性之间的关键点。它是一个极其优秀的 API 设计。Streamlit 还帮助我将我的技术兴趣和技术构思转化为实际应用,并将其托管并与用户或同事共享。

第二个原因是社区和公司。公司本身也非常重视社区,我认为他们投入了大量的资金和努力来管理和维护社区。还有很多致力于维护社区健康的组织者,包括我自己。这让我在与 Streamlit 合作时感到非常舒适。

第三个原因是 Streamlit 的可扩展性。这也是我最初选择它的原因。没有其他选择可以让我在其基础上创建一个实时视频流组件!

Tyler:这些都是很棒的理由。我只是想给你一个机会,在采访结束时推广你正在做的任何事情,或者谈谈你在思考的任何事情。

Yuichiro:我没有太多要说的,但我想说我热爱开源软件。在更高层次上,每当你使用开源软件时,全球的开发者都免费为你创建了它,你可以用它来向世界表达你的意图、奉献和兴趣,并且创造一些非常有趣的原创内容。所以,我想这是我对全球开发者的自我宣传。与此同时,你应该尽可能地向世界表达你的兴趣,并且也应该尽力为开源软件生态系统做贡献和支持。资助一些项目,以便开发者可以继续制作你所使用的产品!

Tyler:再次感谢 Yuichiro!你可以在 GitHub 上找到 Yuichiro:github.com/whitphx,在 Twitter 上找到:twitter.com/whitphx

总结

这也意味着第十二章Streamlit 高级用户,以及本书的结束!我们在这一章中涵盖了许多深刻的内容,从与 Fanilo 讨论社区开发的重要性,到与 Arnaud 和 Zachary 分享一些流行组件的实际示例,再到与 Adrien 讨论 Streamlit 的玩具般功能以及 Streamlit 未来的方向。我们与 Yuichiro 一起了解了无服务器的 Streamlit,并与 Gerard 一起探索了 Streamlit 的新趣味应用。

我只是想对你阅读本书表示感谢;这本书对我来说是一次充满热情的创作,我希望你能与我联系,告诉我它是如何影响你的 Streamlit 开发者体验的。你可以在 Twitter 上找到我:twitter.com/tylerjrichards,我希望你在阅读这本书时,能像我写这本书时那样享受其中。感谢你,去创造一些精彩的 Streamlit 应用吧!

在 Discord 上了解更多

要加入这本书的 Discord 社区——你可以在这里分享反馈、向作者提问并了解新版本——请扫描下面的二维码:

packt.link/sl

posted @ 2025-10-24 10:04  绝不原创的飞龙  阅读(64)  评论(0)    收藏  举报