Python-特征工程秘籍第三版-四-

40 阅读1小时+

Python 特征工程秘籍第三版(四)

原文:annas-archive.org/md5/86f7f4009ce12f06cb3f3594f063591b

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:使用 Featuretools 从关系数据中提取特征

在前面的章节中,我们处理了以行和列组织的数据,其中列是变量,行是观察值,每个观察值都是独立的。在本章中,我们将专注于从关系数据集中创建特征。在关系数据集中,数据结构跨越各种表,可以通过唯一标识符进行连接。这些唯一标识符表明不同表之间存在的关系。

关系数据的经典例子是零售公司持有的数据。一张表包含有关客户的信息,例如姓名和地址。第二张表包含有关客户所进行的购买的信息,例如每次购买的物品类型和数量。第三张表包含有关客户与公司网站互动的信息,例如会话持续时间、使用的移动设备和访问的页面。客户、购买和会话通过唯一标识符进行识别。这些唯一标识符使我们能够将这些表放在一起,这样我们就可以获取有关客户购买或会话的信息。

如果我们想了解更多关于我们拥有的客户类型(即客户细分)或者预测他们是否会购买产品,我们可以创建在客户层面跨不同表汇总或总结信息的特征。例如,我们可以创建捕捉客户在购买中花费的最大金额、他们所进行的购买次数、会话之间的时间或平均会话持续时间的特征。我们可以创建的特征数量以及我们可以在表中跨表汇总数据的不同方式是丰富的。在本章中,我们将讨论使用featuretools Python 库创建关系数据聚合视图的一些常见方法。我们将从设置各种数据表及其关系并自动创建特征开始,然后我们将详细介绍我们可以创建的不同特征。

在本章中,我们将涵盖以下食谱:

  • 设置实体集并自动创建特征

  • 使用通用和累积操作创建特征

  • 组合数值特征

  • 从日期和时间中提取特征

  • 从文本中提取特征

  • 使用聚合原语创建特征

技术要求

在本章中,我们将使用pandasmatplotlibfeaturetools开源 Python 库。您可以使用pip安装featuretools

pip install featuretools

此外,您还可以使用conda

conda install -c conda-forge featuretools

这些命令安装了基本的 featuretools 功能,但我们可以安装用于使用 dask 作为后端而不是 pandas 的附加组件。有关如何安装 featuretools 附加组件(包括 graphviz)的更多信息,请参阅他们的文档:docs.featuretools.com/en/v0.16.0/getting_started/install.html

我们将使用来自 UCI 机器学习仓库的 Online Retail II 数据集,该数据集可在 archive.ics.uci.edu/ml/datasets/Online+Retail+II 找到,并受 Creative Commons Attribution 4.0 国际CC BY 4.0)许可协议的约束:creativecommons.org/licenses/by/4.0/legalcode。该数据的相应引用如下:Chen, Daqing(2019)。Online Retail IIUCI Machine Learning Repository (doi.org/10.24432/C5CG6D)。

我已下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/prepare-retail-dataset.ipynb

你可以在附带的 GitHub 仓库中找到修改后的数据集:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/retail.csv

设置实体集和自动创建特征

关系型数据集或数据库包含分散在多个表中的数据,表之间的关系由一个唯一标识符决定,它告诉我们如何连接这些表。为了使用 featuretools 自动创建特征,我们首先需要输入不同的数据表,并在所谓的 featuretools 中建立它们之间的关系,以便库可以自动根据这些关系创建特征。

我们将使用包含客户、发票和产品信息的数据集。首先,我们将设置一个实体集,突出这三个项目之间的关系。这个实体集将是本章剩余菜谱的起点。接下来,我们将通过在客户、发票和产品级别汇总数据,利用 featuretools 的默认参数自动创建特征。

在本菜谱中,你将学习如何正确设置实体集并自动为每个实体提取一系列特征。在接下来的菜谱中,我们将深入探讨我们可以使用 featuretools 创建的不同类型的特征。

准备就绪

在这个食谱中,我们将使用来自 UCI 机器学习仓库的在线零售 II数据集。在这个表中,有客户,他们是批量从零售公司购买的企业。客户通过一个唯一的customer_id标识符来识别。每位客户进行一次或多次购买,这些购买通过一个唯一的invoice标识符标记,包含发票号。在每张发票中,都有客户购买的一个或多个项目。公司销售的每个项目或产品也通过一个唯一的库存代码来识别。

因此,数据有以下关系:

图 9.1 – 显示数据中关系的图

图 9.1 – 显示数据中关系的图

每位客户进行了一次或多次购买,由发票号识别。每张发票包含一个或多个项目,由库存代码识别。每个项目可以被一个或多个客户购买,因此出现在多个发票中。考虑到这些关系,让我们继续进行食谱。

如何操作...

在这个食谱中,我们将使用数据设置一个实体集,然后突出显示数据集中的不同关系。最后,我们将通过在客户、发票和产品级别对数据集中的信息进行聚合来创建特征:

  1. 让我们导入所需的库:

    import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载在准备就绪部分描述的零售数据集,并显示其前五行:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    df.head()
    

    在下面的屏幕截图中,我们可以看到客户(customer_id)和发票(invoice)的唯一标识符,以及关于每张发票购买的项目(如项目的代码stock_code)、描述、数量、单价以及发票日期的附加信息:

图 9.2 – 在线零售 II 数据集

图 9.2 – 在线零售 II 数据集

注意

使用pandasunique()函数来识别唯一的项目、客户和发票的数量——例如,通过执行df["customer_id"].nunique()

  1. 让我们用一个任意的名字初始化一个实体集,例如data

    es = ft.EntitySet(id="data")
    
  2. 让我们在实体集中添加一个 DataFrame;我们给 DataFrame 起一个名字(data)。我们需要为每一行添加一个唯一标识符,我们称之为rows,由于在这个数据集中我们没有唯一的行标识符,我们将通过设置make_index=True创建它作为额外的列。最后,我们指出invoice_datedatetime类型,customer_id应被视为Categorical

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={ «customer_id»: Categorical},
    )
    
  3. 接下来,我们添加原始data DataFrame 和invoices之间的关系。为此,我们指出原始或基本 DataFrame,我们在步骤 4中将其称为data,我们给新的 DataFrame 起一个名字,invoices,我们添加发票的唯一标识符,并将包含customer_id的列添加到这个 DataFrame 中:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

我们将customer_id变量复制到invoices数据中,因为我们想创建客户和发票之间的后续关系。

  1. 现在,我们添加第二个关系,即客户与发票之间的关系。为此,我们指示基础 DataFrame,我们在步骤 5中将其命名为invoices,然后我们为新 DataFrame 命名,命名为customers,并添加唯一的客户标识符:

    es.normalize_dataframe(
        base_dataframe_name=»invoices»,
        new_dataframe_name=»customers»,
        index=»customer_id»,
    )
    
  2. 我们可以在原始数据和产品之间添加第三个关系:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»items»,
        index=»stock_code»,
    )
    
  3. 让我们显示实体集中的信息:

    es
    

    在以下输出中,我们看到实体集包含四个 DataFrame:原始数据、invoices DataFrame、customers DataFrame 以及产品或items DataFrame。实体还包含发票或项目与原始数据之间的关系,以及客户与发票之间的关系:

    Entityset: data
      DataFrames:
        data [Rows: 741301, Columns: 8]
        invoices [Rows: 40505, Columns: 3]
        customers [Rows: 5410, Columns: 2]
        items [Rows: 4631, Columns: 2]
      Relationships:
        data.invoice -> invoices.invoice
        invoices.customer_id -> customers.customer_id
    invoices DataFrame:
    
    

    es["invoices"].head()

    
    We see in the following output that `featuretools` automatically created a DataFrame containing the invoice’s unique identifier, followed by the customer’s unique identifier and the first date registered for each invoice:
    

图 9.3 – 发票级别的 DataFrame 信息

图 9.3 – 发票级别的 DataFrame 信息

  1. 现在我们显示customers DataFrame:

    es["customers"].head()
    

    在以下输出中,我们可以看到featuretools自动创建了一个包含客户唯一标识符的 DataFrame,随后是此客户的第一个发票日期:

图 9.4 – 客户级别的 DataFrame 信息

图 9.4 – 客户级别的 DataFrame 信息

注意

继续执行es["items"].head()来显示包含产品的 DataFrame。您还可以使用pandasshape函数评估不同 DataFrame 的大小。您会注意到每个 DataFrame 中的行数与唯一发票、客户和产品的数量相匹配。

  1. 我们还可以如下显示这些数据表之间的关系:

    es.plot()
    

注意

要可视化数据关系,您需要安装graphviz。如果没有安装,请按照featuretools文档中的说明进行安装:featuretools.alteryx.com/en/stable/install.html#installing-graphviz

在以下输出中,我们可以看到关系数据集及其关系:

图 9.5 – 包含发票、客户和产品的表之间的关系

图 9.5 – 包含发票、客户和产品的表之间的关系

在输入数据和它们之间的关系后,我们可以开始自动为我们的新 DataFrame(即客户、发票和产品)创建特征,使用featuretools的默认参数。

  1. 让我们通过在客户级别聚合数据来创建特征。为此,我们设置featuretools,将customers指定为目标 DataFrame。在创建特征时,我们希望忽略具有唯一标识符的两个列:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        ignore_columns={
            «invoices»:[«invoice»],
            «invoices»:[«customer_id»],
        }
    )
    

注意

步骤 12 中的命令触发了 114 个具有不同客户级别数据聚合的特征的创建。feature_matrix 变量是一个包含特征值的 DataFrame,feature_defs 是一个包含新特征名称的列表。继续执行 feature_defs 或访问我们的配套 GitHub 仓库 (github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe1-Setting-up-an-entitity-set.ipynb) 来检查创建的特征名称。你将在 如何工作… 部分找到更多关于这些特征的信息。

  1. 由于空间限制,我们无法在书中打印出所有特征,因此,让我们显示五个创建特征的名称:

    feature_defs[5:10]
    

    在以下输出中,我们看到由 featuretools 创建的 114 个特征中的 5 个:

    [<Feature: MIN(data.price)>,
    <Feature: MIN(data.quantity)>,
    <Feature: MODE(data.description)>,
    <Feature: MODE(data.stock_code)>,
    <Feature: NUM_UNIQUE(data.description)>]
    

注意

featuretools 库使用创建它们的函数来命名新特征,然后是用于执行聚合的 DataFrame,最后是聚合变量名称。因此,MIN(data.quantity) 等同于 df.groupby(["customer_id"])["quantity"].min(),如果你熟悉 pandas。我们将在 如何工作… 部分提供更多细节。

  1. 让我们显示包含五个创建特征的 DataFrame 的前五行:

    feature_matrix[feature_matrix.columns[5:10]].head()
    

    在以下输出中,我们可以看到包含五个新特征值的前五行:

图 9.6 – 通过聚合客户级别的数据创建的五个特征的 DataFrame

图 9.6 – 通过聚合客户级别的数据创建的五个特征的 DataFrame

  1. 同样,我们可以通过在发票级别聚合信息来自动创建特征:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»invoices»,
        ignore_columns = {«data»: [«customer_id»]},
        max_depth = 1,
    )
    
  2. 上一步返回了 24 个特征——让我们显示它们的名称:

    feature_defs
    

    我们可以在以下输出中看到特征的名称:

    [<Feature: customer_id>,
    <Feature: COUNT(data)>,
    <Feature: MAX(data.price)>,
    <Feature: MAX(data.quantity)>,
    <Feature: MEAN(data.price)>,
    <Feature: MEAN(data.quantity)>,
    <Feature: MIN(data.price)>,
    <Feature: MIN(data.quantity)>,
    <Feature: MODE(data.description)>,
    <Feature: MODE(data.stock_code)>,
    <Feature: NUM_UNIQUE(data.description)>,
    <Feature: NUM_UNIQUE(data.stock_code)>,
    <Feature: SKEW(data.price)>,
    <Feature: SKEW(data.quantity)>,
    <Feature: STD(data.price)>,
    <Feature: STD(data.quantity)>,
    <Feature: SUM(data.price)>,
    <Feature: SUM(data.quantity)>,
    <Feature: DAY(first_data_time)>,
    <Feature: MONTH(first_data_time)>,
    <Feature: WEEKDAY(first_data_time)>,
    <Feature: YEAR(first_data_time)>]
    

注意

继续执行 feature_matrix.head() 来显示包含新特征的 DataFrame,或检查我们的配套 GitHub 仓库以查看结果。

总结一下,通过使用 步骤 16 中的代码并将目标 DataFrame 名称从 invoices 更改为 items,继续在产品级别自动创建特征。

如何工作...

在这个配方中,我们设置了一个包含数据和某些变量(唯一标识符)之间关系的实体集。之后,我们通过聚合数据集中每个唯一标识符的信息自动创建特征。我们使用了 featuretools 的两个主要类,EntitySetdfs,来创建特征。让我们更详细地讨论这些内容。

EntitySet 类存储数据、变量的逻辑类型以及变量之间的关系。变量类型(是否为数值或分类)由 featuretools 自动分配。我们也可以在将 DataFrame 添加到实体集时设置特定的变量类型。在 步骤 4 中,我们将数据添加到实体集并将 customer_id 的逻辑类型设置为 Categorical

注意

要检查 featuretools 推断的数据类型,您可以执行 es["data"].ww,其中 es 是实体集,data 是 DataFrame 的名称。

EntitySet 类具有 add_dataframe 方法,我们在 步骤 4 中使用此方法添加新的 DataFrame。在使用此方法时,我们需要指定唯一标识符,如果没有,则需要创建一个,就像我们在 步骤 4 中所做的那样,通过将 make_index 设置为 True。请注意,在 add_dataframeindex 参数中,我们传递了 "rows" 字符串。使用此配置,EntitySet 将包含每行的唯一标识符的 rows 列添加到 DataFrame 中,这是一个从 0 开始的新整数序列。

注意

而不是使用 add_dataframe 方法将 DataFrame 添加到实体集中,我们可以通过执行 es["df_name"]=df 来添加它,其中 "df_name" 是我们想要给 DataFrame 的名称,df 是我们想要添加的 DataFrame。

EntitySet 类具有 normalize_dataframe 方法,该方法用于从现有列的唯一值创建新的 DataFrame 和关系。此方法接受新 DataFrame 将关联的 DataFrame 的名称以及新 DataFrame 的名称。我们还需要在 index 参数中指明新 DataFrame 的唯一标识符。默认情况下,此方法创建一个包含唯一标识符的新 DataFrame,后面跟着一个包含每个唯一标识符首次注册的日期的 datetime 列。我们可以通过使用 copy_columns 参数向此 DataFrame 添加更多列,就像我们在 步骤 5 中所做的那样。向新 DataFrame 添加更多列对于如果我们想跟踪与该新 DataFrame 的关系是有用的,就像我们在 步骤 6 中所做的那样。

EntitySet 类还具有 plot() 方法,该方法显示实体集中的现有关系。在 图 9*.5* 中,我们看到了我们的数据表之间的关系;invoicesitems(产品)表与原始数据相关联,而 customers 表与 invoices 表相关联,而 invoices 表又与原始数据相关联。

注意

表之间的关系决定了特征的创建方式。invoicesitems表与原始数据相关。因此,我们只能创建深度为 1 的特征。另一方面,customers表与发票相关,而发票又与数据相关。因此,我们可以创建深度为 2 的特征。这意味着新特征将包括整个数据集的聚合或首先对发票进行聚合,然后对客户进行后续聚合。我们可以通过dfs中的max_depth参数来调节要创建的特征。

在设置好数据和关系后,我们使用了featuretools中的dfs来自动创建特征。在用dfs创建特征时,我们需要设置目标 DataFrame——即应该创建特征的对应数据表。dfs类通过所谓的转换聚合原语,通过转换和聚合现有变量来创建特征。

转换原语转换变量。例如,使用转换原语,dfs可以从日期时间变量中提取monthyeardayweek值。

聚合原语对唯一标识符的信息进行聚合。它使用诸如平均值、标准差、最大值和最小值、总和以及数值变量的偏度系数等数学运算。对于分类变量,聚合原语使用众数和唯一项目的计数。对于唯一标识符,它们计算出现的次数。

考虑到转换和聚合原语的功能,让我们尝试理解在这个配方中创建的特征。我们使用了dfs的默认参数来创建默认特征。

注意

想要了解更多关于featuretools返回的默认特征的信息,请访问featuretools.alteryx.com/en/stable/generated/featuretools.dfs.html#featuretools.dfs

我们首先为每位客户创建了特征。featuretools为每位客户返回了 114 个特征。由于customers数据与invoices数据相关,而invoices数据又与整个数据集相关,因此特征是通过在两个级别上聚合数据来创建的。首先,使用整个数据集对每个客户的数据进行聚合。接下来,首先对每个发票进行聚合,然后对预先聚合的数据再次进行聚合,以创建每个客户的特征。

