首页
/ Polars polars-testing:DataFrame 与 Series 相等性断言工具及其实现原理

Polars polars-testing:DataFrame 与 Series 相等性断言工具及其实现原理

2026-09-05 12:40:30作者:何将鹤

polars-testing 是 Polars 仓库中的一个子 crate,定位为"Polars DataFrame 库的测试套件",为 Rust 侧编写单元测试提供了一组可配置的 Series/DataFrame 相等性断言工具,包括 assert_series_equal! 宏、assert_dataframe_equal! 宏、assert_schema_equal 函数以及两套比较选项结构体。读完本文,你将掌握这些断言 API 的完整参数与默认值、浮点容差比较的判定公式、对嵌套类型/分类类型/NaN 等特殊值的处理逻辑,以及它在 Polars 仓库内部(polars-sql 集成测试、polars-python Python 绑定)的真实使用方式。

一、polars-testing 是什么:定位与依赖结构

按照 crates/polars-testing/README.md 的描述,polars-testing 是 Polars 库的子 crate,"为 Rust 中的单元测试提供完整的测试能力"。它在 crates/polars-testing/Cargo.toml 中的包描述(description)为 "Testing suite for the Polars DataFrame library"。

Cargo.toml 的依赖声明可以推断出它的实现依赖面:

[dependencies]
polars-core = { workspace = true, features = ["dtype-array", "dtype-categorical", "dtype-struct"] }
polars-ops = { workspace = true, features = ["abs", "dtype-categorical", "is_close"] }
  • polars-core 开启了 dtype-arraydtype-categoricaldtype-struct 三个 feature,说明断言工具需要支持固定长度数组、分类类型和嵌套结构类型的比较;
  • polars-ops 开启了 is_close feature,这对应浮点近似比较底层使用的 is_close 函数(在 crates/polars-testing/src/asserts/utils.rs 中直接导入自 polars_ops::series::is_close)。

crate 的入口 crates/polars-testing/src/lib.rs 只有一行:pub mod asserts;,全部能力集中在 asserts 模块中,并在 crates/polars-testing/src/asserts/mod.rs 里统一再导出四个公共符号:

pub use utils::{
    DataFrameEqualOptions, SeriesEqualOptions, assert_dataframe_equal, assert_schema_equal,
    assert_series_equal,
};

此外 assert_series_equalassert_dataframe_equal 两个宏通过 #[macro_export] 直接挂在 crate 根上(见 crates/polars-testing/src/asserts/series.rscrates/polars-testing/src/asserts/frame.rs),因此两种导入方式都可以使用。

重要提示(继承自 README 原文):该 crate 不面向外部使用者。README 明确指出 "This crate is not intended for external usage",普通用户应使用主 polars crate。它主要是 Polars 仓库自身测试基建的一部分,仓库内部依赖它的地方有两处:

二、安装与基本导入

按照 README 给出的用法,在你的 Rust 项目 Cargo.toml 中声明依赖(版本号以 README 中列出的 0.52.0 为例,具体应以当前工作区版本为准):

[dependencies]
polars-testing = "0.52.0"

然后在 Rust 代码中导入:

use polars_testing::*;

宏也可以通过 $crate 路径在任意上下文展开(见 series.rs 中宏定义的 match $crate::asserts::assert_series_equal(...) 展开形式),因此不依赖特定的 use 路径。

三、assert_series_equal:Series 相等性断言

3.1 基本用法

assert_series_equal! 宏提供两种形式:使用默认选项、传入自定义选项。宏的文档注释(series.rs)中给出了可直接运行的示例:

use polars_core::prelude::*;
use polars_testing::assert_series_equal;
use polars_testing::asserts::SeriesEqualOptions;

// 创建两个要比较的 Series
let s1 = Series::new("a".into(), &[1, 2, 3]);
let s2 = Series::new("a".into(), &[1, 2, 3]);

// 使用默认选项断言
assert_series_equal!(&s1, &s2);

// 使用自定义选项断言
let options = SeriesEqualOptions::default()
    .with_check_exact(true)
    .with_check_dtypes(false);
