Polars polars-testing:DataFrame 与 Series 相等性断言工具及其实现原理
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-array、dtype-categorical、dtype-struct三个 feature,说明断言工具需要支持固定长度数组、分类类型和嵌套结构类型的比较;polars-ops开启了is_closefeature,这对应浮点近似比较底层使用的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_equal 与 assert_dataframe_equal 两个宏通过 #[macro_export] 直接挂在 crate 根上(见 crates/polars-testing/src/asserts/series.rs 与 crates/polars-testing/src/asserts/frame.rs),因此两种导入方式都可以使用。
重要提示(继承自 README 原文):该 crate 不面向外部使用者。README 明确指出 "This crate is not intended for external usage",普通用户应使用主 polars crate。它主要是 Polars 仓库自身测试基建的一部分,仓库内部依赖它的地方有两处:
- crates/polars-sql/Cargo.toml 将其声明为依赖,
polars-sql的集成测试(如 crates/polars-sql/tests/functions_aggregate.rs、crates/polars-sql/tests/statements.rs)通过use polars_testing::asserts::assert_dataframe_equal;来校验 SQL 查询结果; - crates/polars-python/Cargo.toml 同样依赖它,并在 crates/polars-python/src/testing/frame.rs 中把断言能力包装成
pyfunction(assert_dataframe_equal_py、assert_schema_equal_py),供 py-polars 的 Python 测试套件调用,Python 侧因此能复用与 Rust 侧完全一致的比较语义。
二、安装与基本导入
按照 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_options 在 series.rs 中逐项验证。
3.3 比较流程与失败信息
assert_series_equal(utils.rs)的检查顺序是确定的:
- 指针快速路径:
std::ptr::eq(left, right)为真时直接通过; - 长度:不等则报错
length mismatch(附两侧长度); - 名称:
check_names为真且名称不同则报name mismatch; - 数据类型:
check_dtypes为真且 dtype 不同则报dtype mismatch; - 数值:委托给内部函数
assert_series_values_equal。
值得注意的一个边界行为:当 check_dtypes 为 false 且两侧 dtype 不同,但两个 Series 全部为 null 时,直接视为相等返回——即"两个全空 Series 不因底层类型差异而失败"(见 utils.rs 中 assert_series_values_equal 开头的短路逻辑)。
3.4 浮点容差公式与特殊值语义
当 check_exact 为 false 且两侧均为浮点类型时,比较采用以下判定(源码注释中明确给出了公式,见 utils.rs):
|left - right| <= max(rel_tol * max(abs(left), abs(right)), abs_tol)
只要满足该式或数值完全相等,即认为在容差内;实际计算复用 polars-ops 的 is_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错误里,便于定位。
无穷大则走精确比较路径:INFINITY 对 NEG_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 后
explode(empty_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 是否按字符串比较 |
这里有一个容易踩坑的默认值差异:DataFrameEqualOptions 的 check_exact 默认为 false(近似比较),而 SeriesEqualOptions 的 check_exact 默认为 true(精确比较)(两个 Default 实现分别在 utils.rs 与 utils.rs,默认值均有单元测试 test_dataframe_equal_options / test_series_equal_options 锚定)。因此 DataFrame 断言默认对浮点列容忍 1e-5 相对误差,而 Series 断言默认要求逐位精确。
4.3 比较流程
assert_dataframe_equal(utils.rs)的检查顺序:
- 指针快速路径:同一对象直接通过;
- Schema 校验:调用
assert_schema_equal_impl检查列名集合、列顺序(check_column_order)与列 dtype(check_dtypes); - 高度(行数):不等报
height (row count) mismatch; - 行排序:
check_row_order为false时,两侧 DataFrame 都用全部列sort(SortMultipleOptions::default())后再比较,实现行序无关的相等性; - 逐列比较:取出每一列,物化为 Series 后调用
assert_series_values_equal(此处行序强制为已对齐,故check_order传true);任一列失败则报错并带上列名: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 的行为分四步:
- 快速路径:schema 相同直接返回;
- 列名集合校验:左有右无、右有左无的列都会分别收集,并输出可定位的错误,例如
columns mismatch: ["col3"] in left, but not in right(对应测试test_dataframe_left_has_extra_column/test_dataframe_right_has_extra_column锚定了这两条错误文案); - 列顺序校验:
check_column_order为真且同名列下标不一致时报columns are not in the same order; - 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.rs 将 DataFrameEqualOptions 与 assert_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 集成测试使用。
七、使用注意与限制
- 不面向外部使用:README 明确说明该 crate "not intended for external usage",对外部用户应使用主
polarscrate。把它当作稳定公共 API 依赖存在风险,它的接口可能随内部测试需求重构。 - 默认值不对称:Series 断言默认精确比较,DataFrame 断言默认近似比较(
check_exact默认值不同),跨工具对比结果时要留意这一点。 - 近似比较不放宽 null/NaN 位置:即使
check_exact = false,null 与 NaN 的模式仍要求逐位一致,仅数值允许容差。 - 行序无关比较依赖排序:
check_row_order = false时通过"全列排序后逐列比较"实现,要求列值可排序且排序结果能区分行;对全空或存在大量重复行的表,该方式仍然正确,但代价是两侧各排序一次。 - 嵌套类型中的 null 元素:嵌套 List 比较时,任一侧某行为 null 会直接报
nested value mismatch,即嵌套比较对内部 null 采取严格策略。
总体而言,polars-testing 的价值在于把 Polars 数据类型体系(浮点容差、null/NaN 语义、Categorical、嵌套 List/Struct、Decimal、Datetime 等)的"语义相等"沉淀成一套可配置、错误信息可定位、且被 Rust 集成测试与 Python 测试共享的断言基建。如果你在为 Polars 相关项目写 Rust 测试,参考 polars-sql 与 polars-python 中的上述用法,即可复用这套语义一致的比较能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00