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

105 阅读1小时+

Streamlit 数据科学第二版(二)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:美化 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://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/826d2496b4774f63a0b47c91ef04f1b0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771548065&x-signature=sxzlIR04NmY41XqV%2Ftow77K7Wjk%3D) 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