assert_series_equal!(&s1, &s2, options);

断言不成立时宏会 panic! 并输出底层错误信息(宏展开中是 Err(e) => panic!("{}", e))。底层同名函数 assert_series_equal(left, right, options) -> PolarsResult<()> 则返回 PolarsResult,适合需要在非 panic 场景中使用的调用方(如 polars-python 的绑定)。

3.2 SeriesEqualOptions 全部参数与默认值

SeriesEqualOptions(定义于 utils.rs)控制比较行为,所有字段均为 pub 并配有对应的链式 builder 方法:

字段 类型 默认值 说明
check_dtypes bool true 是否要求数据类型一致
check_names bool true 是否要求 Series 名称一致
check_order bool true 是否要求元素顺序一致(false 时两侧先各自排序再比较)
check_exact bool true 浮点数是要求精确相等(true)还是容差近似相等(false
rel_tol f64 1e-5 浮点近似的相对容差
abs_tol f64 1e-8 浮点近似的绝对容差
categorical_as_str bool false 是否先把 Categorical 值转换为字符串再比较

默认值有对应的单元测试 test_series_equal_optionsseries.rs 中逐项验证。

3.3 比较流程与失败信息

assert_series_equalutils.rs)的检查顺序是确定的:

  1. 指针快速路径std::ptr::eq(left, right) 为真时直接通过;
  2. 长度:不等则报错 length mismatch(附两侧长度);
  3. 名称check_names 为真且名称不同则报 name mismatch
  4. 数据类型check_dtypes 为真且 dtype 不同则报 dtype mismatch
  5. 数值:委托给内部函数 assert_series_values_equal

值得注意的一个边界行为:当 check_dtypesfalse 且两侧 dtype 不同,但两个 Series 全部为 null 时,直接视为相等返回——即"两个全空 Series 不因底层类型差异而失败"(见 utils.rs 中 assert_series_values_equal 开头的短路逻辑)。

3.4 浮点容差公式与特殊值语义

check_exactfalse 且两侧均为浮点类型时,比较采用以下判定(源码注释中明确给出了公式,见 utils.rs):

|left - right| <= max(rel_tol * max(abs(left), abs(right)), abs_tol)

只要满足该式或数值完全相等,即认为在容差内;实际计算复用 polars-opsis_close 函数。此外还有三条独立的语义规则,分别由三个辅助函数实现:

  • null 必须逐位对齐assert_series_null_values_match 比较 left.is_null()right.is_null(),任何一位不一致即报 null value mismatch。也就是说近似比较放宽的是数值精度,不放宽 null 位置
  • NaN 模式必须一致assert_series_nan_values_match 只对浮点 Series 生效,NaN 出现的位置必须完全相同(位置不同报 nan value mismatch),而两侧相同位置都是 NaN 则视为相等(对应测试 test_series_nan_equal);
  • 超容差时给出反例assert_series_values_within_tolerance 失败时不是简单报"不等",而是过滤出所有超出容差的元素,把两侧的问题值一并打印在 values not within tolerance 错误里,便于定位。

无穷大则走精确比较路径:INFINITYNEG_INFINITY 会报 exact value mismatch(见 test_series_infinity_values_mismatch)。

3.5 嵌套类型(List/Struct)的递归比较

assert_series_values_equal 先用 not_equal_missing 得到逐位不等的掩码;若两侧 dtype 属于"包含浮点数的嵌套类型"(由 comparing_nested_floats 通过 unpack_dtypes 展开内部类型判断),则委托 assert_series_nested_values_equal 递归处理:

  • List/Array 类型:按行配对迭代,遇到 null 即失败;对每一行构造单元素 Series 后 explodeempty_as_null: true, keep_nulls: true),再递归调用 assert_series_values_equal,从而支持任意深度的嵌套列表(测试 test_deeply_nested_list_float_mismatch 验证了三层嵌套的浮点列表比较);
  • Struct 类型unnest 展开后按字段名逐列递归比较,字段值不同报 exact value mismatch

3.6 Categorical 按字符串比较