featuretools 库使用用于聚合数据的函数来命名新特征 – 例如,COUNTMEANSTDSKEW 等。接下来,它使用用于聚合的数据,并跟随聚合的变量。例如,MEAN(data.quantity) 特征包含从整个数据集中计算出的客户购买物品的平均数量,这相当于如果你熟悉 pandas,则是 df.groupby("customer_id")["quantity"].mean()。另一方面,MEAN(invoices.MEAN(data.quantity)) 特征首先获取每个发票的物品平均数量 – 即 df.groupby("invoice")["quantity"].mean() – 然后从生成的序列中获取平均值,考虑特定客户的发票。

对于分类特征,featuretools 确定众数和唯一值。例如,从 description 变量中,我们得到了 NUM_UNIQUE(data.description)MODE(data.description) 特征。描述只是物品的名称。因此,这些特征突出了客户购买的独特物品数量以及客户购买次数最多的物品。

注意一些有趣的事情

在对分类特征进行聚合后,NUM_UNIQUE(data.description)MODE(data.description) 变量是数值型的。featuretools 库通过使用这些新创建变量的数值聚合来创建更多特征。这样,MAX(invoices.NUM_UNIQUE(data.description)) 特征首先找到每个发票的独特物品数量,然后返回特定客户这些值中的最大值,考虑所有客户的发票。

从日期时间特征中,featuretools 默认提取日期组件。记住,customers DataFrame 包含 customer_id 变量和每个客户的第一个发票日期,正如我们在 步骤 10 的输出中看到的。从这个日期时间特征中,featuretools 创建了包含不同日期部分的 DAY(first_invoices_time)MONTH(first_invoices_time)WEEKDAY(first_invoices_time)YEAR(first_invoices_time) 特征。

最后,featuretools 还返回了每个客户的发票总数(COUNT(invoices)) 和每客户的总行数(COUNT(data))。

参见

要了解更多关于 featuretools 的灵感来源,请查看 Kanter 和 Veeramachaneni 在 www.jmaxkanter.com/papers/DSAA_DSM_2015.pdf 发表的原始文章 Deep Feature Synthesis: Towards Automating Data Science Endeavors

使用通用和累积操作创建特征

featuretools 库使用所谓的 转换原语 来创建特征。转换原语将一个或多个数据集中的列作为输入,并返回一个或多个列作为输出。它们应用于一个 单个 DataFrame。

featuretools库根据它们执行的操作类型或修改的变量类型将转换原语分为各种类别。例如,通用转换原语应用数学运算,如平方根、正弦和余弦。累积转换原语通过比较一行值与前一行的值来创建新特征。例如,累积和、累积平均值、累积最小值和最大值以及行值之间的差异属于这一类别。还有一个可以应用于日期时间变量的累积转换,即自上次以来转换,它确定两个连续时间戳之间的时间流逝。

在这个配方中,我们将使用featuretools中的通用和累积转换原语来创建特征。

准备工作

当我们想要改变变量的分布,就像我们在第三章中看到的,转换数值变量时,变量转换,如平方根或对数是有用的。其他数学推导,如正弦和余弦,有助于捕捉潜在的数据模式,正如我们在第八章中描述的从周期性变量创建周期性特征配方中所述,创建新特征。从那些章节中描述的转换中,featuretools支持平方根和对数转换以及正弦和余弦(但不需要在 0 和 2π之间归一化)。

通过累积转换,例如,我们可以通过将发票级别的每一行的项目数量相加来获取每个发票购买的项目总数。为了理解我们将在此配方中创建的特征,让我们首先使用pandas创建它们:

  1. 让我们导入pandasnumpy

    import numpy as np
    import pandas as pd
    
  2. 让我们加载在技术 要求部分中描述的零售数据集:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们捕捉两个数值变量,价格数量,到一个列表中:

    numeric_vars = ["quantity", "price"]
    
  4. 让我们将累积函数的名称捕捉到一个列表中:

    func = ["cumsum", "cummax", "diff"]
    
  5. 让我们创建一个包含我们将创建的变量新名称的列表:

    new_names = [f"{var}_{function}"
        for function in func for var in numeric_vars]
    
  6. 让我们使用第 4 步中的累积函数,应用于第 3 步中的变量,并添加到 DataFrame 中:

    df[new_names] = df.groupby(
        "invoice")[numeric_vars].agg(func)
    

    上一步返回了每个发票内的累积和、累积最大值以及行之间的差异。一旦遇到新的发票编号,它就会重新开始。

  7. 让我们展示一个特定发票的原有和新特征:

    df[df["invoice"] == "489434" ][
        numeric_vars + new_names]
    

    在以下输出中,我们可以看到quantity_cumsum是数量的累积和,而price_diff是逐行后的价格差异:

图 9.7 – 显示在单个实体(发票)中应用于数值特征的累积函数的 DataFrame

图 9.7 - 显示对单个实体(发票)中的数值特征应用累积函数的 DataFrame

现在让我们将正弦和余弦转换应用于整个 DataFrame。

  1. 让我们创建一个新变量名称的列表:

    new_names = [
        f"{var}_{function}"
        for function in ["sin", "cos"]
        for var in numeric_vars]
    
  2. 让我们使用正弦和余弦函数转换价格和数量:

    df[new_names] = df[numeric_vars].agg(
        [np.sin, np.cos])
    

    步骤 9 中的转换被应用于整个数据集,无论发票号如何,这是可以的,因为它是从一行映射到同一行,而不是像累积函数那样从一行映射到下一行。您可以通过执行df[new_names].head()来检查结果。

现在我们已经了解了我们想要创建的特征类型,让我们使用featuretools自动化这个过程。

如何做到这一点...

我们将对每个发票应用累积转换,对整个数据集应用通用转换:

  1. 首先,我们将导入pandasfeaturetools以及Categorical逻辑类型:

    import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载技术要求部分中描述的数据集:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

    es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中:

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            "customer_id": Categorical,
            "invoice": Categorical,
        }
    )
    

注意

默认情况下,featuretools仅在生成的特征矩阵中保留分类、数值和布尔特征,该矩阵是在创建新特征之后生成的。invoice变量的类型没有被准确推断,所以如果我们想让featuretools在包含新特征的数据集中保留它,我们需要通过将其逻辑类型设置为我们在步骤 4 中做的那样来强制将其作为分类。要了解featuretools推断的数据类型,您可以执行es["data"].ww

  1. 让我们创建一个新的与步骤 4 中的 DataFrame 相关联的 DataFrame:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

关于步骤 4 和 5 的更多详细信息,请访问设置实体集和自动创建特征菜谱。

  1. 让我们列出我们将用于创建特征的累积转换:

    cum_primitives = [
        "cum_sum",
        "cum_max",
        "diff",
        "time_since_previous"]
    

注意

您可以在以下链接找到featuretools支持的累积转换:featuretools.alteryx.com/en/stable/api_reference.html#cumulative-transform-primitives

  1. 让我们列出要执行的一般转换:

    general_primitives = ["sine", " cosine "]
    

注意

您可以在以下链接找到featuretools支持的通用转换:featuretools.alteryx.com/en/stable/api_reference.html#general-transform-primitives

  1. 最后,让我们创建特征。我们使用dfs类,将原始 DataFrame 设置为目标 DataFrame——即我们将使用其变量作为新特征模板的那个 DataFrame。注意,我们向agg_primitives参数传递一个空列表;这是为了避免返回默认的聚合原语。我们将步骤 7 中的通用原语传递给trans_primitives参数,将步骤 6 中的累积原语传递给groupby_trans_primitives参数:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=general_primitives,
        groupby_trans_primitives = cum_primitives,
        ignore_dataframes = [«invoices»],
    )
    

注意

步骤 8 触发特征的创建,这可能会根据数据的大小、聚合级别数量以及要创建的特征数量而花费一些时间。你可以在创建它们之前检查输出特征,通过将features_only参数设置为True。这将仅返回特征名称;你可以检查它们,确保它们显示了你需要的内容,然后通过将参数设置回False来触发特征合成。

  1. 让我们现在显示创建的特征名称:

    feature_defs
    

    在以下输出中,我们看到我们创建的特征名称,包括价格和数量的正弦和余弦,以及按发票号分组后的这些变量的累积变换:

    [<Feature: customer_id>,
    <Feature: invoice>,
    <Feature: stock_code>,
    <Feature: description>,
    <Feature: quantity>,
    <Feature: price>,
    <Feature: COSINE(price)>,
    <Feature: COSINE(quantity)>,
    <Feature: SINE(price)>,
    <Feature: SINE(quantity)>,
    <Feature: CUM_MAX(price) by invoice>,
    <Feature: CUM_MAX(quantity) by invoice>,
    <Feature: CUM_SUM(price) by invoice>,
    <Feature: CUM_SUM(quantity) by invoice>,
    <Feature: DIFF(price) by invoice>,
    <Feature: DIFF(quantity) by invoice>,
    <Feature: TIME_SINCE_PREVIOUS(invoice_date) by invoice>]
    

注意

价格和数量的正弦和余弦变换可能不会增加太多价值,因为这些不是周期性特征。我保留这些变换在配方中,是为了向你展示如何在需要时应用变换原语。

如前所述列表所示,新特征作为新列附加到原始 DataFrame 中。你可以通过执行feature_matrix.head()来显示最终的 DataFrame:

图 9.8 – 深度特征合成产生的 DataFrame,包含原始变量和新特征

图 9.8 – 深度特征合成产生的 DataFrame,包含原始变量和新特征

关于创建的特征的更多详细信息,请查看*它是如何工作的…*部分。

它是如何工作的...

要使用featuretools创建一般和累积变换的特征,我们首先需要设置一个包含数据的实体集并定义其变量之间的关系。我们在设置实体集和自动创建特征配方中描述了如何设置实体集。

要应用累积和一般变换,我们使用了featuretools中的dfs类。一般变换应用于整个 DataFrame,而不按特定变量分组。要执行一般变换,我们将包含变换名称的字符串列表传递给dfstrans_primitives参数。

我们在按invoice分组后应用了累积变换。为此,我们将包含累积变换名称的字符串列表传递给dfsgroupby_trans_primitives参数。featuretools库知道应该按发票分组,因为我们通过使用EntitySet中的normalize_dataframe方法在步骤 5中建立了这个唯一标识符。

最后,我们不想从invoicesDataFrame 中的变量创建特征;因此,我们将dfs设置为通过设置ignore_dataframes = ["``invoices"]来忽略此 DataFrame。

dfs类返回了两个变量,包含原始和新特征的 DataFrame 以及一个包含特征名称的列表。新特征以创建它们的操作命名,例如SINECOSINECUM_MAXDIFF,后跟应用转换的变量,以及当适用时用于分组的变量。

注意,featuretools会自动识别并选择应用转换的变量。正弦、余弦、累计总和、最大值和差分被应用于数值变量,而time_since_previous转换被应用于日期时间变量。

结合数值特征

第八章 创建新特征中,我们看到了我们可以通过使用数学运算结合变量来创建新特征。featuretools库支持多种结合变量的操作,包括加法、除法、取模和乘法。在本食谱中,我们将学习如何使用featuretools结合这些特征。

如何操作...

让我们从导入库和准备数据集开始:

  1. 首先,我们将导入pandasfeaturetoolsCategorical逻辑类型:

    import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载在技术 要求部分中描述的数据集:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

    es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中:

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={«customer_id»: Categorical},
    )
    
  5. 让我们创建一个与步骤 4中的 DataFrame 相关联的新 DataFrame:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

关于步骤 4步骤 5的更多详细信息,请访问设置实体集和自动创建特征 食谱

  1. 我们将乘以quantityprice变量,分别反映购买的商品数量和单价,以获得总支付金额:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=[«multiply_numeric»],
        primitive_options={
            («multiply_numeric»): {
                ‹include_columns›: {
                    'data': ["quantity", "price"]
                }
            }
        },
        ignore_dataframes=[«invoices»],
    )
    

注意

我们将agg_primitives设置为空列表,以避免创建默认的原始操作。

  1. 现在我们来显示新特征的名称:

    feature_defs
    

    在以下输出中,我们可以看到特征名称,其中最后一个对应于pricequantity变量的组合:

    [<Feature: customer_id>,
    <Feature: stock_code>,
    <Feature: description>,
    <Feature: quantity>,
    <Feature: price>,
    <Feature: price * quantity>]
    
  2. 最后,让我们检查在步骤 6中创建的新 DataFrame:

    feature_matrix.head()
    

    在以下输出中,我们可以看到新特征被附加到原始 DataFrame 的右侧:

图 9.9 – 由价格与数量乘积产生的新特征的 DataFrame

图 9.9 – 由价格与数量乘积产生的新特征的 DataFrame

featuretools结合特征可能比df["price"].mul(df["quantity"])pandas功能要复杂得多。真正的力量在于我们以这种方式创建新特征并随后在发票或客户级别进行聚合时。我们将在使用聚合 原始操作创建特征*的食谱中讨论聚合函数。

它是如何工作的...

为了乘法特征,我们使用了来自 featuretoolsMultiplyNumeric 原始操作,它可以通过 dfs 使用 multiply_numeric 字符串访问。我们将前一个字符串传递给 trans_primitive 参数,然后使用 primitive_options 参数指定要乘法的变量。请注意,此外,我们还向 agg_primitives 参数传递了一个空列表,以避免返回默认的聚合原始操作,并且我们忽略了来自 invoices DataFrame 的特征。

要查看允许您组合变量的其他函数,请访问 featuretools.alteryx.com/en/stable/api_reference.html#binary-transform-primitives。在撰写本文时,我注意到 MultiplyNumericDivideNumeric 不在文档中。您始终可以通过检查源代码来双检查哪些函数受支持:github.com/alteryx/featuretools/tree/main/featuretools/primitives/standard/transform/binary。您还可以通过在设置实体集及其关系后运行以下命令来检查您可以在数据上执行哪些操作:ft.get_valid_primitives(es, target_dataframe_name="data", max_depth=2)。在这里,es 是由 步骤 5 得到的实体集。

从日期和时间中提取特征

第六章 中,从日期和时间变量中提取特征,我们讨论了如何通过从日期和时间变量的日期和时间部分提取特征来丰富我们的数据集,例如年份、月份、星期几、小时等等。我们可以利用 featuretools 自动提取这些特征。

featuretools 库支持使用其 日期时间转换原始操作 创建来自日期时间变量的各种特征。这些原始操作包括常见的变量,如年份、月份和日期,以及其他特征,如 是否是午餐时间是否是工作日。此外,我们可以提取表示日期是否为联邦假日或银行假日(如英国所称)的特征,或者确定到特定日期的时间距离的特征。对于零售公司来说,圣诞节、黑色星期五或节礼日等日期的邻近性通常意味着销售额的增加,如果他们正在预测需求,这些将是有用的变量。

注意

有关可以创建的日期时间变量特征的更多详细信息,请访问 featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives

在这个菜谱中,我们将使用 featuretools 自动从日期时间变量创建多个特征。

如何做...

让我们从导入库和准备数据集开始:

  1. 首先,我们将导入pandasfeaturetools和一些特殊的 datetime 原始函数:

    import pandas as pd
    import featuretools as ft
    from featuretools.primitives import (
        IsFederalHoliday, DistanceToHoliday)
    from woodwork.logical_types import Categorical
    
  2. 让我们加载技术要求部分描述的数据集:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

    es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中:

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={«customer_id»: Categorical},
    )
    
  5. 让我们创建一个新的 DataFrame,它与步骤 4中的 DataFrame 有关联:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

更多关于步骤 4步骤 5的详细信息,请访问设置实体集和自动创建特征配方。

  1. 让我们创建一个返回布尔向量的原始函数,指示日期是否与英国的银行假日(即非工作日)相符(即,非工作日):

    is_bank_hol = IsFederalHoliday(country="UK")
    

注意

