d3 全精度浮点求和:d3.Adder、d3.fsum 与 d3.fcumsum 实战指南
本篇技术指南围绕 D3 中「全精度浮点数加法」这一专题展开,覆盖 d3.Adder 累加器类、d3.fsum 高精度求和与 d3.fcumsum 高精度累积求和三个 API 的完整用法、参数语义与典型示例。读完本文,你将理解 IEEE 754 双精度加法为什么会丢失精度、D3 提供了哪些手段来补偿这种误差,并能在数据汇总(如体重总和、金额累加)场景下正确选用 fsum/fcumsum 与 sum/cumsum 两套 API。
为什么需要“全精度”加法
JavaScript 的 Number 采用 IEEE 754 双精度浮点表示。当一个大数与一个极小的数相加时,小数部分可能因位数有限而被直接舍掉,例如在普通加法下,1 + 1e-14 的结果仍然是 1,1e-14 这一项“消失”了。如果之后再做 1 - 1,最终结果会是 0,而不是数学上正确的 1e-14。同样地,把 0.1 连续累加 10 次,朴素加法会得到 0.9999999999999999,而不是数学上的 1。
D3 针对这个问题提供了专门的 API。对应文档 docs/d3-array/add.md 开篇即点明其定位:Add floating point numbers with full precision(以全精度方式对浮点数求和)。
在 D3 主包中,这些 API 来自 d3-array 子模块:package.json 声明的依赖为 "d3-array": "^3.2.4",而 src/index.js 通过 export * from "d3-array" 将其完整透传。因此当你以常规方式引入主包时,d3.Adder、d3.fsum、d3.fcumsum 都可直接调用:
import * as d3 from "d3";
// d3.Adder、d3.fsum、d3.fcumsum 均由 d3-array 重新导出
d3.Adder:可复用的全精度累加器
Adder 是一个类形式的累加器,适合在“数据分批到达、需要边到边加”的场景中复用同一个累加状态。
new Adder()
const adder = new d3.Adder();
创建一个初始值为 0 的新累加器。
adder.add(number)
adder.add(42)
把指定的 number 加到累加器当前值上,并返回累加器自身(this),因此支持链式调用:
const total = new d3.Adder().add(0.1).add(0.1).add(0.1);
console.log(+total); // 0.3
adder.valueOf()
adder.valueOf() // 42
返回累加器当前值的 IEEE 754 双精度表示。文档特别指出,它的最佳用法是短写形式 +adder(一元加号触发类型强制转换),或者显式地 Number(adder):
const adder = new d3.Adder();
for (const x of measurements) adder.add(x);
const total = +adder; // 等价于 adder.valueOf() 与 Number(adder)
d3.fsum:一次性高精度求和
fsum 是对任意 iterable 求全精度总和的函数,是朴素 d3.sum 的高精度替代品:
d3.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) // 1
这正是朴素加法会输出 0.9999999999999999 的场景——fsum 通过补偿机制保留了每一步的舍入误差,最终还原出数学上精确的 1。
与 d3.sum 相同,fsum 也支持可选的 accessor 函数,用于对对象数组求和。accessor 会被调用三次参数:元素 d、下标 i 和整个数组 data,其返回值参与累加:
d3.fsum(penguins, (d) => d.body_mass_g) // 1437000
文档给出的取舍建议是:Although slower, d3.fsum can replace d3.sum wherever greater precision is needed(fsum 更慢,但在需要更高精度的任何地方都可以替换 d3.sum)。也就是说它不是免费的——精度换性能,二者按场景取舍。
作为对照,d3.sum 的文档明确说明其语义:返回给定数字可迭代对象的和,忽略 undefined、null 和 NaN,若可迭代对象中不含任何数字则返回 0:
d3.sum([1, 2, 2, 2, NaN, 3, null]) // 10
d3.sum(penguins, (d) => d.body_mass_g) // 1437000
需要提醒的是,fsum 文档本身未逐条罗列缺失值处理细节;在含缺失数据的真实数据上使用时,建议先结合 d3-array 的源码与测试 确认边界行为,或与 d3.sum 的结果交叉验证。
d3.fcumsum:高精度累积求和
fcumsum 返回一个 Float64Array,其中第 i 项是前 i + 1 个数的全精度前缀和:
d3.fcumsum([1, 1e-14, -1]) // [1, 1.00000000000001, 1e-14]
这个例子完整展示了补偿求和的价值:
- 朴素加法:
1 + 1e-14→1(小项被舍入吞掉),再1 + (-1)→0; fcumsum:中间结果保留了1e-14的误差项,最终第三项精确还原为1e-14。
同样支持 accessor 形式,返回的 Float64Array 长度与输入一致:
d3.fcumsum(penguins, (d) => d.body_mass_g) // [3750, 7550, 10800, 10800, 14250, …]
文档建议:Although slower, d3.fcumsum can replace d3.cumsum when greater precision is needed。对照 d3.cumsum 的文档,其语义为返回与输入等长的 Float64Array 前缀和,忽略 undefined 与 NaN(便于跳过缺失数据),无可加数字时返回全零:
d3.cumsum([1, 1, 2, 3, 5]) // [1, 2, 4, 7, 12]
d3.cumsum(penguins, (d) => d.body_mass_g) // [3750, 7550, 10800, 10800, …]
fcumsum 的典型应用是绘制带间隙的堆叠面积图或运行总量曲线:当序列中混有极大和极小的量级(如大额与小额订单交替)时,朴素 cumsum 的前缀和可能出现“平台被拉平、小增量丢失”的视觉误差,而 fcumsum 能保持单调性可信。
API 选型对照
| 维度 | d3.sum | d3.fsum | d3.cumsum | d3.fcumsum |
|---|---|---|---|---|
| 精度 | 朴素 IEEE 754 累加 | 全精度(误差补偿) | 朴素 IEEE 754 累加 | 全精度(误差补偿) |
| 返回类型 | number | number | Float64Array | Float64Array |
| 缺失值(undefined / null / NaN) | 明确忽略 | 文档未逐条罗列,见上文说明 | 明确忽略 | 文档未逐条罗列 |
| 无数字时 | 返回 0 | — | 返回全零 | — |
| 性能 | 快 | 文档注明“slower” | 快 | 文档注明“slower” |
| 替代关系 | — | 需要更高精度时替代 d3.sum | — | 需要更高精度时替代 d3.cumsum |
经验法则:金额、质量、测量值等“绝对误差敏感”的汇总用 fsum/fcumsum;对精度不敏感的计数类统计用 sum/cumsum 以获得更好的性能。
仓库层面的佐证
- 版本前提:package.json 显示本仓库为 D3
7.9.0,依赖d3-array ^3.2.4,且要求 Node>=12(engines字段);fsum系列 API 在 D3 v7 的 ESM 结构下随d3-array一同导出,以上用法适用于该版本区间。 - 历史沿革:CHANGES.md 记录了
d3.fsum与d3.Adder自 D3 早期大版本起即作为 d3-array 的新增能力引入,说明二者属于 D3 数组工具箱中的“老牌”高精度成员,而非近期实验性 API。 - 文档链接可验证性:test/docs-test.js 会对
docs目录下所有 Markdown 的相对链接与锚点做爬取校验(链接必须指向真实存在的.md文件及{#anchor}锚点)。本文引用的docs/d3-array/summarize.md#sum、#cumsum等锚点正是该测试所保障的目标形式。 - 实现归属:
fsum/fcumsum的具体实现位于d3-array包(文档中标注的源文件为该包内的src/fsum.js,Adder与fsum同源),本仓库通过 src/index.js 的export * from "d3-array"透传,因此本仓库内不含该算法的本地副本;从文档描述与 D3 一贯的实现路线看,其原理属于补偿求和(compensated summation)一类算法——在每一步加法中额外追踪被舍入掉的误差并在后续步骤回填,这正是fcumsum([1, 1e-14, -1])末项能精确得到1e-14的原因。
小结
D3 用一组小而完整的 API 解决了浮点累加的精度痛点:d3.Adder 提供可链式复用的累加状态,d3.fsum 与 d3.fcumsum 分别对应 d3.sum 与 d3.cumsum 的全精度版本,且保持相同的 accessor 调用约定(d、i、data 三参数)。代价是更慢的速度,收益是在“大数 + 小数”混合序列下不失真。汇总数据前先问一句:这个总和里有没有量级相差超过 10 个数量级的项?如果有,切换到 f 前缀版本是零成本的正确性升级。
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 StartedRust0622
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