几乎每个数据框库都携带数百个操作方法。仅 pandas 一个库就在 DataFrame 上提供了超过两百个方法。pivot 和 melt 有什么区别?apply 和 map 是否相同?还有 transform、agg、applymap、pipe 这些方法,哪些是真正不同的操作,哪些只是同一操作穿上不同的外衣?没有一套理论框架来区分它们,最终只能靠死记硬背 API 文档,而不是真正理解其背后的结构。这种困境不仅影响学习效率,更制约了数据框系统的优化空间和可组合性。
从两百个方法到十五个算子
解决这个问题的关键在于寻找一种更底层的抽象。Petersohn 等人在《Towards Scalable Dataframe Systems》中做了一个非常有价值的尝试:他们分析了超过一百万个 Jupyter 笔记本,记录人们实际如何使用 pandas,然后提出了一套数据框代数。这套代数用大约十五个操作符就能表达两百多个 pandas 方法的全部功能。他们定义数据框为四元组 (A, R, C, D),其中 A 是数据数组,R 是行标签,C 是列标签,D 是列域向量。这个定义比简单的「表格」更精确,因为它捕捉到了数据框区别于关系型数据库表的特性:行和列都是有顺序、有标签的,并且被对称对待。你可以转置一个数据框,也可以把数据值提升为列标签,这些操作在 SQL 表中是无法实现的。
在这十五个算子中,有九个直接来自关系代数:SELECTION、PROJECTION、UNION、DIFFERENCE、CROSS PRODUCT/JOIN、DROP DUPLICATES、GROUPBY、SORT、RENAME。WINDOW 算子来自 SQL 扩展。而 TRANSPOSE、MAP、TOLABELS、FROMLABELS 这四个是数据框特有的,它们存在的原因正是数据框对称处理行列并允许数据在值和元数据之间流动的能力。Petersohn 的工作证明了一个重要事实:超过百分之八十五的 pandas API 都可以重写为这十五个算子的组合。然而,这里存在一个更深层次的问题:这十五个算子本身是否还能进一步分解?如果存在更小的真正原始操作集合,它们会是什么?
伴随三元素:Δ、Σ、Π 的数学结构
仔细审视 Petersohn 的算子表,一个有趣的模式浮现出来。某些算子改变模式,即改变存在哪些列以及这些列的类型;另一些算子保持模式不变,只影响行。如果只关注改变模式的那一类算子,它们可以分为三组。第一组是重组操作:你重新排列、选取或重命名列,数据本身不变,只有形状改变。这对应关系代数中的 PROJECTION 和 RENAME,在数据框中表现为 select 和 rename 操作。第二组是合并操作:你沿着某个键 Collapse 行,产生汇总结果或集合。这对应 GROUPBY 和 UNION,groupBy 后接 aggregate 就是典型例子。第三组是配对操作:你根据两个表之间的共享键找到匹配的行,然后把它们拼接成更宽的行。这对应 CROSS PRODUCT 和 JOIN,innerJoin 是最直接的例子。
这三个模式是否具有某种更深层的必然性?答案是肯定的,这个答案来自范畴论。Fong 和 Spivak 的《Seven Sketches in Compositionality》第三章提供了一个具体且面向数据库的范畴论介绍。其核心思想是将模式建模为范畴,实例则是从该范畴到集合范畴的函子。具体来说,一个模式由表和表之间的外键关系组成;一个实例为每个表分配一组行,为每个外键分配一个函数,满足一致性条件:当表 A 通过外键引用表 B,表 B 再引用表 C 时,查找表 A 中某行直接对应的表 C 中的行,必须与先找表 B 再找表 C 的结果一致。
关键定理来了:当你有两个模式之间的映射(也是一个函子)时,这个映射自然地诱导出三种数据迁移操作。Sigma(Σ) 处理合并:许多源行指向同一个目标,Sigma 收集目标处的所有数据。Delta(Δ) 处理重组:数据被限制或重命名以适应目标模式,不创造新数据,也不合并行。Pi(Π) 处理配对:找到同时满足所有共享键约束的元组,相当于数据库中的 JOIN。这三个操作通过伴随关系连接成 Σ ⊣ Δ ⊣ Π,这被称为伴随三元素。Sigma 最「慷慨」:合并所有数据。Pi 最「保守」:只保留完全匹配的元组。Delta 则走向另一个方向,限制数据而不创造或组合任何东西。伴随关系意味着这三个操作可以干净地组合:一个 Δ 步骤的输出是任何 Σ 或 Π 步骤的有效输入,反之亦然。这就是为什么你可以链式调用 select、join、groupBy 而模式总是能正确匹配。
Topos 结构:处理行级集合运算
伴随三元素解释了五个关系算子(PROJECTION、RENAME、GROUPBY、UNION、JOIN),但还有两个算子没有被覆盖:DIFFERENCE 和 DROP DUPLICATES。它们不属于模式之间的迁移,而是同一个模式上的实例操作。DIFFERENCE 计算两个具有相同模式的集合之间的补集,DROP DUPLICATES 则是计算行的像。范畴论如何处理这些?答案是 Topos 结构。
Topos 是具有特殊性质的范畴,它支持子对象的概念,配备有补运算和交运算。在数据框的语境下,一个模式的所有实例构成的范畴就是一个 Topos,因为每个表被赋予一组行,每个外键被赋予一个函数,这给出了所有必要的集合论结构。在 Topos 中,DIFFERENCE 对应于子对象的补运算,而 DROP DUPLICATES 对应于像因子分解。这些操作保持了模式不变,只改变哪些行存在,因此它们不属于 Δ、Σ、Π 的范畴,而是独立的第二层结构。
综合来看,关系算子的完整范畴论图景有两层:迁移函子层(Δ、Σ、Π)处理跨模式的结构变化,以及 Topos 结构层处理单一模式内的行级推理。模式保持不变的算子(SELECTION、SORT、WINDOW)则是这两层之外的独立存在,它们是同一模式上实例之间的态射,而不是模式之间的迁移。
类型系统的工程实践
这套理论为 API 设计提供了明确的原则:每个操作都应该有清晰的模式计算规则。Delta 给出的操作可以仅根据输入模式和操作参数就计算出输出模式,无需查看任何数据。这使得这类操作便宜、可预测,并且在优化器中可以安全地重排序。Sigma 操作需要显式声明分组键和聚合方式,输出模式是键列加上每个聚合产生的新列。Pi 操作的输出模式是键列(出现一次)加上两侧的非键列,每种 JOIN 变体只是对缺失匹配的不同处理策略。Topos 层的操作则保持模式不变,输入和输出的类型完全相同。
在实际实现中,Haskell 的类型系统可以编码模式并在编译期验证每一次转换。列名和类型在类型层面被追踪,任何模式不匹配都会导致编译错误。这样的设计确保了如果一个管道能够编译通过,那么其中的每一个模式转换都是有效的。
范畴论的价值在于它提供了一套统一且可证明的原理,将两百多个看似杂乱的方法压缩到一个很小的操作集合中,同时揭示了这些操作之间的深层联系。
资料来源:Fong & Spivak, 《Seven Sketches in Compositionality》;Petersohn et al., 《Towards Scalable Dataframe Systems》