在设置用于确定银行假日的原始函数时,选择正确的国家非常重要。要查看支持的国家列表,请访问github.com/dr-prodigy/python-holidays#available-countries

  1. 让我们检查这个原始函数中包含哪些银行假日:

    hols = is_bank_hol.holidayUtil.federal_holidays.values()
    available_hols = list(set(hols))
    

    如果我们执行available_hols,我们将看到支持英国银行假日的列表:

    ['May Day',
     'Good Friday',
     'Wedding of William and Catherine',
     'Coronation of Charles III',
     'Christmas Day',
     'Wedding of Charles and Diana',
     'Christmas Day (observed)',
     'State Funeral of Queen Elizabeth II',
     'Silver Jubilee of Elizabeth II',
     'Spring Bank Holiday',
     'Diamond Jubilee of Elizabeth II',
     'Boxing Day (observed)',
     'Platinum Jubilee of Elizabeth II',
     "New Year's Day (observed)",
     'Boxing Day',
     'Golden Jubilee of Elizabeth II',
     'Millennium Celebrations',
     "New Year's Day"]
    
  2. 让我们创建另一个原始函数,用于确定到特定日期的天数 – 在这种情况下,到节礼日的距离:

    days_to_boxing = DistanceToHoliday(
        holiday="Boxing Day", country="UK")
    
  3. 现在,让我们创建一个包含字符串的列表,这些字符串可以识别从datetime中获取的常见特征,并包括步骤 6步骤 8中的原始函数:

    date_primitives = [
        "day", "year", "month", "weekday",
        "days_in_month", "part_of_day",
        "hour", "minute",
        is_bank_hol,
        days_to_boxing
    ]
    
  4. 让我们现在根据invoice_date日期变量从步骤 9创建日期和时间特征:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»invoices»,
        agg_primitives=[],
        trans_primitives=date_primitives,
    )
    

注意

步骤 4中,我们将invoice_date变量作为时间变量输入。因此,featuretools将使用此变量来创建日期和时间相关特征。

  1. 让我们显示创建的特征的名称:

    feature_defs
    

    在以下输出中,我们看到原始特征和时间特征的名字:

    [<Feature: customer_id>,
    <Feature: DAY(first_data_time)>,
    <Feature: DAYS_IN_MONTH(first_data_time)>,
    <Feature: DISTANCE_TO_HOLIDAY(
         first_data_time, holiday=Boxing Day, country=UK)>,
    <Feature: HOUR(first_data_time)>,
    <Feature: IS_FEDERAL_HOLIDAY(
         first_data_time, , country=UK)>,
    <Feature: MINUTE(first_data_time)>,
    <Feature: MONTH(first_data_time)>,
    <Feature: PART_OF_DAY(first_data_time)>,
    <Feature: WEEKDAY(first_data_time)>,
    feature_matrix.head() to take a look at the resulting DataFrame with the features created from the invoice date. The DataFrame is quite big, so for reasons of space, we’ll only display a few columns in the book.
    
  2. 让我们显示包含三个新特征的 DataFrame 的结果:

    columns = [
        "DISTANCE_TO_HOLIDAY(first_data_time,
            holiday=Boxing Day, country=UK)",
        "HOUR(first_data_time)",
        "IS_FEDERAL_HOLIDAY(first_data_time,
            country=UK)",
    ]
    feature_matrix[columns].head()
    

    在以下输出中,我们看到包含新特征的 DataFrame:

图 9.10 – 从 datetime 派生的一些 DataFrame 特征

图 9.10 – 从 datetime 派生的一些 DataFrame 特征

注意到一些创建的特征是数值型的,例如HOURDAY,一些是布尔型的,例如IS_FEDERAL_HOLIDAY,还有一些是分类型的,例如PART_OF_DAY。要查看PART_OF_DAY的值,请执行feature_matrix["PART_OF_DAY(first_data_time)"].unique()

它是如何工作的...

要从日期时间变量中创建特征,我们使用了 featuretools 的日期时间转换原语 (featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives)。这些原语可以通过 dfs 使用我们在 步骤 69 中指定的字符串和函数通过 trans_primitive 参数访问。请注意,此外,我们还向 agg_primitives 参数传递了一个空列表,以避免返回应用于我们的日期时间特征的默认聚合原语。我们还忽略了来自 invoices DataFrame 的特征。

注意

我们将 agg_primitives 设置为空列表,并忽略了 invoices DataFrame,以保持输出简单并能够专注于日期时间特征。然而,请注意,featuretools 的真正威力在于从 datetime 创建原语,然后在不同的实体级别进一步聚合它们。

从文本中提取特征

第十一章 从文本变量中提取特征 中,我们将讨论我们可以利用 pandasscikit-learn 从文本片段中提取的各种特征。我们还可以通过利用 featuretools 自动从文本中提取多个特征。

featuretools 库支持创建多个基本特征作为其默认功能的一部分,例如文本中的字符数、单词数、每个单词的平均字符数以及文本片段中的中位词长等。

注意

要查看默认文本原语的全列表,请访问 featuretools.alteryx.com/en/stable/api_reference.html#naturallanguage-transform-primitives

此外,还有一个配套的 Python 库,nlp_primitives,其中包含额外的原语,用于基于 NLP 创建更高级的特征。在这些函数中,我们发现了一些用于确定多样性得分、极性得分或停用词计数的原语。

注意

在编写本文档时,没有关于 nlp_primitives 库支持的原始语法的文档,因此要了解更多信息,您需要检查源代码:github.com/alteryx/nlp_primitives/tree/6243ef2379501bfec2c3f19e35a30b5954605e57/nlp_primitives

在这个配方中,我们将首先利用 featuretools 的默认功能从文本变量中创建多个特征,然后突出显示如何使用 nlp_primitives 库中的原语。

准备工作

要跟随这个配方,您需要安装 nlp_primitives 库,您可以使用 pip 来完成:

pip install nlp_primitives

否则,您可以使用 conda

conda install -c conda-forge nlp-primitives

注意

更多详情,请访问 nlp_primitives GitHub 仓库:github.com/alteryx/nlp_primitives

如何做到这一点...

让我们先导入库并准备好数据集:

  1. 首先,我们将导入 pandasfeaturetools 和逻辑类型:

    import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import (
       Categorical, NaturalLanguage)
    
  2. 让我们将第 技术 要求 部分中描述的数据集加载进来:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

    es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中,并突出显示 description 变量是一个文本变量:

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            «customer_id»: Categorical,
            "invoice": Categorical,
            «description»: NaturalLanguage,
        }
    )
    

注意

为了使 featuretools 库的文本原语正常工作,我们需要通过使用 NaturalLanguage 逻辑类型来指示哪些变量是文本。

  1. 让我们创建一个新的 DataFrame,它与第 4 步中的 DataFrame 有关系:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

更多关于 步骤 45 的详情,请访问 设置实体集和自动创建特征 的配方。

  1. 让我们创建一个字符串列表,对应于我们想要创建的文本特征:

    text_primitives = [
        "num_words",
        "num_characters",
        "MeanCharactersPerWord" ,
        "PunctuationCount"]
    
  2. 现在我们从 description 变量中提取文本特征:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=text_primitives,
        ignore_dataframes=[«invoices»],
    )
    
  3. 让我们显示创建的特征的名称:

    feature_defs
    

    在以下输出中,我们看到原始特征名称,然后是来自 description 变量的创建特征:

    [<Feature: customer_id>,
    <Feature: invoice>,
    <Feature: stock_code>,
    <Feature: quantity>,
    <Feature: price>,
    <Feature: MEAN_CHARACTERS_PER_WORD(description)>,
    <Feature: NUM_CHARACTERS(description)>,
    <Feature: NUM_WORDS(description)>,
    feature_matrix.head().
    
  4. 让我们显示包含文本派生特征的 DataFrame 的一个切片:

    text_f = [
         "NUM_CHARACTERS(description)",
         "NUM_WORDS(description)",
         "PUNCTUATION_COUNT(description)",
    ]
    feature_matrix[text_f].head()
    

    在以下输出中,我们看到一个包含从文本创建的特征的 DataFrame:

图 9.11 – 从文本创建的特征 DataFrame

图 9.11 – 从文本创建的特征 DataFrame

注意

featuretools 库移除了原始文本变量 description,并返回了新的特征。

要使用 nlp_primitives 包中的原语创建特征,您需要首先导入它们 – 例如,通过执行 from nlp_primitives import DiversityScore – 然后将原语添加到我们在第 6 步中创建的文本原语列表中。请注意,这些是复杂函数,因此创建特征可能需要一些时间。

它是如何工作的...

要从文本变量创建特征,我们使用了 featuretools 的默认文本原语。这些原语可以通过 dfs 通过传递一个字符串列表来访问,这些字符串对应于原语名称,例如第 6 步中的那些,传递给 trans_primitives 参数。

对于更高级的原语,您需要从 nlp_primitives 库中导入原语函数,然后将它们传递给 dfstrans_primitives 参数。这样,dfs 就可以利用这些原语的功能从文本中创建新的特征。nlp_primitives 库在底层使用 nltk Python 库。

使用聚合原语创建特征

在本章中,我们通过将现有变量映射到新特征中,通过各种函数自动创建了特征。例如,我们从日期时间变量中提取日期和时间部分,计算文本中的单词、字符和标点符号的数量,将数值特征组合成新变量,并使用正弦和余弦等函数转换特征。为了创建这些特征,我们使用了转换原语。

featuretools 库也支持与发票相关的 price,一个聚合原语将取单个发票的所有价格观测值并返回一个单一值,例如该发票的平均价格或总和(即支付的总金额)。

注意

featuretools 的聚合功能相当于 pandas 中的 groupby,随后是 pandas 函数如 meansumstdcount 等。

一些聚合原语与数值变量一起工作,例如平均值、总和、最大值和最小值。其他聚合原语是特定于分类变量的,例如唯一值的数量和最频繁的值(众数)。

注意

要获取支持的聚合原语的完整列表,请访问 featuretools.alteryx.com/en/stable/api_reference.html#aggregation-primitives

在这个菜谱中,我们首先将通过聚合现有变量来创建多个特征。之后,我们将结合使用转换和聚合原语,以突出 featuretools 的真正威力。

准备工作

在这个菜谱中,我们将使用来自 UCI 机器学习仓库的 Online Retail II 数据集。这个数据集包含有关产品(项目)、发票和客户的信息。为了跟随这个菜谱,了解这些实体的性质及其关系,以及如何正确设置 featuretools 的实体集,非常重要,这些我们在 设置实体集和自动创建特征 菜谱中已经描述过。在继续下一部分之前,请确保你已经查看了那个菜谱。

如何做到这一点...

让我们先导入库并准备数据集:

  1. 首先,我们将导入 pandasfeaturetools 和逻辑类型:

    import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import (
        Categorical, NaturalLanguage)
    
  2. 让我们加载 技术 要求 部分中描述的数据集:

    df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

    es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中,并突出显示 description 变量是一个文本变量,customer_id 是分类变量,而 invoice_date 是一个日期时间特征:

    es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            «customer_id»: Categorical,
            «description»: NaturalLanguage,
        }
    )
    
  5. 让我们创建一个新的 DataFrame,它与 步骤 4 中的 DataFrame 有关系:

    es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    
  6. 现在,我们添加第二个关系,即客户与发票之间的关系。为此,我们指出基础 DataFrame,我们在 步骤 5 中将其称为 invoices,我们给新的 DataFrame 起个名字,customers,并添加一个唯一的客户标识符:

    es.normalize_dataframe(
        base_dataframe_name=»invoices»,
        new_dataframe_name=»customers»,
        index=»customer_id»,
    )
    

注意

更多关于步骤 4步骤 5的详细信息,请访问设置实体集和自动创建特征配方。

  1. 让我们创建一个字符串名称列表,以标识我们想要使用的聚合原语:

    agg_primitives = ["mean", "max", "min", "sum"]
    
  2. 让我们通过在客户级别聚合数据来创建特征。为此,我们设置featuretools中的dfs类,将customers指定为目标 DataFrame,并将步骤 7中的聚合原语传递给trans_primitives参数,同时传递一个空列表以防止dfs返回默认的转换:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        agg_primitives=agg_primitives,
        trans_primitives=[],
    )
    
  3. 让我们显示创建的特征名称:

    feature_defs
    

    在以下输出中,我们可以看到在客户级别聚合的特征名称:

    [<Feature: MAX(data.price)>,
    <Feature: MAX(data.quantity)>,
    <Feature: MEAN(data.price)>,
    <Feature: MEAN(data.quantity)>,
    <Feature: MIN(data.price)>,
    <Feature: MIN(data.quantity)>,
    <Feature: SUM(data.price)>,
    <Feature: SUM(data.quantity)>,
    <Feature: MAX(invoices.MEAN(data.price))>,
    <Feature: MAX(invoices.MEAN(data.quantity))>,
    <Feature: MAX(invoices.MIN(data.price))>,
    <Feature: MAX(invoices.MIN(data.quantity))>,
    <Feature: MAX(invoices.SUM(data.price))>,
    <Feature: MAX(invoices.SUM(data.quantity))>,
    <Feature: MEAN(invoices.MAX(data.price))>,
    <Feature: MEAN(invoices.MAX(data.quantity))>,
    <Feature: MEAN(invoices.MEAN(data.price))>,
    <Feature: MEAN(invoices.MEAN(data.quantity))>,
    <Feature: MEAN(invoices.MIN(data.price))>,
    <Feature: MEAN(invoices.MIN(data.quantity))>,
    <Feature: MEAN(invoices.SUM(data.price))>,
    <Feature: MEAN(invoices.SUM(data.quantity))>,
    <Feature: MIN(invoices.MAX(data.price))>,
    <Feature: MIN(invoices.MAX(data.quantity))>,
    <Feature: MIN(invoices.MEAN(data.price))>,
    <Feature: MIN(invoices.MEAN(data.quantity))>,
    <Feature: MIN(invoices.SUM(data.price))>,
    <Feature: MIN(invoices.SUM(data.quantity))>,
    <Feature: SUM(invoices.MAX(data.price))>,
    <Feature: SUM(invoices.MAX(data.quantity))>,
    <Feature: SUM(invoices.MEAN(data.price))>,
    <Feature: SUM(invoices.MEAN(data.quantity))>,
    <Feature: SUM(invoices.MIN(data.price))>,
    <Feature: SUM(invoices.MIN(data.quantity))>]
    

注意

请记住,featuretools使用创建它们的函数来命名特征,然后是用于计算的 DataFrame,最后是用于计算的变量。因此,MAX(data.price)是每个客户在数据集中看到的最高价格。另一方面,MEAN(invoices.MAX(data.price))是每个客户在特定发票中观察到的所有最高价格的平均值。也就是说,如果一个客户有六张发票,我们首先找到六张发票中的每张的最高价格,然后取这些值的平均值。

  1. 现在让我们显示包含原始数据和新特征的 DataFrame 的结果:

    feature_matrix.head()
    

    在以下输出中,我们可以看到dfs返回的 DataFrame 中的一些变量:

图 9.12 – 在客户级别聚合的一些特征结果的 DataFrame

图 9.12 – 在客户级别聚合的一些特征结果的 DataFrame

由于空间限制,我们无法显示步骤 10的整个输出,所以请确保你在你的电脑上执行它,或者访问我们的配套 GitHub 仓库以获取更多详细信息:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe6-Creating-features-with-aggregation-primitives.ipynb.

为了跟进,让我们结合从使用转换原语和本配方中的聚合函数中学习到的内容。首先,我们将从现有的日期时间和文本变量中创建新特征;然后,我们将这些特征与数值变量一起在客户级别进行聚合。

  1. 让我们创建日期和文本原语列表:

    trans_primitives = ["month", "weekday", "num_words"]
    
  2. 让我们创建一个包含聚合原语的列表:

    agg_primitives = ["mean"]
    
  3. 现在让我们通过转换和聚合变量来自动创建特征:

    feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        agg_primitives=agg_primitives,
        trans_primitives=trans_primitives,
        max_depth=3,
    )
    

    步骤 13中的代码触发了特征的创建以及它们在客户级别的后续聚合。

  4. 让我们显示新特征的名称:

    feature_defs
    

    在以下输出中,我们可以看到创建的变量的名称:

    [<Feature: MEAN(data.price)>,
    <Feature: MEAN(data.quantity)>,
    <Feature: MONTH(first_invoices_time)>,
    <Feature: WEEKDAY(first_invoices_time)>,
    <Feature: MEAN(invoices.MEAN(data.price))>,
    <Feature: MEAN(invoices.MEAN(data.quantity))>,
    <Feature: MEAN(data.NUM_WORDS(description))>,
    <Feature: MEAN(invoices.MEAN(data.NUM_
        WORDS(description)))>] WORDS(description)))>]
    

注意,在我们的配方中,由于空间限制,我们尽量减少特征的创建,但你可以创建尽可能多的特征,并使用 featuretools 内置的功能显著丰富你的数据集。

它是如何工作的...

在这个配方中,我们将使用转换原语创建特征的方法与使用聚合原语创建特征的方法结合起来,这些方法我们在本章中进行了讨论。

要使用 featuretools 自动创建特征,我们首先需要将数据输入到实体集中,并建立数据之间的关系。我们在 设置实体集和自动创建特征 的配方中讨论了如何设置实体集。

为了聚合现有特征,我们使用了 dfs 类。我们创建了一个包含对应聚合原语字符串的列表,并将其传递给 dfsagg_primitives 参数。为了聚合现有变量而不创建新特征,我们将一个空列表传递给 dfstrans_primitives 参数。