categorical_as_str 开启后,两侧 Series 会先经过 categorical_series_to_string 转换:内部通过 categorical_dtype_to_string_dtype 递归地把(包括嵌套在 List/Array/Struct 内的)Categorical dtype 改写为 String dtype,再 cast 成字符串 Series 后比较。这在跨全局字符串缓存(Categories::global())注册的分类列比较时特别有用,避免了分类索引顺序差异带来的假阳性。

四、assert_dataframe_equal:DataFrame 相等性断言

4.1 基本用法

宏文档注释(frame.rs)中的示例:

use polars_core::prelude::*;
use polars_testing::assert_dataframe_equal;
use polars_testing::asserts::DataFrameEqualOptions;

let df1 = df! {
    "a" => [1, 2, 3],
    "b" => [4.0, 5.0, 6.0],
}.unwrap();
let df2 = df! {
    "a" => [1, 2, 3],
    "b" => [4.0, 5.0, 6.0],
}.unwrap();

// 默认选项
assert_dataframe_equal!(&df1, &df2);

// 自定义选项
let options = DataFrameEqualOptions::default()
    .with_check_exact(true)
    .with_check_row_order(false);
assert_dataframe_equal!(&df1, &df2, options);

4.2 DataFrameEqualOptions 全部参数与默认值

字段 类型 默认值 说明
check_row_order bool true 行顺序是否必须一致(false 时两侧先用全部列排序再比较)
check_column_order bool true 列顺序是否必须一致
check_dtypes bool true 对应列的数据类型是否必须一致
check_exact bool false 浮点是否要求精确相等;默认即近似比较
rel_tol f64 1e-5 相对容差
abs_tol f64 1e-8 绝对容差
categorical_as_str bool false Categorical 是否按字符串比较

这里有一个容易踩坑的默认值差异:DataFrameEqualOptionscheck_exact 默认为 false(近似比较),而 SeriesEqualOptionscheck_exact 默认为 true(精确比较)(两个 Default 实现分别在 utils.rsutils.rs,默认值均有单元测试 test_dataframe_equal_options / test_series_equal_options 锚定)。因此 DataFrame 断言默认对浮点列容忍 1e-5 相对误差,而 Series 断言默认要求逐位精确。

4.3 比较流程

assert_dataframe_equalutils.rs)的检查顺序:

  1. 指针快速路径:同一对象直接通过;
  2. Schema 校验:调用 assert_schema_equal_impl 检查列名集合、列顺序(check_column_order)与列 dtype(check_dtypes);
  3. 高度(行数):不等报 height (row count) mismatch
  4. 行排序check_row_orderfalse 时,两侧 DataFrame 都用全部列 sortSortMultipleOptions::default())后再比较,实现行序无关的相等性;
  5. 逐列比较:取出每一列,物化为 Series 后调用 assert_series_values_equal(此处行序强制为已对齐,故 check_ordertrue);任一列失败则报错并带上列名value mismatch for column "...",同时打印两侧该列内容。

4.4 Schema 断言函数 assert_schema_equal

assert_schema_equal(left_schema, right_schema, check_dtypes, check_column_order)utils.rs)可以独立用于只验证 schema 而不用物化数据的场景(例如对 LazyFrame 的输出 schema 做断言)。其实现 assert_schema_equal_impl 的行为分四步:

  1. 快速路径:schema 相同直接返回;
  2. 列名集合校验:左有右无、右有左无的列都会分别收集,并输出可定位的错误,例如 columns mismatch: ["col3"] in left, but not in right(对应测试 test_dataframe_left_has_extra_column / test_dataframe_right_has_extra_column 锚定了这两条错误文案);
  3. 列顺序校验check_column_order 为真且同名列下标不一致时报 columns are not in the same order
  4. dtype 校验check_dtypes 为真时逐列比对,失败时报 dtypes do not match, differences only 并只列出存在差异的(列名, dtype)对,而不是整表 dump,输出更聚焦。

错误前缀通过 context 参数区分是 "Schemas" 还是 "DataFrames",方便排查断言失败发生在哪一层。

五、失败信息的语义速查

