在过去的几个版本中, NVIDIA cuDF 团队为用户定义函数( UDF )添加了几个新特性,这些特性可以简化开发过程,同时提高总体性能。在本文中,我将介绍新的 UDF 增强功能,并展示如何在自己的应用程序中利用它们:
- cuDF
Series.apply
API 及其使用方法 - cuDF
DataFrame.apply
API 以及如何根据“行”编写自定义项 - 使用两个 apply API 增强对缺失数据的支持
- 带有计时的真实用例示例
- 实际考虑、限制和未来计划
为 cuDF 系列应用 API
如果您不熟悉 pandas , series apply 是用于将任意 Python 函数映射到单个数据系列的主要入口点。例如,您可能希望使用已编写为 Python 函数的公式将摄氏温度转换为华氏温度。
下面是运行此代码的输出后的快速刷新:
从技术上讲,您可以在函数f
中编写任何有效的 Python 代码, pandas 在序列上循环运行函数。这使得apply
在 pandas 环境中非常灵活,因为任何 UDF 都可以成功应用,只要它能够成功处理所有输入数据,甚至是依赖于外部库或期望或返回任意 Python 对象的 UDF 。
但是,这种灵活性是以性能为代价的。由于各种原因,在长循环中运行 Python 函数并不是一种有效的策略(例如,从一开始就对 Python 进行解释)。因此,如果您的 UDF 更简单,例如那些对标量值进行纯数学运算的 UDF ,那么这种性能约束可能会令人沮丧。
幸运的是,这些用例正是 cuDF 构建的目的。最近在 UDF 范围内的 cuDF 改进促使引入等效的apply
API :
如果您熟悉 pandas ,您可以生成与使用 pandas 处理数值相同的结果。唯一显著的区别是,得到的数据总是 cuDF dtype
,而不是object
,这通常是熊猫的情况。
函数f
可以包含由纯数学运算或 Python 运算组成的任何 Python UDF 。 cuDF 基于通过 Numba 对函数的检查推断出适当的返回dtype
,并在 GPU 上编译和运行等效函数。
函数也可以编写为接受任意
虽然在 cuDF 中有其他方法可以使用自定义内核和其他方法实现相同的目标,但这种编写 UDF 的方法有助于将 GPU 从过程中抽象出来,这可以缩短从事快节奏、真实项目的数据科学家的开发时间。
到目前为止,我只讨论了基于Series
的数据的情况。也就是说,我已经向您展示了如何使用单个输入和输出编写 UDF 。然而,许多用例需要多列输入,这需要稍微不同的思考。
数据框架 UDF 和按行思考
期望多个列作为输入并生成单个列作为输出的 UDF 是 pandas DataFrame apply API 支持的函数集。
在这些情况下,第一个函数参数表示一行数据,而不仅仅是单个输入列中的一个值。我所说的 row 是指某种能够通过键控获取值的数据结构,其中键是列名,值是与该行中这些列的值相对应的标量。从概念上讲,这是在熊猫中使用iloc
时得到的结果:
数量的标量参数。在以下代码示例中,您可以看到支持args=
:
下面的代码示例显示了如何在 pandas 中编写和使用使用此类行对象的 UDF :
cuDF 现在,您可以在不重写自定义项的情况下完成确切的操作。
在应用这些函数时,需要注意的是,尽管 cuDF API 希望您以行的形式编写函数,但在执行此函数时,实际上并不涉及任何行。
cuDF 避免使用 for 循环,而是执行“假装”存在数据行的 CUDA 内核。通过一点魔法, Numba 知道如何编写一个合适的内核来获得与 pandas 相同的结果。因为没有循环,所以通过此 API 执行函数时应该会看到更高的性能。
使用 series 和 DataFrame 应用对缺失值的支持
从历史上看, cuDF 中的 UDF 并没有提供对缺失值的完全支持。这是由于 cuDF 内部的架构选择与 cuDF 记录哪些元素为空的方式有关,特别是它使用空掩码来节省内存。
pandas apply
API 的循环设计仅在数据包含空值时有效。如果数据中遇到 null , UDF 将接收特殊值pd.NA.
,如果特殊值未触发错误,则执行将正常进行。然而, cuDF 不是这样工作的,它需要一些额外的机器来支持相同的功能。如果使用 cuDF apply API ,您应该发现您的 UDF 以自然的方式处理空值:
您甚至可以在cudf.NA
单例上设置条件并获得预期的答案,或者直接从函数返回:
显然,这里的情况与行的情况相同: cuDF 实际上并不像 pandas 那样运行 Python 函数。相反,它使用了更多的 Numba 魔法将此类函数转换为等效的 CUDA 内核,然后返回结果。
在下一节中,我将查看一个真实的示例,并执行一些粗略的计时。
使用 apply 的真实示例
考虑一下这个场景:一个在线流媒体服务正在调查其订阅者中哪些部分的订阅时间最长。此外, leadership 还要求制定一个具体的细分方案,按年龄划分订阅者:
- 18 – 19
- 20 – 29
- 30 – 39
- 40 – 49
- 50 – 59
- 60 – 69
- 69 +
提供的数据只有两个字段:age
和days_subscribed
。
以下是 UDF 如何解决这个问题。首先,编写应用分组的按行自定义函数。接下来,获取结果,按组 ID 分组,并平均续订次数。
在此代码示例中,数据是随机生成的,因此您的里程数可能与实际答案不同。然而,它展示了这个过程。对代码的 UDF 部分进行计时涉及到创建一个变量pdf
到pdf = df.to_pandas
,并使用 IPython 完成粗略比较:
%timeit df.apply(f, axis=1) # 1.64 ms ± 34.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) %timeit pdf.apply(f, axis=1) # 19.2 s ± 63.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
虽然这不是官方的基准测试, CUDA 内核在这个特定的情况下平均快了四个数量级以上,它是在 32 GB V100 GPU 上运行的。
实际考虑、限制和未来
虽然这些 cuDF 改进代表了比以前的迭代更广泛的功能,但总有增长的空间。以下是为 cuDF 中的 apply 编写自定义项时要考虑的关键项目列表:
- JIT compilation. 第一次针对 cuDF 对象执行函数时,您会遇到编译正确的 CUDA 内核的开销影响。除非目标数据集的
dtypes
发生更改,否则该函数的后续使用不需要重新编译。 - dtype support. 到目前为止,
apply
中只支持数字dtypes
。然而,对其他类型的支持已经在路线图上,从字符串开始。 - External libraries. 常见的模式是在 pandas 中执行数据准备,然后使用外部库在 UDF 中为每一行进行处理。由于您无法将外部代码任意映射到 GPU ,因此目前不支持此操作。
总结
UDF 是快速解决特定问题的简单方法。在设计管道逻辑时,它们可以帮助您从单个数据的角度进行思考。通过这些新的 cuDF UDF 增强,目的是加快涉及 cuDF 的工作流的开发,并允许您快速原型化解决方案,以及重用现有业务逻辑。此外, null 支持允许您明确说明如何处理缺少的值,而不需要额外的处理步骤。
值得注意的是, UDF 是 cuDF 中积极开发的一个领域,目前正在进行更新。如果您选择像往常一样尝试这些新的 UDF 增强功能,我很乐意在评论部分了解您的体验。