Polars 中直接调用 NumPy 通用函数(ufunc):表达式与 Series 互操作实战指南
Polars 中直接调用 NumPy 通用函数ufunc表达式与 Series 互操作实战指南【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polarsPolars 的表达式与 Series 支持直接接收 NumPy 的通用函数universal functions简称 ufunc与广义通用函数generalized ufunc。当 Polars 自身没有提供某个数学函数时你不必跳出查询引擎、不必逐行循环 Python 解释器只需像np.log(pl.col(x))这样把 NumPy 函数嵌进表达式里就能继续享受列式columnar向量化的处理速度。本文以 numpy-functions.md 为主线结合 py-polars 中Series.__array_ufunc__、Expr.__array_ufunc__的实现与互操作测试讲清 ufunc 在 Polars 中的正确用法、底层分派机制以及缺失值null相关的关键边界。为什么需要在 Polars 中调用 NumPy 函数Polars 内置了大量表达式函数覆盖数学、字符串、时间等常用领域但它不可能也不必要为每个 NumPy 数学函数都提供一个原生别名。Polars 表达式对 NumPy ufunc 的开放支持意味着你可以直接复用 NumPy/SciPy 生态中数百个经过验证的向量化函数通过 NumPy 的 C 级向量化执行路径依然保持快速列式运算而不是退化为逐元素的 Python 回调函数可以出现在select、with_columns、filter、group_by().agg()乃至rolling等任何表达式上下文中并与 Polars 的惰性查询优化协同工作。一句话概括官方文档的核心承诺如果某个函数 Polars 没有提供你可以使用 NumPy并且仍然通过 NumPy API 获得快速的列式操作。最小可运行示例给所有列取对数官方指南给出的示例位于 numpy-example.py完整代码如下import polars as pl import numpy as np df pl.DataFrame({a: [1, 2, 3], b: [4, 5, 6]}) out df.select(np.log(pl.all()).name.suffix(_log)) print(out)执行结果数值为ln自然对数精确到小数点后 6 位shape: (3, 2) ┌──────────┬──────────┐ │ a_log ── Float64 │ b_log ── Float64 │ ├──────────┼──────────┤ │ 0.0 │ 1.386294 │ │ 0.693147 │ 1.609438 │ │ 1.098612 │ 1.791759 │ └──────────┴──────────┘这个例子的几个要点pl.all()展开为 DataFrame 中每一列对应的表达式np.log被逐个应用到每个列表达式上使用.name.suffix(_log)为输出列重命名避免与原始列名冲突df.select(...)在 eager立即执行模式下同样生效——从源码与测试看ufunc 表达式在DataFrame与LazyFrame上均可使用。表达式中 ufunc 的底层分派机制Expr.__array_ufunc__是让np.log(pl.col(a))这类写法成立的关键协议实现在 expr.py。当 NumPy 发现调用对象不是ndarray时会回调该协议方法。其分派逻辑可以概括为仅支持method __call__其他调用方式如.reduce、.accumulate会抛出NotImplementedError若输入只有一个且为Expr、且无额外关键字参数则直接分派为inputs[0].map_batches(ufunc, is_elementwise...)即把 ufunc 作为逐元素elementwise批处理函数交给 Polars 执行引擎若存在多个Expr输入例如np.arctan2(pl.col(v), pl.col(u))或混有标量/数组则会先逐个求值为 Series再一次性调用 ufunc 本身。自定义 ufunc 的提醒对于带signature的自定义 ufunc典型如 numbaguvectorize产物在表达式上下文中会触发一个CustomUFuncWarning定义于 exceptions.py。其含义是原生 NumPy/SciPy ufunc 以is_elementwiseTrue分发是安全的但自定义 ufunc 在group_by场景下无法保证按组正确切分因而会被降级为is_elementwiseFalse。若你的自定义 ufunc 确实需要逐元素执行官方建议直接显式使用map_batches。对应测试可参考 test_ufunc_expr.py其中包括 ufunc 输入不位于首参数np.power(2.0, pl.col(a))、惰性查询LazyFrame、以及滚动窗口内np.log(pl.col(A)).mean()与原生pl.col(A).log().mean()结果一致性等场景。Series 层面的互操作普通 ufunc 与广义 ufunc除了表达式Series也实现了完整的 NumPy 互操作协议因此你可以直接对一列数据调用 NumPy 函数import polars as pl import numpy as np s pl.Series(a, [1.0, 2.0, 4.0]) print(np.exp(s)) # 逐元素调用返回 SeriesSeries.__array_ufunc__的实现位于 series.py其行为特征如下对多 chunk 的 Series 会先原地 rechunkrechunk(in_placeTrue)保证底层数据连续便于向量化执行仅支持输出一个一维数组的 ufuncufunc.nout ! 1会直接报错输出会直接写入预先分配的缓冲区避免中间拷贝字符串 Series 传入np.char.*等字符串 ufunc 前会先被转换为定长字符串类型见__array__实现中的相关处理。广义 ufuncgeneralized ufunc同样受支持——它们通过signature如(n)-(n)声明对整段数组而非单个元素做运算。来自 numba 的guvectorize函数就是典型代表相关验证见 test_ufunc_series.py。返回类型的自动推导Polars 在调用 ufunc 前会做一次细致的类型协商见 series.py用np.result_type(*args)求出能让所有输入参数安全统一的最小字符码遍历该 ufunc 所有已注册的输入/输出类型结合supported_numpy_char_code过滤掉 Polars 不支持的类型用np.can_cast找到第一个可安全转换的 ufunc dtype作为最终执行 dtype。这意味着np.power作用于 UInt8 列时默认保持 UInt8 输出作用于 Float64 列时保持 Float64同时你也可以像np.power(pl.col(a), 2, dtypenp.uint16)这样通过dtype关键字显式覆盖输出类型。以上行为在 test_ufunc_series.py 中被逐一断言覆盖了 UInt8/Int8/…/Float64 多种 dtype、np.exp保留 null、多 chunk Series 运算、以及np.maximum的空值传播。缺失值位图最容易踩的坑Polars 用**独立的位图bitmask**来跟踪缺失值null这一点与 NumPy 完全不同——NumPy 数组没有 null 概念通常只能用np.nan之类的哨兵值近似。详见 missing-data.md。Polars 的 null 位图并不会自动传给 NumPy这会导致两类问题普通逐元素ufunc如np.exp、np.cos、np.divide等逐个元素运算、互不依赖计算结果本身不受 null 影响。Polars 会先对整个含空值位的数据执行 ufunc计算完成后再依据原始有效性掩码把对应位置的输出重新置为 null从而保证空值语义不丢失实现见 series.py。广义 ufunc如np.convolve、窗口类归约它们的计算依赖整段数组、是聚合/全局性质的null 位图一旦丢失输入就会退化为含哨兵值的普通数值导致结果残缺或错误——而且这种错误通常难以察觉。因此 Polars 采取的策略是直接拒绝只要传入广义 ufunc 的 Series 含有 null就会抛出ComputeError错误信息明确提示cant pass a Series with missing data to a generalized ufunc并指引阅读缺失值处理文档以移除或填充缺失数据。import polars as pl import numpy as np s pl.Series(f, [1.0, 2.0, 3.0, None], dtypepl.Float64) # 逐元素 ufunc安全null 会自动传播 print(np.exp(s)) # 输出结果中对应位置仍为 null # 广义 ufunc直接报 ComputeError而不是返回错误结果 # np.convolve(s, [1.0, 1.0]) # ❌ ComputeError这一保护行为在测试 test_generalized_ufunc_missing_data 中被显式验证测试用一个(n)-(n)的 gufunc 输入含 null 的 Series期望它抛出匹配cant pass a Series with missing data to a generalized ufunc的ComputeError。测试注释点明了设计动机即便某些例子碰巧没问题在一般情况下比如整数输入求mean()缺失数据会让 NumPy 给出错误结果因此不能假定函数能正确处理缺失值。需要时显式转换to_numpy当确实需要把数据交给不受控的 NumPy 函数处理时官方建议先用to_numpy把 Series 转换成ndarray。转换过程中缺失值会被替换为np.nan。to_numpy的完整签名与语义见 series.pywritable是否要求返回可写的数组。由于底层 Arrow 数据不可变置为True会强制拷贝allow_copy是否允许为转换拷贝内存。置为False时一切非零拷贝的转换都会失败。该转换在满足整数/浮点/Datetime/Duration/Array 类型、无 null、单 chunk、writableFalse四个条件时为真正的零拷贝返回的数组直接指向 Series 底层的值缓冲区——测试 test_to_numpy_series.py 通过比较to_arrow().buffers()[1].address与arr.__array_interface__[data][0]的指针地址来断言这一点。一旦含 null数据必须被拷贝以填入np.nan此时应把这种显式转换视作离开 Polars 语义边界的明确操作。关于兼容范围的补充说明可用函数范围NumPy 官方列出的大量可用 ufunc指数、对数、三角函数、双曲函数、位运算、比较与逻辑运算等都能在这种模式下使用Polars 支持的数值字符码之外的 exotic 类型会被自动跳过。仅__call__语义对 Series 直接调用 ufunc 时只有普通调用__call__被实现ufunc 的.reduce、.accumulate、.outer等方法会抛出NotImplementedError此类聚合语义请改用 Polars 原生表达式或map_batches表达。数值正确性优先于猜测宁可让含 null 的广义 ufunc 直接报错也不返回看起来像是对的错误结果——这是贯穿上述实现与测试的明确设计取舍。小结一张速查表场景推荐用法空值行为表达式内调用逐元素 ufuncdf.select(np.log(pl.all()))null 自动保留并传播Series 直接调用逐元素 ufuncnp.exp(pl.Series([1.0, None, 3.0]))null 经掩码恢复null_count()不变表达式/Series 调用广义 ufunc允许但输入含 null 会抛ComputeError先drop_nulls/填充或改用map_batches把 Series 交给外部 NumPy 代码arr s.to_numpy()null 替换为np.nan注意可能触发拷贝综合来看Polars 对 NumPy ufunc 的支持不是简单的兼容垫片而是一套完整的互操作协议Expr.__array_ufunc__把函数接进查询计划Series.__array_ufunc__负责类型协商、缓冲区复用与空值语义修复测试套件test_ufunc_expr.py、test_ufunc_series.py则把每一类行为固化为可回归的契约。理解了这套机制后你便可以在Polars 负责整体查询、NumPy 补足函数生态的组合中游刃有余。如需继续深入可阅读该用户指南的表达式目录主页 expressions/index.md以及讨论空值处理的 missing-data.md。【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考