断言宏 panic 的信息直接来自底层 PolarsResult 错误,以下是从源码与 #[should_panic] 测试中可确认的完整文案集合,可用于编写回归断言或解析测试输出:

失败场景 错误文案(Series) 对应测试
长度不同 length mismatch test_series_length_mismatch
名称不同 name mismatch test_series_names_mismatch
dtype 不同 dtype mismatch test_series_dtype_mismatch
精确值不同 exact value mismatch test_series_value_mismatch_int
超出容差 values not within tolerance test_series_float_exceeded_tol
null 位置不同 null value mismatch test_series_check_exact_false_null
NaN 位置不同 nan value mismatch test_series_check_exact_false_nan
嵌套结构不同 nested value mismatch test_series_list_values_float_mismatch
高度不同(DataFrame) height (row count) mismatch test_dataframe_height_mismatch
列缺失(DataFrame) columns mismatch: [...] in left/right, but not in right/left test_dataframe_left_has_extra_column
列顺序不同(DataFrame) columns are not in the same order test_dataframe_column_order_mismatch
列 dtype 不同(DataFrame) dtypes do not match, differences only test_dataframe_dtype_mismatch
某列值不同(DataFrame) value mismatch for column "..." test_dataframe_value_mismatch

这些测试集中在 crates/polars-testing/src/asserts/series.rs(约 650 行测试,覆盖整数、字符串、浮点、NaN/null/inf、Categorical、嵌套 List/Struct、Datetime、Decimal、Binary 等类型)和 crates/polars-testing/src/asserts/frame.rs 中,可以直接运行 cargo test -p polars-testing 验证行为。

六、在仓库中的真实使用方式

1. polars-sql 集成测试crates/polars-sql/tests/functions_aggregate.rs 开头即 use polars_testing::asserts::assert_dataframe_equal;,用它把 SQL 聚合查询的实际结果与期望 df! 结果做全等断言;statements.rs 则用于校验各种 SQL 语句执行后的输出表。

2. polars-python 绑定crates/polars-python/src/testing/frame.rsDataFrameEqualOptionsassert_dataframe_equal / assert_schema_equal 包装成 pyo3 函数,参数签名(check_row_order, check_column_order, check_dtypes, check_exact, rel_tol, abs_tol, categorical_as_str)与 Rust 侧一一对应,错误经 PyPolarsErr 转换为 Python 异常。这使得 Python 测试套件(py-polars 的测试)与 Rust 测试共用同一套比较实现,避免两侧语义漂移。

3. 主 crate 集成polars 主 crate 的 Cargo 配置中同样声明了 polars-testing(见根 Cargo.toml 的 workspace 依赖),供 crates/polars/tests/it/ 下的 Rust 集成测试使用。

七、使用注意与限制

  1. 不面向外部使用:README 明确说明该 crate "not intended for external usage",对外部用户应使用主 polars crate。把它当作稳定公共 API 依赖存在风险,它的接口可能随内部测试需求重构。
  2. 默认值不对称:Series 断言默认精确比较,DataFrame 断言默认近似比较(check_exact 默认值不同),跨工具对比结果时要留意这一点。
  3. 近似比较不放宽 null/NaN 位置:即使 check_exact = false,null 与 NaN 的模式仍要求逐位一致,仅数值允许容差。
  4. 行序无关比较依赖排序check_row_order = false 时通过"全列排序后逐列比较"实现,要求列值可排序且排序结果能区分行;对全空或存在大量重复行的表,该方式仍然正确,但代价是两侧各排序一次。
  5. 嵌套类型中的 null 元素:嵌套 List 比较时,任一侧某行为 null 会直接报 nested value mismatch,即嵌套比较对内部 null 采取严格策略。

总体而言,polars-testing 的价值在于把 Polars 数据类型体系(浮点容差、null/NaN 语义、Categorical、嵌套 List/Struct、Decimal、Datetime 等)的"语义相等"沉淀成一套可配置、错误信息可定位、且被 Rust 集成测试与 Python 测试共享的断言基建。如果你在为 Polars 相关项目写 Rust 测试,参考 polars-sqlpolars-python 中的上述用法,即可复用这套语义一致的比较能力。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384