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

81 阅读1小时+

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,输出每千个tokens0.0015,输出每千个 tokens0.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