customers DataFrame 是 invoice DataFrame 的子集,而 invoice DataFrame 又是原始数据的子集。因此,dfs 从原始数据和每个发票的预聚合数据中创建了聚合。因此,MEAN(data.price) 特征由客户购买的商品的平均价格组成,这个价格是通过整个数据计算得出的,而 MEAN(invoices.MEAN(data.price)) 首先计算每个发票的平均价格,然后取这些值的平均值来计算客户的价格。因此,如果一个客户有五张发票,featuretools 首先计算每张发票支付的平均价格,然后取这些值的平均值。因此,MEAN(data.price)MEAN(invoices.MEAN(data.price)) 不是同一个特征。

注意

聚合原语对唯一标识符的信息进行聚合。聚合原语使用数学运算,如均值、标准差、最大值和最小值、总和以及数值变量的偏度系数。对于分类变量,聚合原语使用众数和唯一项的计数。对于唯一标识符,聚合原语计算出现的次数。

接下来,我们将新特征的创建与聚合相结合。为此,我们将对应转换原语的字符串列表传递给 dfstrans_primitives 参数,并将对应聚合原语的字符串列表传递给 dfsagg_primitives 参数。

步骤 13 的一个输出是一个新特性的列表。从这些特性中,我们可以识别出从每个客户的第一个发票日期创建的特性,例如 MONTH(first_invoices_time)WEEKDAY(first_invoices_time)。我们还可以看到从文本特性中聚合而来的特性,例如 MEAN(data.NUM_WORDS(description))MEAN(invoices.MEAN(data.NUM_WORDS(description)))。最后,我们还可以看到现有数值变量的聚合,例如 MEAN(data.price)MEAN(invoices.MEAN(data.price))

注意

如果你想要将转换和聚合原语应用于特定的变量,你可以通过指定如这里所讨论的原语选项来实现:docs.featuretools.com/en/stable/guides/specifying_primitive_options.html

第十章:使用 tsfresh 从时间序列创建特征

在本书的整个过程中,我们讨论了针对表格和关系数据集定制的特征工程方法和工具。在本章中,我们将把我们的重点转向时间序列数据。时间序列是在时间上按顺序连续观察到的观察值的序列。例如包括能源生产和需求、温度、空气污染物浓度、股价和销售收入。这些示例中的每一个都代表一个变量,它们的值随时间变化。

便宜且能够测量运动、移动、湿度、葡萄糖和其他参数的传感器的广泛应用,显著增加了时间标注数据的数量。这些时间序列可以用于各种分类任务。例如,通过分析在特定时间间隔内家庭的电力使用模式,我们可以推断出是否使用了特定的电器。同样,超声波传感器的信号可以帮助确定(气体)管道故障的概率,而声音波长的特征可以帮助预测听众是否会喜欢一首歌。时间序列数据对于回归任务也很有价值。例如,机械设备传感器的信号可以用来预测设备的剩余使用寿命。

要使用时间序列与传统的监督机器学习模型,如线性回归、逻辑回归或基于决策树的算法,我们需要将每个时间序列映射到一个定义良好的特征向量,以捕捉其特征。时间序列模式,包括趋势、季节性和周期性等,可以通过简单和复杂的数学运算的组合来捕捉。简单的计算包括,例如,计算时间序列的平均值和标准差。更复杂的方法包括确定相关性或熵,例如。此外,我们可以应用非线性时间序列分析函数来分解时间序列信号,例如傅里叶变换或小波变换,并使用这些函数的参数作为监督模型的特征。

从时间序列中创建特征可能非常耗时;我们需要应用各种信号处理和时间序列分析算法来识别和提取有意义的特征。tsfresh Python 包,代表 tsfresh,包含一个特征选择算法,该算法可以识别给定时间序列的最具预测性的特征。通过自动化复杂时间序列方法的应用,tsfresh弥合了信号处理专家和机器学习实践者之间的差距,使得从时间序列数据中提取有价值特征变得更加容易。

在本章中,我们将学习如何通过利用 tsfresh 自动从时间序列数据中创建数百个特征。随后,我们将讨论如何通过选择最相关的特征、从不同的时间序列中提取不同的特征以及将特征创建过程集成到 scikit-learn 流程中来对时间序列数据进行分类。

在本章中,我们将介绍以下食谱:

  • 从时间序列中自动提取数百个特征

  • 从时间序列数据中自动创建和选择预测特征

  • 从不同的时间序列中提取不同的特征

  • 通过特征选择创建特征子集

  • 将特征创建嵌入到 scikit-learn 流程中

技术要求

在本章中,我们将使用开源的 tsfresh Python 库。您可以通过执行 pip install tsfresh 来使用 pip 安装 tsfresh

注意

如果您使用的是旧版 Microsoft 操作系统,您可能需要更新 Microsoft C++ 编译工具,以便继续安装 tsfresh 包。请按照此线程中的步骤进行操作:stackoverflow.com/questions/64261546/how-to-solve-error-microsoft-visual-c-14-0-or-greater-is-required-when-inst

我们将使用来自 UCI 机器学习仓库的 Occupancy Detection 数据集,该数据集可在 archive.ics.uci.edu/ml/datasets/Occupancy+Detection 找到,并受 Creative Commons Attribution 4.0 国际 (CC BY 4.0) 许可协议的许可:creativecommons.org/licenses/by/4.0/legalcode。该数据的相应引用如下:

Candanedo, Luis. (2016). Occupancy Detection. UCI Machine Learning Repository. doi.org/10.24432/C5X01N

我已下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb

要获取修改后的数据集和目标变量,请查看以下链接中的文件 occupancy.csvoccupancy_target.csvgithub.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh

占用检测数据集包含在每分钟间隔内采集了 135 小时的时间序列数据。变量测量了温度、湿度、<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 级别和办公室的能耗。使用摄像头录像来确定是否有人在办公室。目标变量显示办公室在任意一小时是否被占用。如果目标值为 1,则表示该小时办公室被占用;否则,值为 0

时间序列数据集和目标变量数据集的行数不同。时间序列数据集包含每分钟间隔的 135 小时记录,即 8,100 行。目标变量只有 135 行,每个标签指示在 135 小时中的每个小时办公室是否被占用。

注意

查看本书 GitHub 仓库中的笔记本,以熟悉数据集的不同时间序列的绘图:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb

从时间序列中自动提取数百个特征

时间序列是按时间顺序索引的数据点。分析时间序列序列使我们能够做出各种预测。例如,传感器数据可以用来预测管道故障,声音数据可以帮助识别音乐类型,健康历史或个人测量,如血糖水平,可以指示一个人是否生病,正如我们将在这个菜谱中展示的,光照使用模式、湿度和<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 级别可以确定办公室是否被占用。

使用传统的机器学习算法,如线性回归或随机森林来训练回归和分类模型,我们需要一个大小为 M x N 的数据集,其中 M 是行数,N 是特征数或列数。然而,对于时间序列数据,我们拥有的是 M 个时间序列的集合,每个时间序列都有多个按时间顺序索引的行。要在监督学习模型中使用时间序列,每个时间序列都需要映射到一个定义良好的特征向量 N,如下面的图所示:

图 10.1 – 显示从时间序列创建特征过程以进行分类或回归的图

图 10.1 – 显示从时间序列创建特征以进行分类或回归的过程的图表

这些特征向量,如图 10.1.1 所示,表示为<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:math><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>,和<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub></mml:math>,应该捕捉时间序列的特征。例如,<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:math>可能是时间序列的均值,而<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>是其方差。我们可以创建许多特征来描述数据点的分布、相关性、平稳性或熵等时间序列的特性。因此,特征向量 N 可以通过应用一系列特征化方法来构建,这些方法以时间序列作为输入,并返回一个或多个标量作为输出。均值或总和将时间序列序列作为输入,并返回一个标量作为输出,即时间序列的均值或其值的总和。我们还可以将线性趋势拟合到时间序列序列中,这将返回两个标量——一个表示斜率,另一个表示截距。

tsfresh对时间序列应用 63 种特征化方法,每种方法返回一个或多个标量,因此对于任何给定的时间序列,都会产生超过 750 个特征。在本配方中,我们将使用tsfresh将时间序列数据转换为 M x N 特征表,然后我们将使用该特征表来预测办公室占用情况。

准备工作

在本食谱中,我们将使用技术要求部分中描述的占用检测数据集。此数据集包含在办公室内每分钟间隔的温度、湿度、<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>水平和照明消耗的测量值。共有 135 小时的测量数据,每小时都有一个唯一的标识符。还有一个包含目标变量的数据集,该变量指示在 135 小时中的哪一小时办公室是有人使用的。让我们加载数据并绘制一些图表来了解其模式:

  1. 让我们加载pandasmatplotlib

    import matplotlib.pyplot as plt
    import pandas as pd
    
  2. 加载数据集并显示前五行:

    X = pd.read_csv(
        "occupancy.csv", parse_dates=["date"])
    X.head()
    

    在以下图中,我们可以看到包含唯一标识符的数据集,随后是测量日期和时间以及五个时间序列的值,这些时间序列捕捉了温度、湿度、灯光和<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>水平:

图 10.2 – 包含时间序列数据的 DataFrame

图 10.2 – 包含时间序列数据的 DataFrame

  1. 让我们创建一个函数来绘制给定小时(id列是 135 小时记录中每一小时的唯一标识符)的步骤 2中的时间序列图:

    def plot_timeseries(n_id):
        fig, axes = plt.subplots(nrows=2, ncols=3,
        figsize=(20, 10))
        X[X[«id»] == n_id]["temperature"].plot(
            ax=axes[0, 0], title="temperature")
        X[X[«id»] == n_id]["humidity"].plot(
            ax=axes[0, 1], title="humidity")
        X[X[«id»] == n_id]["light"].plot(
            ax=axes[0, 2], title="light")
        X[X[«id»] == n_id]["co2"].plot(
        ax=axes[1, 0], title="co2")
        X[X[«id»] == n_id]["humidity_ratio"].plot(
            ax=axes[1,1], title="humidity_ratio")
        plt.show()
    
  2. 让我们绘制一个小时办公室无人使用时的时间序列图:

    plot_timeseries(2)
    

    在以下图中,我们可以看到记录的第二小时的时间序列值,当时办公室是空的:

图 10.3 – 数据收集第二小时办公室空时的时间序列值

图 10.3 – 数据收集第二小时办公室空时的时间序列值

注意到灯光是关闭的,这就是为什么我们在右上角light消耗的图表中看到一条平坦的直线。

  1. 现在,让我们绘制一个小时办公室有人使用时的时间序列数据:

    plot_timeseries(15)
    

    在以下图中,我们可以看到记录的第十五小时的时间序列值,当时办公室是有人使用的:

图 10.4 – 数据收集第十五小时办公室有人使用时的时间序列值

图 10.4 – 数据收集第十五小时办公室有人使用时的时间序列值

注意这次灯光是开着的(右上角面板)。

在这个菜谱中,我们将从这些每个时间序列数据的一小时窗口中提取特征,捕捉它们特性的各个方面。从每个这些 60 分钟的时间序列段中,我们将使用tsfresh自动生成超过 750 个特征,确保数据的属性得到全面表示。

如何实现...

我们将首先自动从单个时间序列lights创建数百个特征,然后使用这些特征来预测办公室在任意给定小时是否被占用:

  1. 让我们导入所需的 Python 库和函数:

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh import extract_features
    from tsfresh.utilities.dataframe_functions import (
        impute
    )
    
  2. 加载技术要求部分中描述的数据集:

    X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    
  3. 将目标变量加载到pandas Series 中:

    y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    
  4. 让我们使用tsfresh为每个小时的能耗记录自动创建数百个特征。要从light变量创建特征,我们将包含此变量的 DataFrame 和每个时间序列的唯一标识符传递给tsfreshextract_features函数:

    features = extract_features(
        X[[«id», «light»]], column_id="id")
    

    如果我们执行features.shape,我们会得到(135, 789),这对应于结果 DataFrame 的大小,其中每一行代表一个小时的记录,每一列代表由tsfresh创建的一个特征。有 789 个特征可以描述任意给定小时的能耗。现在执行features.head()来查看结果 DataFrame 的概览。由于空间限制,我们无法在书中展示整个 DataFrame,所以我们将探索一些特征。

  5. 让我们将创建的五个特征的名称存储在一个数组中:

    feats = features.columns[10:15]
    

    如果我们执行feats,我们会看到五个特征的名称,分别对应每小时能耗的平均值、长度、标准差、变异系数和方差:

    Index(['light__mean', 'light__length', 
        'light__standard_deviation', 
        'light__variation_coefficient', 
        'light__variance'], dtype='object')
    
  6. 现在,让我们显示步骤 5中前五个小时的特征值:

    features[feats].head()
    

    在下面的 DataFrame 中,我们看到从前五个小时的能耗时间序列中提取的特征:

图 10.5 – 为每小时能耗创建的特征

图 10.5 – 为每小时能耗创建的特征

图 10.4.4 中查看平均能耗值,我们可以看到前一个小时灯是亮着的,接下来的四个小时则是关闭的。时间序列的长度为 60,因为我们每小时有 60 分钟的记录。

注意

tsfresh对时间序列应用 63 种特征创建方法。根据时间序列的特性,如长度或其变异性,某些方法可能会返回缺失值或无穷大值。例如,在图 10.4.4 中,我们看到在能耗恒定的那些小时无法计算变异系数。在这些情况下,方差也是0。实际上,对于我们的数据集,许多生成的特征只包含NaN值,或者像长度一样是常数,因此对于训练机器学习模型没有用。

  1. tsfresh 包含一个插补函数,用于插补包含 NaN 值的特征。让我们继续插补我们的特征:

    impute(features)
    

    tsfreshimpute 函数将 NaN-InfInf 值分别替换为变量的中位数、最小值或最大值。

    让我们使用这些特征来训练一个逻辑回归模型并预测办公室是否被占用。

  2. 让我们先分离数据集为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  3. 现在,让我们设置并训练一个逻辑回归模型,然后评估其性能:

    cls = LogisticRegression(random_state=10, C=0.01)
    cls.fit(X_train, y_train)
    print(classification_report(
         y_test, cls.predict(X_test)))
    

    在以下输出中,我们看到用于分类分析的常用评估指标值,这表明创建的特征对预测办公室占用情况是有用的:

                      precision     recall  f1-score   support
                   0         1.00        1.00        1.00           11
                   1         1.00        1.00        1.00            3
         accuracy                                       1.00           14
       macro avg         1.00        1.00        1.00           14
    weighted avg         1.00        1.00        1.00           14
    

注意

为了保持食谱简单,我没有优化模型超参数或调整概率阈值——这些是我们通常为了确保模型准确而做的事情。

  1. 最后,让我们从每个时间序列中提取特征,即 lighttemperaturehumidityco2,这次,我们将在提取后立即插补特征:

    features = extract_features(
        X,
        column_id="id",
        impute_function=impute,
        column_sort="date",
    )
    

注意

步骤 10 中,我们指出我们想要根据包含测量时间和日期的时间戳对时间序列进行排序,通过将 date 变量传递给 column_sort 参数。当我们的时间序列不是等距的或不是按时间顺序排列时,这很有用。如果我们将此参数设置为 Nonetsfresh 假设时间序列是有序且等距的。

步骤 10 的输出是一个包含 135 行的 DataFrame,包含 3,945 个特征(执行 features.shape 检查),这些特征描述了五个原始时间序列——温度、光照、湿度及其比率,以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 办公室。这些特征在 步骤 10 中进行了插补,因此您可以继续使用此 DataFrame 来训练另一个逻辑回归模型以预测办公室占用情况。

它是如何工作的...

在这个食谱中,我们使用了 tsfresh 从五个时间序列自动创建数百个特征,然后使用这些特征来训练一个逻辑回归模型以预测办公室是否被占用。

注意

要使用 tsfresh 创建特征,我们需要从其中提取特征的时间序列间隔必须用 id 变量标记。

要从时间序列创建特征,我们使用了 tsfreshextract_features 函数。此函数接受包含时间序列的唯一标识符的 DataFrame 作为输入,并返回包含提取特征的 DataFrame 作为输出。

extract_features 有三个关键参数:column_idcolumn_sortimpute_functioncolumn_id 接收用于提取特征的每个序列的唯一标识符列的名称。column_sort 用于在提取特征之前重新排序时间序列。当 column_sort 设置为 None 时,tsfresh 假设数据是按时间顺序排列的,并且时间戳是等距的。在 步骤 10 中,我们传递了 date 变量作为排序变量,这告诉 tsfresh 在提取特征之前如何排序数据。

注意

在我们的数据集中,将 column_sort 设置为 None 或传递 date 变量没有区别,因为我们的时间序列已经按时间顺序排列,并且时间戳是等距的。如果您的时序不是这种情况,请使用此参数正确创建特征。

最后,extract_features 函数也通过 impute_function 参数接受 impute 函数,以自动从创建的特征中移除无限和 NaN 值。将在接下来的菜谱中讨论 extract_features 的其他参数。

注意

更多关于 extract_features 函数的详细信息,请访问 tsfresh.readthedocs.io/en/latest/api/tsfresh.feature_extraction.html#module-tsfresh.feature_extraction.extraction

impute 函数,它可以独立使用,就像我们在 步骤 7 中做的那样,或者在我们 步骤 10 中做的那样,在 extract_features 函数内部使用,分别用变量的中位数、最小值或最大值替换 NAN-InfInf 值。如果特征只包含 NaN 值,则它们被零替换。插补是在原地发生的——也就是说,在正在插补的同一个 DataFrame 中。

extract_features 函数返回一个包含数据中唯一标识符行数的 DataFrame。在我们的例子中,它返回了一个包含 135 行的 DataFrame。结果 DataFrame 的列对应于应用于每个 135 个 60 分钟时间序列的 63 种表征方法返回的 789 个值。

步骤 5 中,我们探索了一些结果特征,这些特征捕捉了时间序列的均值、方差和变异系数,以及它们的长度。让我们再探索一些结果特征。

一些创建的变量是自我解释的。例如,'light__skewness''light__kurtosis' 变量包含偏度和峰度系数,这些系数表征了数据分布。'light__has_duplicate_max''light__has_duplicate_min''light__has_duplicate' 变量指示时间序列是否在时间间隔内有重复值或重复的最小值或最大值。'light__quantile__q_0.1''light__quantile__q_0.2''light__quantile__q_0.3' 变量显示了时间序列的不同分位数。最后,'light__autocorrelation__lag_0''light__autocorrelation__lag_1''light__autocorrelation__lag_2' 变量显示了时间序列与其过去值的自相关,滞后 0,1 或 2 步——这些信息通常对预测很有用。

其他特征化方法返回的是从信号处理算法中获取的特征,例如,对于 Ricker 小波,连续小波变换返回的 'light__cwt_coefficients__coeff_0__w_2__widths_(2, 5, 10, 20)', 'light__cwt_coefficients__coeff_0__w_5__widths_(2, 5, 10, 20)', 'light__cwt_coefficients__coeff_0__w_10__widths_(2, 5, 10, 20)', 和 'light__cwt_coefficients__coeff_0__w_20__widths_(2, 5, 10, 20)' 等特征。

注意

由于方法众多,我们无法在本书中详细讨论每种特征化方法及其输出。您可以在 tsfresh.readthedocs.io/en/latest/api/tsfresh.feature_extraction.html 找到关于 tsfresh 支持的转换及其公式的更多详细信息。

一些由 tsfresh 自动创建的特征可能对于某些时间序列来说没有意义,甚至无法计算,因为它们需要一定的长度或数据变异性,或者时间序列必须满足某些分布假设。因此,特征的适用性将取决于时间序列的性质。

注意

您可以根据领域知识决定从您的时间序列中提取哪些特征,或者通过创建所有可能的特征,然后应用特征选择算法或进行数据分析来决定。实际上,从我们的数据集中,许多生成的特征要么是常数,要么只包含缺失数据。因此,我们可以通过从数据中去除这些特征来减少特征空间到信息特征。

参考资料链接

想要了解更多关于 tsfresh 的详细信息,请参阅文章 Christ M.,Braun N.,Neuffer J.,和 Kempa-Liehr A. (2018). *基于可扩展假设检验的时间序列特征提取 (tsfresh – 一个 Python 包). Neurocomputing 307 (2018). 页码 72-77. dl.acm.org/doi/10.1016/j.neucom.2018.03.067

从时间序列数据自动创建和选择预测特征

在先前的菜谱中,我们使用tsfresh自动从时间序列变量中提取了数百个特征。如果我们有多个时间序列变量,我们很容易得到一个包含数千个特征的数据库。此外,许多生成的特征只有缺失数据或常数,因此对训练机器学习模型没有用。

当我们创建分类和回归模型来解决现实生活中的问题时,我们通常希望我们的模型只输入少量相关特征以产生可解释的机器学习输出。简单的模型有很多优点。首先,它们的输出更容易解释。其次,简单的模型存储成本更低,训练速度更快。它们也能更快地返回输出。

tsfresh包含一个基于非参数统计假设检验的高度可并行化的特征选择算法,该算法可以在特征创建过程的后面执行,以快速去除无关特征。特征选择过程使用不同的测试针对不同的特征。

tsfresh使用以下测试来选择特征:

  • 如果特征和目标都是二元的,则使用费舍尔精确检验独立性

  • 如果特征或目标中任一个是二元的,则使用柯尔莫哥洛夫-斯米尔诺夫检验

  • 如果特征或目标都不是二元的,则使用肯德尔秩检验

这些测试的优势在于它们是非参数的,因此不对被测试变量的潜在分布做出假设。

这些测试的结果是一个 p 值向量,衡量每个特征与目标之间的关联显著性。然后根据 Benjamini-Yekutieli 过程评估这些 p 值,以决定保留哪些特征。

注意

关于tsfresh的特征选择过程的更多详细信息,请参阅文章 Christ, Kempa-Liehr, and Feindt,分布式和并行时间序列特征提取用于工业大数据应用。亚洲机器学习会议(ACML)2016,大数据学习研讨会(WLBD),汉密尔顿(新西兰),arXiv,arxiv.org/abs/1610.07717v1

在这个菜谱中,我们将自动从各种时间序列中创建数百个特征,然后利用tsfresh选择最相关的特征。

如何操作...

我们将首先自动从一条时间序列,照明,创建和选择特征,然后我们将自动化处理多条时间序列:

  1. 让我们导入所需的 Python 库和函数:

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh import (
        extract_features,
        extract_relevant_features,
        select_features,
    )
    from tsfresh.utilities.dataframe_functions import impute
    
  2. 加载数据集和目标变量,如技术 要求部分所述:

    X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 让我们为每个小时的照明使用记录自动创建数百个特征,并对生成的特征进行插补:

    features = extract_features(
        X[[«id», «light»]],
        column_id="id",
        impute_function=impute,
    )
    

    上一步的输出是一个包含 135 行和 789 列的 DataFrame,对应于从每个小时的照明消耗中创建的特征。

注意

有关步骤 3或占用检测数据集的更多详细信息,请查看从时间序列自动提取数百个特征菜谱。

  1. 现在,让我们根据我们在本菜谱介绍中提到的非参数测试来选择特征:

    features = select_features(features, y)
    

    如果我们执行len(features),我们将看到值135,这意味着在步骤 3中创建的 789 个特征中,只有 135 个具有统计学意义。继续执行features.head()以显示结果的 DataFrame 的前五行。

  2. 由于空间原因,我们只显示前五个特征:

    feats = features.columns[0:5]
    features[feats].head()
    

    在以下 DataFrame 中,我们可以看到前五个小时光消耗的前五个特征的价值:

图 10.6 – 从每小时的光消耗中创建的五个选定特征的 DataFrame

图 10.6 – 从每小时的光消耗中创建的五个选定特征的 DataFrame

请查看“如何工作…”部分,以获取对步骤 4生成的 DataFrame 的更详细分析。

  1. 现在,我们将使用步骤 4中的特征来训练一个逻辑回归模型并预测办公室是否被占用。让我们首先将数据集分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  2. 让我们设置并训练一个逻辑回归模型,然后评估其性能:

    cls = LogisticRegression(
        random_state=10, C=0.1, max_iter=1000)
    cls.fit(X_train, y_train)
    print(classification_report(
        y_test, cls.predict(X_test)))
    

    在以下输出中,我们可以看到分类分析中常用评估指标的价值。这些表明选定的特征对预测办公室占用是有用的:

                      precision     recall  f1-score   support
                   0         1.00        0.91        0.95           11
                   1         0.75        1.00        0.86            3
    accuracy                                       0.93           14
       macro avg         0.88        0.95        0.90           14
    extract_relevant_features, and, like this, combine *steps 3* and *4*. We’ll do that to create and select features automatically for the five time series in our dataset:
    
    

    features = extract_relevant_features(

    X,

    y,

    column_id="id",

    column_sort="date",

    )

注意

extract_relevant_features的参数与extract_features的参数非常相似。请注意,然而,前者将自动执行插补以能够进行特征选择。我们在从时间序列自动提取数百个特征菜谱中讨论了extract_features的参数。

步骤 8的输出是一个包含 135 行和 968 个特征的 DataFrame,来自tsfresh默认返回的原始 3,945 个特征(您可以通过执行features.shape来检查这一点)。继续使用此 DataFrame 来训练另一个逻辑回归模型以预测办公室占用。

如何工作...

在这个菜谱中,我们从时间序列中创建了数百个特征,然后根据非参数统计测试选择了最相关的特征。特征创建和选择过程由tsfresh自动执行。

为了创建特征,我们使用了tsfreshextract_features函数,我们在从时间序列自动提取数百个特征菜谱中对其进行了详细描述。

为了选择特征,我们使用了来自 tsfreshselect_features 函数。这个函数根据特征和目标的不同性质应用不同的统计测试。简而言之,如果特征和目标是二元的,它通过费舍尔精确检验测试它们之间的关系。如果特征或目标是二元的,而另一个变量是连续的,它通过使用 Kolmogorov-Smirnov 检验测试它们之间的关系。如果特征和目标都不是二元的,它使用 Kendall 排序检验。

这些测试的结果是一个向量,每个特征都有一个 p 值。接下来,tsfresh 应用 Benjamini-Yekutieli 程序,旨在降低假发现率,根据 p 值选择要保留的特征。这个特征选择程序有一些优点,主要优点是统计测试计算速度快,因此选择算法可扩展且可并行化。另一个优点是测试是非参数的,因此适用于线性和非线性模型。

然而,评估每个特征单独的特征选择方法无法移除冗余特征。实际上,tsfresh 自动创建的许多特征将高度相关,例如那些捕捉到不同量级的光消费的特征。因此,它们将显示相似的 p 值并被保留。但在实践中,我们只需要一个或少数几个来捕捉时间序列的信息。我建议在 tsfresh 选择程序之后跟进其他能够识别特征交互的特征选择方法。

最后,在 步骤 8 中,我们通过使用 extract_relevant_features 函数将特征创建步骤 (步骤 3) 与特征选择步骤 (步骤 4) 结合起来。extract_relevant_features 函数将 extract_features 函数应用于每个时间序列以创建特征,并进行插补。接下来,它应用 select_features 函数以返回一个包含每个唯一标识符的行和一个为每个时间序列选择的特征的 DataFrame。请注意,对于不同的时间序列,可以选择不同的特征。

参见

tsfresh 中的选择算法提供了一种快速的方法来移除无关特征。然而,它并不能找到分类或回归任务的最佳特征子集。可以在 tsfresh 算法之后应用其他特征选择方法来进一步减少特征空间。

想要了解更多关于特征选择算法的细节,请查看 Soledad Galli 在 Leanpub 上出版的书籍《Python 机器学习中的特征选择》:leanpub.com/feature-selection-in-machine-learning/

从不同的时间序列中提取不同的特征

tsfresh 基于时间序列的特征和分布提取许多特征,例如它们的关联属性、平稳性和熵。它还应用非线性时间序列分析函数,例如通过傅里叶或小波变换分解时间序列信号。根据时间序列的性质,这些变换中的一些比其他的有意义。例如,波长分解方法对于由信号或传感器产生的时间序列可能是有意义的,但并不总是对表示销售或股价的时间序列有用。

在这个食谱中,我们将讨论如何优化特征提取过程,从每个时间序列中提取特定特征,然后使用这些特征来预测办公室的占用情况。

如何操作...

tsfresh 通过包含方法名称作为键的字典访问将用于创建特征的函数,如果它们需要参数,则参数作为值。tsfresh 还包含一些预定义的字典。我们将首先探索这些预定义的字典,这些字典可以通过 settings 模块访问:

  1. 让我们导入所需的 Python 库、函数和 settings 模块:

    import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh.feature_extraction import (
        extract_features
    )
    from tsfresh.feature_extraction import settings
    
  2. 加载数据集和 技术要求 部分中描述的目标变量:

    X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    

    tsfresh 包含三个主要的字典来控制特征创建输出:settings.ComprehensiveFCParameterssettings.EfficientFCParameterssettings.MinimalFCParameters。在这里,我们将探索返回最少特征的字典。您可以重复这些步骤来探索其他字典。

  3. 显示在字典返回最少特征时将应用的特征创建方法:

    minimal_feat = settings.MinimalFCParameters()
    minimal_feat.items()
    

    步骤 3 的输出中,我们看到一个以特征提取方法名称为键,以这些方法使用的参数(如果有)为值的字典:

    ItemsView({'sum_values': None, 'median': None, 'mean': None, 'length': None, 'standard_deviation': None, 'variance': None, 'root_mean_square': None, 'maximum': None, 'absolute_maximum': None, 'minimum': None})
    

注意

好吧,通过调整 步骤 3 中的代码,继续探索其他两个预定义的字典,settings.ComprehensiveFCParameterssettings.EfficientFCParameters

  1. 现在,让我们使用 步骤 3 中的字典从 light 时间序列中提取仅这些特征,然后显示生成的 DataFrame 的形状:

    features = extract_features(
        X[[«id», «light»]],
        column_id="id",
        default_fc_parameters=minimal_feat,
    )
    features.shape
    

    步骤 4 的输出是 (135, 10),这意味着为 135 小时的光照消耗数据中每个数据点只创建了 10 个特征。

  2. 让我们显示生成的 DataFrame:

    features.head()
    

    在以下 DataFrame 中,我们看到前五小时光照消耗生成的特征值:

图 10.7 – 为每小时光照消耗创建的 DataFrame

图 10.7 – 为每小时光照消耗创建的 DataFrame

现在,我们将使用这些特征来训练一个逻辑回归模型,以预测办公室是否被占用。

  1. 让我们先从将数据集分为训练集和测试集开始:

    X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  2. 现在,让我们设置并训练一个逻辑回归模型,然后评估其性能:

    cls = LogisticRegression(random_state=10, C=0.01)
    cls.fit(X_train, y_train)
    print(classification_report(
        y_test, cls.predict(X_test)))
    

    在以下输出中,我们看到的是用于分类分析的常用评估指标。这些指标表明所选特征对预测办公室占用率是有用的:

                        precision     recall  f1-score   support
    0         1.00        0.91        0.95           11
                   1         0.75        1.00        0.86            3
         accuracy                                       0.93           14
       macro avg         0.88        0.95        0.90           14
    weighted avg         0.95        0.93        0.93           14
    

注意

因为光照消耗是办公室占用率的一个非常好的指标,通过非常简单的特征,我们可以获得一个预测性的逻辑回归模型。

现在,让我们学习如何为不同的时间序列指定创建不同特征的方法。

  1. 让我们创建一个包含我们想要用于从light时间序列创建特征的名称的字典。我们将方法名称作为键,如果方法需要参数,我们将它作为额外的字典传递给相应的键;否则,我们传递None作为值:

    light_feat = {
        «sum_values": None,
        "median": None,
        «standard_deviation": None,
        "quantile": [{"q": 0.2}, {"q": 0.7}],
    }
    
  2. 现在,让我们创建一个包含从co2时间序列创建的特征的字典:

    co2_feat = {
        «root_mean_square": None,
        «number_peaks": [{"n": 1}, {"n": 2}],
    }
    
  3. 让我们将这些字典合并到一个新的字典中:

    kind_to_fc_parameters = {
        «light»: light_feat,
        "co2": co2_feat,
    }
    
  4. 最后,让我们使用第 10 步的字典从两个时间序列中创建特征:

    features = extract_features(
        X[[«id», «light», «co2»]],
        column_id="id",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

    第 11 步的输出是一个包含 135 行和 8 个特征的 DataFrame。如果我们执行features.columns,我们将看到创建的特征的名称:

    Index(['light__sum_values', 'light__median',
        'light__standard_deviation',
        'light__quantile__q_0.2',
        'light__quantile__q_0.7',
        'co2__root_mean_square',
        'co2__number_peaks__n_1',
        'co2__number_peaks__n_2'],
        dtype='object')
    

注意,在第 11 步的输出中,从lightco2时间序列中分别创建了不同的变量。

它是如何工作的...

在这个菜谱中,我们从我们的时间序列数据中提取了特定的特征。首先,我们根据tsfresh附带的一个预定义字典创建了特征。接下来,我们创建了自己的字典,指定为不同的时间序列创建不同的特征。

tsfresh包附带了一些预定义的字典,可以通过settings模块访问。MinimalFCParameters字典用于根据时间序列分布的基本统计参数(如平均值、中位数、标准差、方差、值的总和、计数(或长度)、最小值和最大值)创建 10 个简单特征。在第 3 步中,我们展示了这个字典,其中方法名称作为键,由于这些方法不需要额外的参数,每个键的值都是None

tsfresh有两个额外的预定义字典。EfficientFCParameters用于应用计算速度快的方法,而ComprehensiveFCParameters返回所有可能的特征,并且是extract_features函数默认使用的。

注意

更多关于预定义字典的详细信息,请查看tsfresh的文档:tsfresh.readthedocs.io/en/latest/text/feature_extraction_settings.html

通过在 tsfreshextract_features 函数的 default_fc_parameters 参数中使用这些预定义的字典,我们可以从一个或多个时间序列中创建特定特征,就像我们在 步骤 4 中做的那样。请注意,default_fc_parameters 指示 extract_features所有 时间序列中创建相同的特征。如果我们想从不同的时间序列中提取不同的特征怎么办?

为了为不同的时间序列创建不同的特征,我们可以使用 tsfreshextract_features 函数的 kind_to_fc_parameters 参数。该参数接受一个字典的字典,指定应用于每个时间序列的方法。

步骤 8 中,我们创建了一个字典来指定从 light 时间序列创建特定特征。请注意,"sum_values""mean" 方法使用 None 作为值,但 quantile 方法需要额外的参数,对应于应从时间序列返回的分位数。在 步骤 9 中,我们创建了一个字典来指定从 co2 时间序列创建特征。在 步骤 10 中,我们将这两个字典合并为一个,以时间序列的名称作为键,特征创建字典作为值。然后,我们将这个字典传递给 tsfreshextract_features 函数的 kind_to_fc_parameters 参数。如果使用领域知识创建特征,或者只创建少量特征,这种方式指定特征是合适的。

如果我们想为各种时间序列创建多个特征,是否需要手动将每个方法键入字典中?实际上并不需要。在下面的食谱中,我们将学习如何根据 Lasso 选定的特征指定要创建哪些特征。

创建通过特征选择识别的特征子集

从时间序列数据自动创建和选择预测特征 食谱中,我们学习了如何使用 tsfresh 选择相关特征。我们还讨论了 tsfresh 选择过程的局限性,并建议采用替代特征选择方法来识别预测特征,同时避免冗余。

在本食谱中,我们将使用 tsfresh 创建和选择特征。之后,我们将通过利用 Lasso 正则化进一步减少特征空间。然后,我们将学习如何从选定的特征名称创建字典,以触发仅从未来的时间序列创建这些特征。

如何操作...

让我们先导入必要的库并准备数据集:

  1. 让我们导入所需的库和函数:

    import pandas as pd
    from sklearn.feature_selection import SelectFromModel
    from sklearn.linear_model import LogisticRegression
    from tsfresh import (
        extract_features,
        extract_relevant_features,
    )
    from tsfresh.feature_extraction import settings
    
  2. 加载在 技术要求 部分描述的 占用检测 数据集:

    X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv(
        "occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 从我们的五个时间序列中创建和选择特征,然后显示结果 DataFrame 的形状:

    features = extract_relevant_features(
        X,
        y,
        column_id="id",
        column_sort="date",
    )
    features.shape
    

    步骤 3 的输出是 (135, 968),表示从五个原始时间序列中返回了 968 个特征,对应于每小时的记录。

注意

我们在 从时间序列数据自动创建和选择预测特征 菜谱中讨论了 步骤 3 的函数。

让我们通过选择具有 Lasso 正则化的特征进一步减少特征空间。

  1. 设置具有 Lasso 正则化的逻辑回归,这是 "l1" 惩罚。我还随意设置了一些额外的参数:

    cls = LogisticRegression(
        penalty="l1",
        solver=»liblinear",
        random_state=10,
        C=0.05,
        max_iter=1000,
    )
    
  2. 让我们设置一个转换器来保留那些逻辑回归系数不等于 0 的特征:

    selector = SelectFromModel(cls)
    
  3. 训练逻辑回归模型并选择特征:

    selector.fit(features, y)
    
  4. 现在,将所选特征捕获到一个变量中:

    features = selector.get_feature_names_out()
    

    如果我们执行 features,我们将看到所选特征的名字:

    array([
    'light__sum_of_reoccurring_data_points',
    'co2__fft_coefficient__attr_"abs"__coeff_0',
    'co2__spkt_welch_density__coeff_2', 'co2__variance',
    'temperature__c3__lag_1', 'temperature__abs_energy',
    'temperature__c3__lag_2', 'temperature__c3__lag_3',
    'co2__sum_of_reoccurring_data_points',
    'light__spkt_welch_density__coeff_8',
    'light__agg_linear_trend__attr_"intercept"__chunk_len_50__f_agg_"var"',
             'light__agg_linear_trend__attr_"slope"__chunk_len_50__f_agg_"var"',  'light__agg_linear_trend__attr_"intercept"__chunk_len_10__f_agg_"var"'],
    dtype=object)
    
  5. 为了只从时间序列中提取 步骤 6 的特征,我们需要在字典中捕获特征创建方法名称和相应的参数。我们可以使用 tsfresh 自动完成此操作:

    kind_to_fc_parameters = settings.from_columns(
        selector.get_feature_names_out(),
    )
    

    如果我们执行 kind_to_fc_parameters,我们将看到从 步骤 6 的特征名称创建的字典:

    {'light':
        {‹sum_of_reoccurring_data_points': None,
        ‹spkt_welch_density': [{'coeff': 8}],
        'variance': None,
        ‹agg_linear_trend': [
            {‹attr': 'slope','chunk_len': 50,
                'f_agg': 'var'},
            {‹attr': 'intercept',
                'chunk_len': 10,'f_agg':'var'}
            ]
        },
    'co2':
        {‹spkt_welch_density': [{'coeff': 2}],
        'variance': None,
        ‹sum_of_reoccurring_data_points': None
        },
        'temperature': {
            'c3': [{'lag': 1}, {'lag': 2}, {'lag':3}],
            'abs_energy': None}
    }
    
  6. 现在,我们可以使用 步骤 8 的字典与 extract_features 函数一起创建我们数据集中的特征:

    features = extract_features(
        X,
        column_id="id",
        column_sort="date",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

新的 DataFrame,可以通过执行 features.head() 来显示,仅包含由 Lasso 选出的 12 个特征。请在你自己的计算机上验证这个结果。

它是如何工作的...

在这个菜谱中,我们从 5 个时间序列创建了 968 个特征。接下来,我们使用 Lasso 正则化将特征空间减少到 12 个特征。最后,我们在字典中捕获了所选特征的规格,以便在未来的操作中,我们只创建来自我们时间序列的特征。

要自动创建和选择 tsfresh 的特征,我们使用了 extract_relevant_features 函数,我们已在 从时间序列数据自动创建和选择预测特征 菜谱中详细描述了该函数。

Lasso 正则化具有将逻辑回归模型的一些系数减少到 0 的内在能力。系数为 0 的特征对办公室占用预测的贡献为零,因此可以被移除。SelectFromModel() 类可以识别并移除这些特征。我们使用具有 Lasso 正则化的逻辑回归模型设置了一个 SelectFromModel() 实例来找到模型系数。通过 fit()SelectFromModel() 使用我们从时间序列创建的 968 个特征训练了逻辑回归模型,并识别了那些系数不等于 0 的特征。然后,通过 get_feature_names_out() 方法,我们在一个新变量中捕获了所选特征的名字。

为了只创建由 Lasso 正则化选择的 12 个特征,我们使用tsfreshfrom_columns()函数从变量名创建了一个字典。此函数返回一个字典,其中的键是从中选择了特征的变量,值是包含用于创建特征的方法的键和(如果有的话)参数的值。为了创建新特征,我们使用此字典与extract_features函数一起。

注意

步骤 9中,我们将整个数据集传递给了extract_features函数。结果特征只包含从五个时间序列中的三个提取的特征。另外两个时间序列被忽略了。

将特征创建嵌入到 scikit-learn 管道中

在本章中,我们讨论了如何通过利用tsfresh自动创建和选择时间序列数据中的特征。然后,我们使用这些特征来训练一个分类模型,以预测在任何给定小时办公室是否被占用。

tsfresh在其主要函数extract_featuresextract_relevant_features周围包含包装器类,以便特征创建和选择与 scikit-learn 管道兼容。

在这个菜谱中,我们将设置一个 scikit-learn 管道,使用tsfresh从时间序列中提取特征,然后使用这些特征训练一个逻辑回归模型来预测办公室的占用情况。

如何做到这一点...

让我们首先导入必要的库并准备好数据集:

  1. 让我们导入所需的库和函数:

    import pandas as pd
    from sklearn.pipeline import Pipeline
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import classification_report
    from tsfresh.transformers import (
        RelevantFeatureAugmenter)
    
  2. 加载技术要求部分中描述的占用检测数据集:

    X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv(
        "occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 创建一个包含目标变量索引的空 DataFrame:

    tmp = pd.DataFrame(index=y.index)
    
  4. 现在,让我们将步骤 3中的 DataFrame 和步骤 2中的目标分割成训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        tmp, y, random_state=0)
    

注意

X_trainX_test将用作容器来存储tsfresh创建的特征。它们对于我们将要讨论的RelevantFeatureAugmenter()的功能是必需的。

  1. 让我们创建一个字典,指定从每个时间序列中提取的特征(我任意定义了以下特征):

    kind_to_fc_parameters = {
        "light": {
            "c3": [{"lag": 3}, {"lag": 2}, {"lag": 1}],
            «abs_energy": None,
            «sum_values": None,
            «fft_coefficient": [
                {«attr": "real", "coeff": 0},
                {«attr": "abs", "coeff": 0}],
            «spkt_welch_density": [
                {«coeff": 2}, {"coeff":5}, {"coeff": 8}
            ],
            «agg_linear_trend": [
                {«attr": "intercept",
                „chunk_len": 50, „f_agg": „var"},
                {"attr": "slope",
                «chunk_len": 50, "f_agg":"var"},
            ],
            «change_quantiles": [
                {«f_agg": "var", "isabs": False,
                «qh": 1.0,"ql": 0.8},
                {«f_agg": "var", "isabs": True,
                «qh": 1.0,"ql": 0.8},
            ],
        },
    "co2": {
        «fft_coefficient": [
            {«attr": "real", "coeff": 0},
            {«attr": "abs", "coeff": 0}],
        "c3": [{"lag": 3}, {"lag": 2}, {"lag": 1}],
        «sum_values": None,
        «abs_energy": None,
        «sum_of_reoccurring_data_points": None,
        «sum_of_reoccurring_values": None,
        },
    "temperature": {"c3": [{"lag": 1},
        {«lag»: 2},{«lag»: 3}], «abs_energy": None},
    }
    

    我们在从不同时间序列提取不同特征菜谱中讨论了此字典的参数。

  2. 让我们设置RelevantFeatureAugmenter(),这是一个围绕extract_relevant_features函数的包装器,以创建步骤 5中指定的特征:

    augmenter = RelevantFeatureAugmenter(
        column_id="id",
        column_sort="date",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

注意

要创建所有可能的特征,请在步骤 6中使用FeatureAugmenter()类。

  1. 让我们将步骤 6中的特征创建实例与逻辑回归模型结合到一个 scikit-learn 管道中:

    pipe = Pipeline(
        [
            ("augmenter", augmenter),
            («classifier», LogisticRegression(
        random_state=10, C=0.01)),
        ]
    )
    
  2. 现在,让我们告诉RelevantFeatureAugmenter()它需要使用哪个数据集来创建特征:

    pipe.set_params(augmenter__timeseries_container=X)
    
  3. 让我们拟合管道,这将触发特征创建过程,然后训练逻辑回归模型:

    pipe.fit(X_train, y_train)
    
  4. 现在,让我们使用测试集中的时间序列来获取预测,并通过分类报告评估模型性能:

    print(classification_report(
        y_test, pipe.predict(X_test)))
    

    我们可以在这里看到步骤 10的输出:

                      precision     recall  f1-score   support
                   0         1.00        0.96        0.98           28
                   1         0.86        1.00        0.92            6
         accuracy                                       0.97           34
    macro avg         0.93        0.98        0.95           34
    weighted avg         0.97        0.97        0.97           34
    

分类报告的值表明,提取的特征适合预测在任何给定小时办公室是否被占用。

它是如何工作的...

在这个配方中,我们将从时间序列创建特征与使用 scikit-learn 库训练机器学习算法的管道结合起来。

tsfresh库在其主要函数周围包含两个包装类,以使特征创建过程与 scikit-learn 管道兼容。在这个配方中,我们使用了RelevantFeatureAugmenter()类,它包装了extract_relevant_features函数,用于从时间序列创建并选择特征。

RelevantFeatureAugmenter()的工作方式如下;使用fit(),它通过extract_relevant_features创建并选择特征。所选特征的名称随后存储在转换器内部。使用transform()RelevantFeatureAugmenter()从时间序列创建所选特征。

我们通过传递一个包含我们想要创建的特征的字典到其kind_to_fc_parameters参数,覆盖了RelevantFeatureAugmenter()的默认功能。因此,使用transform()RelevantFeatureAugmenter()从时间序列创建了指定的特征。

要从时间序列创建所有特征,tsfresh包括FeatureAugmenter()类,它具有与RelevantFeatureAugmenter()相同的功能,但没有特征选择步骤。

RelevantFeatureAugmenter()FeatureAugmenter()都需要两个 DataFrame 来工作。第一个 DataFrame 包含时间序列数据和唯一标识符(我们在步骤 2中加载了这个 DataFrame)。第二个 DataFrame 应该是空的,并包含其索引中的唯一标识符(我们在步骤 3中创建了此 DataFrame)。特征是从包含时间序列的第一个 DataFrame 中提取的(在应用transform()时),然后添加到第二个 DataFrame 中,然后用于训练逻辑回归或获取其预测。

注意

空 DataFrame 的索引被RelevantFeatureAugmenter()FeatureAugmenter()用于识别从中提取特征的时间序列。因此,在传递X_train时应用fit(),从id值在训练集中的时间序列中提取了特征。之后,通过观察使用测试集做出的预测来评估模型,这触发了从id值在X_test中的时间序列创建特征。

当我们在管道上使用fit()时,我们从原始时间序列创建了特征,并使用这些特征训练了一个逻辑回归模型。使用predict()方法,我们从测试集创建了特征,并基于这些特征获得了逻辑回归的预测。

参见

如需了解此配方中使用的类和程序的更多详细信息,请访问以下链接:

第十一章:从文本变量中提取特征

文本可以是我们数据集中的一个变量。例如,在保险领域,描述事故情况的详细信息可能来自表单中的自由文本字段。如果一家公司收集客户评价,这些信息将以用户提供的短文本片段的形式收集。文本数据不显示我们在这本书中一直处理的数据集的 表格 模式。相反,文本中的信息在长度、内容和写作风格上可能有所不同。我们可以从文本变量中提取大量信息,用作机器学习模型中的预测特征。本章中我们将涵盖的技术属于 自然语言处理 (NLP) 的领域。NLP 是语言学和计算机科学的一个子领域,它关注计算机与人类语言之间的交互,换句话说,就是如何编程计算机理解人类语言。NLP 包括理解文本的句法、语义和语篇的众多技术。因此,要公正地对待这个领域,就需要一本整本书。

在本章中,我们将讨论允许我们快速从短文本片段中提取特征以补充我们的预测模型的方法。具体来说,我们将讨论如何通过查看文本的一些统计参数来捕捉文本的复杂性,例如单词长度和计数、使用的单词和唯一单词的数量、句子的数量等等。我们将使用 pandasscikit-learn 库,并且我们将对一个非常有用的 Python NLP 工具包进行浅入浅出的探讨,该工具包称为 自然语言 工具包 (NLTK)。

本章包括以下食谱:

  • 计算字符、单词和词汇量

  • 通过计算句子来估计文本复杂度

  • 使用词袋和 n-gram 创建特征

  • 实现词频-逆文档频率

  • 清洗和词干提取文本变量

技术要求

在本章中,我们将使用 pandasmatplotlibscikit-learn Python 库。我们还将使用 NLTK,这是一个全面的 Python NLP 和文本分析库。你可以在 www.nltk.org/install.html 找到安装 NLTK 的说明。

如果你使用的是 Python Anaconda 发行版,请按照 anaconda.org/anaconda/nltk 上的说明安装 NLTK

在你安装了 NLTK 之后,打开一个 Python 控制台并执行以下命令:

import nltk
nltk.download('punkt')
nltk.download('stopwords')

这些命令将为你下载运行本章中食谱所需的数据。

注意

如果你还没有下载这些或其他必要的 NLTK 功能数据源,NLTK 将会引发错误。仔细阅读错误信息,因为它会指导你下载运行你试图执行的命令所需的数据。

计算字符、单词和词汇量

文本的一个显著特征是其复杂性。长描述比短描述更有可能包含更多信息。包含不同、独特词汇的文本比反复重复相同词汇的文本更可能细节丰富。同样,当我们说话时,我们使用许多短词,如冠词和介词来构建句子结构,而主要概念通常来自我们使用的名词和形容词,这些往往是较长的词。所以,正如你所看到的,即使不阅读文本,我们也可以通过确定单词数量、唯一单词数量(单词的非重复出现)、词汇多样性和这些单词的长度来推断文本提供的信息量。在这个菜谱中,我们将学习如何使用pandas从文本变量中提取这些特征。

准备工作

我们将使用包含大约 18,000 篇关于 20 个不同主题的新闻的scikit-learn。更多关于这个数据集的详细信息可以在以下网站上找到:

在深入菜谱之前,让我们讨论一下我们将从这些文本片段中提取的特征。我们提到,较长的描述、文章中的更多单词、更多唯一词汇和较长的单词往往与文章提供的信息量相关。因此,我们可以通过提取以下关于文本的信息来捕捉文本复杂性:

  • 总字符数

  • 总单词数

  • 唯一单词的总数

  • 词汇多样性(总单词数除以唯一单词数)

  • 单词平均长度(字符数除以单词数)

在这个菜谱中,我们将使用pandas提取这些数值特征,pandas具有广泛的字符串处理功能,可以通过str向量化字符串函数访问。

如何实现...

让我们先加载pandas并准备好数据集:

  1. 加载pandas和来自scikit-learn的数据集:

    import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    
  2. 让我们将 20 个新闻组数据集的火车集部分加载到一个pandas DataFrame 中:

    data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    

小贴士

您可以通过执行print(df['text'][1])来打印 DataFrame 中的文本示例。通过更改[]之间的数字来显示不同的文本。注意,每个文本描述都是一个由字母、数字、标点和空格组成的单个字符串。您可以通过执行type(df["text"][1])来检查数据类型。

现在我们已经将文本变量放入一个pandas DataFrame 中,我们准备提取特征。

  1. 让我们在新列中捕捉每个文本片段中的字符数:

    df['num_char'] = df['text'].str.len()
    

小贴士

在计数字符之前,您可以通过在len()方法之前添加strip()方法来移除字符串末尾的空白字符,包括新行中的空白字符,如下所示:df['num_char'] = df['text'].str.strip().str.len()

  1. 让我们在新列中捕捉每个文本中的单词数量:

    df['num_words'] = df['text'].str.split().str.len()
    

    要计算单词数量,我们使用pandas库的split()方法,该方法在空白处分割文本。例如,通过执行df["text"].loc[1].split()来分离 DataFrame 中第二个文本的单词。

  2. 让我们在新列中捕捉每个文本中的唯一单词数量:

    df['num_vocab']df[
        'text'].str.lower().str.split().apply(
            set).str.len()
    

注意

如果一个单词的首字母大写,Python 会将它解释为两个不同的单词。为了避免这种行为,我们可以在split()方法之前应用lower()方法。

  1. 让我们创建一个特征来捕捉词汇多样性 – 即总单词数(步骤 4)与唯一单词数(步骤 5)的对比:

    df['lexical_div'] = df['num_words'] / df['num_vocab']
    
  2. 让我们通过将字符数(步骤 3)除以单词数(步骤 4)来计算平均单词长度:

    df['ave_word_length'] = df[
        'num_char'] / df['num_words']
    

    如果我们执行df.head(),我们将看到包含文本和新建特征的前五行数据:

图 11.1 – 包含文本变量和总结文本某些特征的 DataFrame

图 11.1 – 包含文本变量和总结文本某些特征的 DataFrame

这样,我们已经提取了五个不同的特征来捕捉文本复杂性,我们可以将这些特征用作机器学习算法的输入。

注意

在这个菜谱中,我们直接从原始数据中创建了新特征,而没有进行任何数据清理、移除标点符号,甚至没有进行词干提取。请注意,这些步骤是在大多数标准 NLP 过程之前执行的。要了解更多信息,请访问本章末尾的清洗和词干提取文本变量菜谱。

它是如何工作的...

在这个菜谱中,我们通过使用pandasstr来访问内置的pandas字符串功能,创建了五个新特征来捕捉文本复杂性。我们处理了scikit-learn附带的数据集20 Newsgrouptrain子集的文本列。这个数据集中的每一行都是一个包含文本的字符串。

我们使用pandasstr,然后是len(),来计算每个字符串中的字符数 – 即字母、数字、符号和空格的总数。我们还结合了str.len()str.strip()来移除字符串开头和结尾的空白字符以及在新行中的空白字符,在计数字符之前。

要计算单词数量,我们使用pandasstr,然后是split(),将字符串分割成单词列表。split()方法通过在单词之间的空白处断开字符串来创建单词列表。接下来,我们使用str.len()来计数这些单词,得到每个字符串的单词数。

注意

我们可以通过传递一个字符串或字符来改变str.split()的行为,我们希望用它来分割字符串。例如,df['text'].str.split(';');的每个出现处分割字符串。

要确定唯一单词的数量,我们使用了 pandas 的str.split()函数将字符串分割成单词列表。接下来,我们在 pandas 的apply()方法中应用了内置的 Python set()方法,以返回一个单词集合。记住,集合包含列表中元素的唯一出现 – 那就是唯一单词。接下来,我们使用 pandas 的str.len()函数来计数这些单词,并返回lower()函数,在分割字符串和计数唯一单词之前将所有字符设置为小写。

要创建词汇多样性和平均词长特征,我们只是简单地执行了两个pandas序列的向量除法。就是这样;我们创建了五个关于文本复杂性的新特征。

更多内容...

我们可以通过使用可视化来检查数据集中 20 个不同新闻主题中提取的文本特征的分布。

在运行本食谱中“如何工作...”部分的全部步骤之后,要制作新创建特征的直方图,请遵循以下步骤:

  1. 导入matplotlib

    import matplotlib.pyplot as plt
    
  2. 将新闻主题的目标添加到 20 个新闻组 DataFrame 中:

    df['target'] = data.target
    
  3. 创建一个函数,用于显示每个新闻主题中你选择的特征的直方图:

    def plot_features(df, text_var):
        nb_rows = 5
        nb_cols = 4
        fig, axs = plt.subplots(
            nb_rows, nb_cols,figsize=(12, 12))
        plt.subplots_adjust(wspace=None, hspace=0.4)
        n = 0
        for i in range(0, nb_rows):
            for j in range(0, nb_cols):
                axs[i, j].hist(
                    df[df.target==n][text_var], bins=30)
                axs[i, j].set_title(
                    text_var + ' | ' + str(n))
                     n += 1
        plt.show()
    
  4. 运行单词数量特征的函数:

    plot_features(df, 'num_words')
    

    之前的命令返回以下图表,其中你可以看到 20 个新闻主题中每个主题的单词数量分布,图表标题中从 0 到 19 编号:

图 11.2 – 每个文本中单词数量的分布直方图,按每个文本中讨论的主题进行细分

图 11.2 – 每个文本中单词数量的分布直方图,按每个文本中讨论的主题进行细分

单词数量在不同新闻主题中显示出不同的分布。因此,这个特征可能在分类算法中预测文本主题时很有用。

参见

要了解更多关于 pandas 内置字符串处理功能的信息,请访问pandas.pydata.org/pandas-docs/stable/user_guide/text.html#method-summary

通过计算句子来估计文本复杂性

我们可以在特征中捕捉到的一篇文本的一个方面是其复杂性。通常,包含多个句子并分布在几个段落中的较长的描述,比包含非常少句子的描述提供的信息更多。因此,捕捉句子的数量可能有助于了解文本提供的信息量。这个过程被称为NLTK Python 库,它提供了这个功能。

准备工作

在这个菜谱中,我们将使用NLTK Python 库。有关如何安装NLTK的指南,请参阅本章的技术要求部分。

如何做到...

让我们先导入所需的库和数据集:

  1. 让我们加载pandasNLTK中的句子分词器和scikit-learn中的数据集:

    import pandas as pd
    from nltk.tokenize import sent_tokenize
    from sklearn.datasets import fetch_20newsgroups
    
  2. 为了理解NLTK中的句子分词器的功能,让我们创建一个包含多个句子的字符串变量:

    text = """
    The alarm rang at 7 in the morning as it usually did on Tuesdays. She rolled over, stretched her arm, and stumbled to the button till she finally managed to switch it off. Reluctantly, she got up and went for a shower. The water was cold as the day before the engineers did not manage to get the boiler working. Good thing it was still summer.
    Upstairs, her cat waited eagerly for his morning snack. Miaow! He voiced with excitement as he saw her climb the stairs.
    """
    
  3. 现在,让我们使用NLTK库的句子分词器将步骤 2 中的字符串拆分为句子:

    sent_tokenize(text)
    

小贴士

如果你在步骤 3 中遇到错误,请仔细阅读错误消息并下载NLTK所需的数据源,如错误消息中所述。更多详情,请参阅技术 要求部分。

句子分词器返回以下输出中显示的句子列表:

['\nThe alarm rang at 7 in the morning as it usually did on Tuesdays.',
 'She rolled over,\nstretched her arm, and stumbled to the button till she finally managed to switch it off.',
 'Reluctantly, she got up and went for a shower.',
 'The water was cold as the day before the engineers\ndid not manage to get the boiler working.',
 'Good thing it was still summer.',
 'Upstairs, her cat waited eagerly for his morning snack.',
 'Miaow!',
 'He voiced with excitement\nas he saw her climb the stairs.']

注意

后跟字母的转义字符\n表示新的一行。

  1. 让我们计算text变量中的句子数:

    len(sent_tokenize(text))
    

    之前的命令返回8,这是我们的text变量中的句子数。现在,让我们确定整个 DataFrame 中的句子数。

  2. 让我们将 20 个新闻组数据集的train子集加载到pandas DataFrame 中:

    data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了加快以下步骤,我们只处理 DataFrame 的前10行:

    df = df.loc[1:10]
    
  4. 让我们也移除文本的第一部分,这部分包含关于电子邮件发送者、主题和其他我们不感兴趣的信息。大部分这些信息都在Lines这个词之后,跟着一个冒号:,所以让我们在Lines:处拆分字符串并捕获字符串的第二部分:

    df['text'] = df['text'].str.split('Lines:').apply(
        lambda x: x[1])
    
  5. 最后,让我们创建一个包含每text中句子数的变量:

    df['num_sent'] = df['text'].apply(
        sent_tokenize).apply(len)
    

    使用df命令,你可以通过text变量和新特性(包含每段文本的句子数)显示整个 DataFrame:

图 11.3 – 包含文本变量和每段文本句子数的 DataFrame

图 11.3 – 包含文本变量和每段文本句子数的 DataFrame

现在,我们可以使用这个新特性作为机器学习算法的输入。

它是如何工作的...

在这个菜谱中,我们使用NLTK库的sent_tokenizer将包含文本的字符串拆分为句子。sent_tokenizer已经预先训练以识别大写字母和不同类型的标点符号,这些标点符号标志着句子的开始和结束。

首先,我们手动创建一个字符串并应用sent_tokenizer以熟悉其功能。分词器将文本分为一个包含八个句子的列表。我们将分词器与内置的 Python len()方法结合使用,以计算字符串中的句子数。

接下来,我们加载了一个包含文本的数据集,为了加快计算速度,我们只使用 pandas 的loc[]函数保留了 DataFrame 的前 10 行。接下来,我们移除了文本的前一部分,这部分包含了关于电子邮件发送者和主题的信息。为此,我们使用 pandas 的str.split("Lines:")函数在Lines:处分割字符串,该函数返回一个包含两个元素的列表:Lines:之前的字符串和之后的字符串。利用apply()中的 lambda 函数,我们保留了文本的第二部分,即split()返回的列表中的第二个字符串。

最后,我们使用 pandas 的apply()方法将sent_tokenizer应用于 DataFrame 中的每一行,将字符串分割成句子,然后使用内置的 Python len()方法应用于句子列表,以返回每个字符串的句子数。这样,我们创建了一个包含每个文本的句子数的新特征。

更多内容...

NLTK除了其他有用功能外,还具有单词分词功能,我们可以用NLTK代替pandas来计数并返回单词数。你可以在这里了解更多关于NLTK功能的信息:

  • 《使用 NLTK 3 进行 Python 3 文本处理食谱》,作者:雅各布·珀金斯,Packt 出版社

  • www.nltk.org/上的NLTK文档。

使用词袋模型和 n-gram 创建特征

词袋模型Bag-of-WordsBoW)是对文本的一种简化表示,它捕捉了文本中出现的单词以及每个单词在文本中出现的次数。因此,对于文本字符串 Dogs like cats, but cats do not like dogs,得到的 BoW 如下:

图 11.4 – 从句子 Dogs like cats, but cats do not like dogs 导出的 BoW

图 11.4 – 从句子 Dogs like cats, but cats do not like dogs 导出的 BoW

在这里,每个单词成为一个变量,变量的值表示单词在字符串中出现的次数。正如你所见,BoW 捕捉了多重性,但没有保留单词顺序或语法。这就是为什么它是一种简单而有用的方式来提取特征并捕捉我们正在处理的文本的一些信息。

为了捕捉一些语法,BoW 可以与n-gram一起使用。n-gram 是在给定文本中连续的n个项的序列。继续使用句子 Dogs like cats, but cats do not like dogs,得到的 2-gram 如下:

  • 狗喜欢

  • 喜欢猫

  • 猫和

  • 但是

  • 像狗一样

我们可以创建一个与 BoW 一起的 n-gram 袋,其中额外的变量由 2-gram 给出,每个 2-gram 的值是它们在每个字符串中出现的次数;对于这个例子,值是 1。因此,我们的最终包含 2-gram 的 BoW 看起来如下:

图 11.5 – 包含 2-gram 的 BoW

图 11.5 – 包含 2-gram 的 BoW

在这个菜谱中,我们将学习如何使用scikit-learn创建带有或不带有 n-gram 的 BoW。

准备工作

在深入这个菜谱之前,让我们熟悉一下 BoW 的一些参数,我们可以调整这些参数以使 BoW 更全面。在创建多个文本片段的 BoW 时,对于我们在分析的文本片段中至少出现一次的每个唯一单词,都会创建一个新特征。如果单词只在一个文本片段中出现,它将为该特定文本显示 1 的值,而为其他所有文本显示 0。因此,BoWs 往往是稀疏矩阵,其中大部分值都是零。

如果我们处理大量的文本语料库,BoW 中的列数(即单词数)可以相当大,如果我们还包括 n-gram,则更大。为了限制列数和返回矩阵的稀疏性,我们可以保留在多个文本中出现的单词;或者换句话说,我们可以保留至少在某个百分比文本中出现的单词。

为了减少列数和 BoW 的稀疏性,我们还应该处理与 Python 识别单词时相同大小写的单词——例如,小写。我们还可以通过移除停用词来减少列数和稀疏性。停用词是非常常用的单词,使句子流畅,但本身并不携带任何有用的信息。停用词的例子包括代词,如我、你和他,以及介词和冠词。

在这个菜谱中,我们将学习如何将单词转换为小写,移除停用词,保留具有最低可接受频率的单词,并使用scikit-learn的单个转换器CountVectorizer()一起捕获 n-gram:

如何操作...

让我们先加载必要的库并准备好数据集:

  1. 加载pandasCountVectorizer以及从scikit-learn的 dataset:

    import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    from sklearn.feature_extraction.text import (
        CountVectorizer
    )
    
  2. 让我们将 20 个新闻组数据集的训练集部分加载到 pandas DataFrame 中:

    data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了使结果更容易解释,让我们从文本变量中移除标点符号和数字:

    df['text'] = df['text'].str.replace(
        ‹[^\w\s]›,››, regex=True).str.replace(
        ‹\d+›,››, regex=True)
    

注意

要了解更多关于 Python 中的正则表达式,请点击此链接:docs.python.org/3/howto/regex.html

  1. 现在,让我们设置CountVectorizer(),使其在创建 BoW 之前将文本转换为小写,移除停用词,并保留至少在 5%的文本片段中出现的单词:

    vectorizer = CountVectorizer(
        lowercase=True,
        stop_words='english',
        ngram_range=(1, 1),
        min_df=0.05)
    

注意

要将 n-gram 作为返回列的一部分引入,我们可以将ngrams_range的值更改为,例如(1,2)。这个元组提供了不同 n-gram 的 n 值范围的上下边界。在(1,2)的情况下,CountVectorizer()将返回单个单词和两个连续单词的数组。

  1. 让我们调整CountVectorizer(),使其学习在 BoW 中应使用哪些单词:

    vectorizer.fit(df['text'])
    
  2. 现在,让我们创建 BoW:

    X = vectorizer.transform(df['text'])
    
  3. 最后,让我们将相应的特征名称与 BoW 一起捕获到 DataFrame 中:

    bagofwords = pd.DataFrame(
        X.toarray(),
        columns = vectorizer.get_feature_names_out()
    )
    

    通过这样,我们创建了一个包含单词作为列和每个文本中它们出现的次数作为值的pandas DataFrame。你可以通过执行bagofwords.head()来检查结果:

图 11.6 – 由 20 个新闻组数据集生成的 BoW DataFrame

图 11.6 – 由 20 个新闻组数据集生成的 BoW DataFrame

我们可以将这个 BoW 作为机器学习模型的输入。

它是如何工作的...

scikit-learn 的CountVectorizer()将一组文本文档转换为标记计数的矩阵。这些标记可以是单个单词或两个或更多连续单词的数组——即 n-gram。在这个菜谱中,我们从 DataFrame 中的文本变量创建了一个 BoW。

我们从scikit-learn加载了 20 个新闻组文本数据集,并使用 pandas 的replace()函数从文本行中移除了标点符号和数字,该函数可以通过 pandas 的str模块访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。然后,我们使用CountVectorizer()创建 BoW。我们将lowercase参数设置为True,在提取 BoW 之前将单词转换为小写。我们将stop_words参数设置为english以忽略停用词——也就是说,避免 BoW 中的停用词。我们将ngram_range设置为(1,1)元组,以仅返回作为列的单个单词。最后,我们将min_df设置为0.05,以返回至少出现在 5%的文本中的单词,换句话说,在 DataFrame 的 5%的行中。

在设置好转换器之后,我们使用了fit()方法让转换器找到满足先前标准的单词。最后,使用transform()方法,转换器返回一个包含 BoW 及其特征名称的对象,我们将它捕获在一个pandas DataFrame 中。

参见

更多关于CountVectorizer()的详细信息,请访问scikit-learn库的文档scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

实现词频-逆文档频率

词频-逆文档频率TF-IDF)是一个数值统计量,它捕捉了单词在考虑整个文档集合的情况下在文档中的相关性。这意味着什么?一些单词在文本文档中以及跨文档中都会出现很多次,例如,英语单词 theais 等。这些单词通常对文档的实际内容传达很少信息,并且不会使文本脱颖而出。TF-IDF 通过考虑单词在文档中出现的次数与在文档中出现的频率之间的关系来提供衡量单词重要性的方法。因此,常见的单词如 theais 将具有较低的权重,而更具体于某个主题的单词,如 leopard,将具有更高的权重。

TF-IDF 是两个统计量的乘积:词频tf)和逆文档频率idf),表示如下:tf-idf = tf × idf。tf 在其最简单形式中,是单个文本中单词的计数。因此,对于术语 t,tf 的计算为 tf(t) = count(t),并且基于文本进行确定。idf 是衡量单词在 所有 文档中普遍程度的一个指标,通常在对数尺度上计算。一个常见的实现如下:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><mi>i</mi><mi>d</mi><mi>f</mi><mfenced open="(" close=")"><mi>t</mi></mfenced><mo>=</mo><mrow><mrow><mi>log</mi><mo>(</mo></mrow></mrow><mrow><mrow><mfrac><mi>n</mi><mrow><mn>1</mn><mo>+</mo><mi>d</mi><mi>f</mi><mo>(</mo><mi>t</mi><mo>)</mo></mrow></mfrac><mo>)</mo></mrow></mrow></mrow></mrow></math>

在这里,n 是文档的总数,df(t) 是包含术语 t 的文档数。df(t) 的值越大,该术语的权重越低。如果一个单词在文本中出现的次数很多(高 tf)或在文本中出现的次数很少(高 idf),则该单词的重要性会很高。

注意

TF-IDF 可以与 n-gram 结合使用。同样,为了权衡 n-gram,我们将某个文档中 n-gram 的频率与跨文档中 n-gram 的频率相乘。

在这个菜谱中,我们将学习如何使用 scikit-learn 通过 n-gram 或不使用 n-gram 提取 TF-IDF 特征。

准备工作

scikit-learn 使用一种稍微不同的方式来计算 IDF 统计量:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mii</mml:mi>mml:mid</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mit</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mid</mml:mi>mml:mif</mml:mi>mml:mo(</mml:mo>mml:mit</mml:mi>mml:mo)</mml:mo></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>

这种公式确保了出现在所有文本中的单词获得最低权重 1。此外,在计算每个单词的 TF-IDF 之后,scikit-learn将特征向量(包含所有单词的向量)归一化到其欧几里得范数。有关确切公式的更多详细信息,请访问scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weightingscikit-learn文档。

TF-IDF 在创建术语矩阵时具有与 BoW 相同的特征——即高特征空间和稀疏性。为了减少特征数量和稀疏性,我们可以移除停用词,将字符转换为小写,并保留在最小百分比观察中出现过的单词。如果您不熟悉这些术语,请访问本章中的使用词袋和 n-gram 创建特征菜谱进行复习。

在这个菜谱中,我们将学习如何将单词转换为小写,移除停用词,保留具有最低可接受频率的单词,捕获 n-gram,然后使用 scikit-learn 的单个转换器TfidfVectorizer()返回单词的 TF-IDF 统计量。

如何做到这一点...

让我们先加载必要的库并准备数据集:

  1. 加载pandasTfidfVectorizer()以及从scikit-learn中的数据集:

    import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    from sklearn.feature_extraction.text import (
        TfidfVectorizer
    )
    
  2. 让我们将 20 个新闻组数据集的火车集部分加载到一个 pandas DataFrame 中:

    data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了使结果更容易解释,让我们从文本变量中移除标点符号和数字:

    df['text'] = df['text'].str.replace(
        ‹[^\w\s]›,››, regex=True).str.replace(
        '\d+','', regex=True)
    
  4. 现在,让我们设置scikit-learn中的TfidfVectorizer(),以便在创建 TF-IDF 度量之前,将所有文本转换为小写,移除停用词,并保留至少在 5%的文本片段中出现的单词:

    vectorizer = TfidfVectorizer(
        lowercase=True,
        stop_words='english',
        ngram_range=(1, 1),
        min_df=0.05)
    

注意

为了将 n-gram 作为返回列的一部分引入,我们可以将ngrams_range的值更改为,例如(1,2)。这个元组提供了不同 n-gram 的 n 值范围的上下界。在(1,2)的情况下,TfidfVectorizer()将返回单个单词和两个连续单词的数组作为列。

  1. 让我们拟合TfidfVectorizer(),以便它学习哪些单词应该作为 TF-IDF 矩阵的列引入,并确定单词的idf

    vectorizer.fit(df['text'])
    
  2. 现在,让我们创建 TF-IDF 矩阵:

    X = vectorizer.transform(df['text'])
    
  3. 最后,让我们将 TF-IDF 矩阵捕获到一个具有相应特征名称的 DataFrame 中:

    tfidf = pd.DataFrame(
        X.toarray(),
        columns = vectorizer.get_feature_names_out()
    )
    

    通过这样,我们创建了一个包含单词作为列和 TF-IDF 作为值的pandas DataFrame。您可以通过执行tfidf.head()来检查结果:

图 11.7 – 由 TF-IDF 产生的特征 DataFrame

图 11.7 – 由 TF-IDF 产生的特征 DataFrame

现在,我们可以使用这个词频 DataFrame 来训练机器学习模型。

它是如何工作的...

在这个菜谱中,我们通过使用 scikit-learn 的TfidfVectorizer()提取了至少存在于 5%的文档中的单词的 TF-IDF 值。

我们从scikit-learn加载了 20 个新闻组文本数据集,然后使用 pandas 的replace()从文本行中删除了标点符号和数字,这可以通过 pandas 的str访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。然后,我们使用TfidfVectorizer()为单词创建 TF-IDF 统计信息。我们将lowercase参数设置为True,在计算之前将单词转换为小写。我们将stop_words参数设置为english,以避免返回矩阵中的停用词。我们将ngram_range设置为(1,1)元组,以返回单个单词作为特征。最后,我们将min_df参数设置为0.05,以返回至少出现在 5%的文本或换句话说,在 5%的行中的单词。

在设置完转换器后,我们应用了fit()方法,让转换器找到最终项矩阵中需要保留的单词。使用transform()方法,转换器返回了一个包含单词及其 TF-IDF 值的对象,然后我们将其捕获到一个具有适当特征名称的 pandas DataFrame 中。现在,我们可以将这些特征用于机器学习算法。

另请参阅

有关TfidfVectorizer()的更多详细信息,请访问 scikit-learn 的文档:scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html

清洗和词干化文本变量

我们数据集中的某些变量来自自由文本字段,这些字段由用户手动填写。人们有不同的写作风格,我们使用各种标点符号、大写模式、动词变位来传达内容以及与之相关的情感。我们可以通过创建总结文本复杂度、关键词和文档中单词相关性的统计参数来从文本中提取(一些)信息,而无需费心阅读它。我们已在本章前面的菜谱中讨论了这些方法。然而,为了推导这些统计信息和聚合特征,我们首先应该清理文本变量。

文本清理或预处理包括标点符号删除、停用词消除、字符大小写设置和词干提取。标点符号删除包括删除不是字母、数字或空格的字符;在某些情况下,我们也会删除数字。停用词消除是指删除在我们语言中用于允许句子结构和流畅性的常见单词,但它们各自传达的信息很少或没有。例如,英语中的停用词包括诸如thea之类的冠词,以及诸如Iyouthey之类的代词,以及各种变形中常用的动词,如to beto have,以及诸如woulddo之类的助动词。

为了让计算机正确识别单词,还需要将所有单词设置为相同的格式,因为由于第一个单词中的大写T,计算机会将Toytoy识别为不同的单词。

最后,为了专注于文本的信息,我们不希望计算机在单词不同变形的情况下将单词视为不同。因此,我们将使用词干提取作为预处理流程的一部分。词干提取是指将每个单词还原为其词根或基础,使得单词playingplaysplayed变为play,本质上传达的是相同或非常相似的意义。

在本食谱中,我们将学习如何使用 pandas 和NLTK删除标点符号、停用词,将单词转换为小写,并执行词干提取。

准备工作

我们将使用NLTK的词干提取包来执行词干提取,该包包含不同的算法,可以从英语和其他语言中提取单词。每种方法在寻找单词的时使用的算法不同,因此它们可能会输出略微不同的结果。我建议您了解更多相关信息,尝试不同的方法,并选择适合您正在工作的项目的那个。

更多关于 NLTK 词干提取器的信息可以在www.nltk.org/api/nltk.stem.html找到。

如何操作...

让我们先加载必要的库并准备数据集:

  1. NLTK加载pandasstopwordsSnowballStemmer,以及从scikit-learn加载数据集:

    import pandas as pd
    from nltk.corpus import stopwords
    from nltk.stem.snowball import SnowballStemmer
    from sklearn.datasets import fetch_20newsgroups
    
  2. 让我们先将 20 个新闻组数据集的训练集部分加载到 pandas DataFrame 中:

    data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    

    现在,让我们开始文本清理。

注意

在执行本食谱中的每个命令后,通过执行例如print(df['text'][10])之类的命令来打印一些示例文本,以便您可以可视化对文本引入的变化。现在就做吧,然后在每个步骤之后重复该命令。

  1. 让我们先删除标点符号:

    df["text"] = df['text'].str.replace('[^\w\s]','')
    

小贴士

您还可以使用 Python 内置的string模块来删除标点符号。首先,通过执行import string来导入模块,然后执行df['text'] = df['text'].str.replace('[{}]'.format(string.punctuation), '')

  1. 我们也可以移除数字字符,只留下字母,如下所示:

    df['text'] = df['text'].str.replace(
        '\d+', '', regex=True)
    
  2. 现在,让我们将所有单词转换为小写:

    df['text'] = df['text'].str.lower()
    

    现在,让我们开始移除停用词的过程。

注意

如果你没有下载NLTK库的stopwords步骤 6可能会失败。请访问本章的技术要求部分以获取更多详细信息。

  1. 让我们创建一个函数,该函数可以将字符串分割成单词列表,移除停用词,并将剩余的单词重新连接成字符串:

    def remove_stopwords(text):
        stop = set(stopwords.words('english'))
        text = [word
        for word in text.split() if word not in stop]
        text = ‹ ‹.join(x for x in text)
        return text
    

注意

为了能够使用scikit-learn库的CountVectorizer()TfidfVectorizer()处理数据,我们需要文本以字符串格式存在。因此,在移除停用词后,我们需要将单词作为单个字符串返回。我们已经将 NLTK 库的停用词列表转换为一个集合,因为集合比列表扫描更快,这提高了计算时间。

  1. 现在,让我们使用第6 步中的函数从text变量中移除停用词:

    df['text'] = df['text'].apply(remove_stopwords)
    

    如果你想知道哪些单词是停用词,请执行stopwords.words('english')

    最后,让我们使用NLTKSnowballStemer来提取我们数据中的词干。

  2. 让我们为英语语言创建一个SnowballStemer实例:

    stemmer = SnowballStemmer("english")
    

小贴士

尝试在单个单词上使用词干提取器以查看其工作方式;例如,运行stemmer.stem('running')。你应该看到run作为该命令的结果。尝试不同的单词!

  1. 让我们创建一个函数,该函数可以将字符串分割成单词列表,对每个单词应用stemmer,并将词干列表重新连接成字符串:

    def stemm_words(text):
        text = [
            stemmer.stem(word) for word in text.split()
        ]
        text = ‹ ‹.join(x for x in text)
        return text
    
  2. 让我们使用第9 步中的函数来对数据中的单词进行词干提取:

    df['text'] = df['text'].apply(stemm_words)
    

    现在,我们的文本已经准备好根据字符和单词计数创建特征,以及创建 BoWs 或 TF-IDF 矩阵,正如本章前面的食谱所描述的。

    如果我们执行print(df['text'][10]),我们将在清理后看到一个文本示例:

    irwincmptrclonestarorg irwin arnstein subject recommend duc summari what worth distribut usa expir sat may gmt organ computrac inc richardson tx keyword ducati gts much line line ducati gts model k clock run well paint bronzebrownorang fade leak bit oil pop st hard accel shop fix tran oil leak sold bike owner want think like k opinion pleas email thank would nice stabl mate beemer ill get jap bike call axi motor tuba irwin honk therefor computracrichardsontx irwincmptrclonestarorg dod r
    

注意

如果你正在计算句子数,你需要在移除标点符号之前这样做,因为标点和大小写是定义每个句子边界的必要条件。

它是如何工作的...

在这个食谱中,我们从文本变量中移除了标点符号、数字和停用词,将单词转换为小写,并最终将单词还原到词根。我们使用 pandas 的replace()函数从文本变量中移除标点符号和数字,该函数可以通过 pandas 的str访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。或者,我们可以使用内置string包中的punctuation模块。

小贴士

在导入string后,在 Python 控制台中运行string.punctuation以查看将被替换为空字符串的符号。

接下来,通过 pandas 的字符串处理功能str,我们使用lower()方法将所有单词转换为小写。为了从文本中移除停用词,我们使用了NLTK中的stopwords模块,该模块包含了一组频繁出现的单词列表——即停用词。我们创建了一个函数,该函数接受一个字符串并使用 pandas 的str.split()将其拆分为单词列表,然后使用列表推导,我们遍历列表中的单词并保留非停用词。最后,使用join()方法,我们将保留的单词重新连接成字符串。我们使用 Python 内置的set()方法在NLTK停用词列表上提高计算效率,因为遍历集合比遍历列表要快。最后,通过 pandas 的apply(),我们将该函数应用于文本数据的每一行。

小贴士

在从NLTK导入stopwords后,在 Python 控制台中运行stopwords.words('english')以可视化将要被移除的停用词列表。

最后,我们使用NLTK中的SnowballStemmer对单词进行了词干提取。SnowballStemmer一次处理一个单词。因此,我们创建了一个函数,该函数接受一个字符串并使用 pandas 的str.split()将其拆分为单词列表。在列表推导中,我们逐个单词应用SnowballStemmer,然后使用join()方法将提取的词干单词列表重新连接成字符串。通过 pandas 的apply(),我们将该函数应用于 DataFrame 的每一行以提取词干。

在本食谱中我们执行的清洗步骤产生了包含原始文本的字符串,没有标点符号或数字,全部小写,没有常用词汇,并且使用词根而不是其变形。返回的数据可以用来推导特征,如计数字符、单词和词汇食谱中所述,或者创建 BoWs 和 TI-IDF 矩阵,如使用词袋和 n-gram 创建特征实现词频-逆文档频率食谱中所述。

如本食谱所示,清洗文本可能会根据文本的特征导致数据丢失,如果我们希望在创建 BoW 或 TF-IDF 矩阵后解释模型,理解词干的重要性可能并不那么直接。