D3 数据汇总统计全解:d3-array 的 22 个 summarize 函数(count、extent、quantile 等)深度指南
D3(当前仓库版本 7.9.0)不仅擅长把数据画出来,更擅长在画图之前把数据“算清楚”。本文以官方文档 Summarizing data 为主体,完整覆盖 d3-array 提供的所有汇总统计函数——从极值(min/max)、值域(extent)、集中趋势(mean/median/mode)、分位数(quantile)到离散程度(variance/deviation),逐一讲解其行为语义、缺省值处理规则与 accessor 用法,并结合本仓库源码与测试说明这些函数是如何进入 d3 总入口的。读完后你将能直接套用这些函数完成比例尺定域、分箱、分组排序等常见可视化前处理。
这些函数从哪里来:d3 伞包对 d3-array 的再导出
summarize.md 中出现的 d3.count、d3.extent、d3.quantile 等 API 并非定义在 d3 主包内,而是来自依赖项 d3-array。仓库 package.json 声明了 "d3-array": "^3.2.4",而 src/index.js 的第一行即为:
export * from "d3-array";
这意味着 d3 伞包(umbrella package)将 d3-array 的全部导出原样提升到顶层 d3 命名空间下,所以文档示例里才能直接写 d3.count(...)、d3.extent(...)。这一契约由测试文件 test/d3-test.js 守护:它遍历 package.json 的每个依赖模块,逐个断言 “d3 exports everything from ${moduleName}”,即任何 d3-array 中的导出若未出现在 d3 上都会导致测试失败。因此在任何使用 import * as d3 from "d3" 的项目中,本文介绍的全部函数都可直接以 d3. 前缀调用;若只安装 d3-array 子包,则去掉前缀使用。
三条贯穿全文的通用约定
在逐个函数之前,先归纳 summarize.md 中反复出现的三条规则,它们决定了 d3 汇总函数与原生 Math.min/reduce 的最大差异:
-
输入是任意可迭代对象(iterable)。参数名普遍写作 iterable 而非 array,
d3.count(new Set([1,3,5,7]))这类写法是合法的(见every/some章节示例),d3.every、d3.some的示例直接用Set演示了这一点。 -
可选的 accessor 函数。绝大多数函数接受第二个参数 accessor,其效果“等价于先调用
Array.from再计算”。accessor 每次接收可迭代对象的一个元素(惯写作d)与零基索引(i),返回参与统计的值。例如:d3.sum(penguins, (d) => d.body_mass_g) // 1437000 d3.mean(penguins, (d) => d.body_mass_g) // 4201.754385964912penguins是 d3 示例生态中的经典数据集(文档中 342 条有效体质量记录,总质量 1437000 克),alphabet(英文字母频率)同样是官方示例数据。 -
自动忽略缺失值。对数值型统计(
sum、mean、median、count、mode、cumsum、variance、deviation等),undefined、null和NaN会被跳过,这对含缺失数据的真实数据集非常有用:d3.sum([1, 2, 2, 2, NaN, 3, null]) // 10 d3.mean([1, 2, 2, 2, NaN, 3, null]) // 2 d3.median([1, 2, 2, 2, NaN, 3, null]) // 2这一特性还有一个精巧用法:在 accessor 里对不想要的元素返回
NaN,即可在统计中“隐形”排除它们(见下文min/max章节)。
计数与极值:count、min、max 及其 Index 变体
d3.count
返回可迭代对象中“有效数值”的个数,即统计非 null、非 NaN、非 undefined 的元素:
d3.count(penguins, (d) => d.body_mass_g) // 342
这是处理缺失数据时的第一步:先知道有多少条记录真正可参与统计。
d3.min / d3.max
返回给定可迭代对象中按**自然序(natural order)**取的最小/最大值:
d3.min([3, 2, 1, 1, 6, 2, 4]) // 1
d3.max([3, 2, 1, 1, 6, 2, 4]) // 6
与 Math.min/Math.max 相比有两个关键区别:
-
不做数值强制转换。对字符串
["20", "3"],d3.min的结果是"20"、d3.max的结果是"3"(字典序),而Math.min(20, 3)与Math.max(20, 3)则是 3 与 20。它同样直接支持字符串与日期对象:d3.min(["bob", "alice", "carol"]) // "alice" d3.min([new Date("2018-01-01"), new Date("2011-03-09")]) // 2011-03-09 d3.max(["bob", "alice", "carol"]) // "carol" d3.max([new Date("2018-01-01"), new Date("2011-03-09")]) // 2018-01-01 -
忽略
undefined、null、NaN:d3.min([3, 2, 1, NaN, 4]) // 1 d3.max([3, 2, 1, NaN, 4]) // 4
利用“accessor 返回 NaN 即被忽略”的规则,可以在 accessor 中过滤掉特定样本。文档给出的例子是:求字母表中除 Z 外最低频字母的频率:
d3.min(alphabet, (d) => d.frequency) // 0.00074
d3.min(alphabet, (d) => d.letter === "Z" ? NaN : d.frequency) // 0.00095
d3.max(alphabet, (d) => d.frequency) // 0.12702
d3.max(alphabet, (d) => d.letter === "E" ? NaN : d.frequency) // 0.09056
空输入(或 accessor 全部返回不可比较值)时,min 与 max 都返回 undefined:
d3.min([]) // undefined
d3.min(alphabet, (d) => d.doesnotexist) // undefined
d3.max([]) // undefined
d3.max(alphabet, (d) => d.doesnotexist) // undefined
d3.minIndex / d3.maxIndex
与 min/max 行为一致,但返回最小/最大值的索引而非值本身:
d3.minIndex([3, 2, 1, 1, 6, 2, 4]) // 2
d3.maxIndex([3, 2, 1, 1, 6, 2, 4]) // 2
配合索引即可取回原始对象,等价于“按 accessor 找最小/最大元素”:
d3.minIndex(alphabet, (d) => d.frequency) // 25
alphabet[d3.minIndex(alphabet, (d) => d.frequency)] // {letter: "Z", frequency: 0.00074}
d3.maxIndex(alphabet, (d) => d.frequency) // 0
alphabet[d3.maxIndex(alphabet, (d) => d.frequency)] // {letter: "E", frequency: 0.12702}
自定义比较:least、greatest 及其 Index 变体
min/max 固定按自然序比较,而 least/greatest 允许传入比较器(comparator),从而在任意度量上取“最小/最大元素”(返回的是元素本身,而非 accessor 的值)。
d3.least / d3.greatest
d3.least(alphabet, (a, b) => a.frequency - b.frequency) // {letter: "Z", frequency: 0.00074}
d3.least(alphabet, (a, b) => b.frequency - a.frequency) // {letter: "E", frequency: 0.12702}
注意一个重要的重载规则:如果比较器只接受一个参数,它会被解释为 accessor,返回值之间再按自然序比较:
d3.least(alphabet, (d) => d.frequency) // {letter: "Z", frequency: 0.00074}
d3.least(alphabet, (d) => -d.frequency) // {letter: "E", frequency: 0.12702}
若完全不传比较器,默认使用 d3.ascending:
d3.least(alphabet.map((d) => d.frequency)) // 0.00074
greatest 与其对称:
const array = [{foo: 42}, {foo: 91}];
d3.greatest(array, (a, b) => a.foo - b.foo); // {foo: 91}
d3.greatest(array, (a, b) => b.foo - a.foo); // {foo: 42}
d3.greatest(array, (d) => d.foo); // {foo: 91}
当可迭代对象中没有可比较元素(即比较器把元素与自身比较返回 NaN)时,least/greatest 返回 undefined,例如 d3.least([]) // undefined。
d3.leastIndex / d3.greatestIndex
返回最小/最大元素的索引,参数规则与 least/greatest 相同;无元素可比较时返回 -1,未指定比较器时默认 ascending:
const array = [{foo: 42}, {foo: 91}];
d3.leastIndex(array, (a, b) => a.foo - b.foo); // 0
d3.leastIndex(array, (a, b) => b.foo - a.foo); // 1
d3.leastIndex(array, (d) => d.foo); // 0
d3.greatestIndex(array, (a, b) => a.foo - b.foo); // 1
d3.greatestIndex(array, (a, b) => b.foo - a.foo); // 0
d3.greatestIndex(array, (d) => d.foo); // 1
四者速记:min/max 按自然序取 accessor 的最小/最大值;least/greatest 按自定义比较器取元素;*Index 后缀则取下标。
值域:extent —— 比例尺定域的标配
d3.extent([3, 2, 1, 1, 6, 2, 4]) // [1, 6]
d3.extent(alphabet, (d) => d.frequency) // [0.00074, 0.12702]
extent 一次返回 [min, max] 对,规则与 min/max 相同;无可比较值时返回 [undefined, undefined]:
d3.extent(alphabet, (d) => d.doesnotexist) // [undefined, undefined]
extent 在整个 d3 生态中的存在感远超统计本身——它是最常用的比例尺 domain 来源。本仓库 docs/getting-started.md 的最小柱状图示例中就有:
const y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
此外,d3-scale 的 nicing(如 linear 比例尺 与 time 比例尺 的 .nice())被明确推荐给“用 extent 从数据计算 domain”的场景,因为数据驱动得到的边界往往不规则;d3.bin 的默认 domain 访问器也是 extent。可以说 extent 是“从原始数据走到比例尺/坐标轴”的枢纽函数。
众数:d3.mode
返回出现次数最多的值,忽略 undefined、null、NaN:
d3.mode([1, 2, 2, 2, 3, 3]) // 2
d3.mode(penguins, (d) => d.island) // "Biscoe"
两个细节:出现次数并列时返回先出现的那个值;无可比较值时返回 undefined。与 min/max 不同,mode 对字符串、对象键等离散值同样适用(penguins 按 island 分组取众岛的例子就是典型用法)。
聚合与集中趋势:sum、mean、median
d3.sum
d3.sum([1, 2, 2, 2, NaN, 3, null]) // 10
d3.sum(penguins, (d) => d.body_mass_g) // 1437000
忽略 undefined/null/NaN;若可迭代对象中没有任何数字,返回 0。若需要更高的浮点精度(例如累加大量 0.1 这类场景),文档指向 fsum——它使用 Kahan 补偿求和,d3.fsum([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) 恰好等于 1,速度较慢但可无缝替换 d3.sum。
d3.mean
算术平均值,忽略缺失值;无数字时返回 undefined:
d3.mean([1, 2, 2, 2, NaN, 3, null]) // 2
d3.mean(penguins, (d) => d.body_mass_g) // 4201.754385964912
d3.median / d3.medianIndex
median 使用中位数(0.5 分位数),实现采用 R-7 方法(R 语言与 Excel 的默认分位数算法),忽略缺失值;无数字时返回 undefined:
d3.median([1, 2, 2, 2, NaN, 3, null]) // 2
d3.median(penguins, (d) => d.body_mass_g) // 4050
medianIndex 与 median 相同,但返回中位数左侧元素的索引:
d3.medianIndex([1, 2, 2, 2, NaN, 3, null]) // 2
中位数比均值抗离群值,这在 d3 的分组统计里很常见——例如 docs/d3-array/group.md 展示用 d3.groupSort 按各组的 d3.median(D, (d) => d.body_mass_g) 对企鹅物种排序;d3.bin 也用 d3.median(values) 作为按中位数切分的分箱阈值示例。
累计和:d3.cumsum 与高精度 fcumsum
d3.cumsum([1, 1, 2, 3, 5]) // [1, 2, 4, 7, 12]
d3.cumsum(penguins, (d) => d.body_mass_g) // [3750, 7550, 10800, 10800, …]
返回与原序列等长的 Float64Array,忽略 undefined 与 NaN;若没有任何数字则返回全零。需要精度保障时,可用 fcumsum 替换(d3.fcumsum([1, 1e-14, -1]) // [1, 1.00000000000001, 1e-14],普通 cumsum 在此例中会丢失 1e-14)。
分位数:quantile、quantileIndex、quantileSorted
quantile(iterable, p, accessor) 返回 p-分位数,p 在 [0, 1] 区间:中位数即 p=0.5,上下四分位即 p=0.25/0.75。实现采用 R-7 方法(R 与 Excel 的默认约定):
const numbers = [0, 10, 30];
d3.quantile(numbers, 0); // 0
d3.quantile(numbers, 0.5); // 10
d3.quantile(numbers, 1); // 30
d3.quantile(numbers, 0.25); // 5
d3.quantile(numbers, 0.75); // 20
d3.quantile(numbers, 0.1); // 2
注意 0.25 分位数得到 5 而非 0 与 10 的中点——这正是 R-7 位置内插的结果。quantileIndex(array, p, accessor) 返回 p 分位数左侧的索引;quantileSorted(array, p, accessor) 与 quantile 相同但要求输入已排序,优点是 accessor 只对被触及的元素调用(适合 accessor 昂贵的场景,可先配合 d3.sort 排序)。
quantile 的另一处重要下游是 d3.scaleQuantile:该比例尺把采样 domain 排序后视为“离散值总体”,再按 range 的数量切分分位点,其计算内核就是本文的 d3.quantile。
排名:d3.rank
返回与输入等长的秩数组,即每个值在排序后可迭代对象中的零基索引:
d3.rank([{x: 1}, {}, {x: 2}, {x: 0}], d => d.x); // [1, NaN, 2, 0]
d3.rank(["b", "c", "b", "a"]); // [1, 3, 1, 0]
d3.rank([1, 2, 3], d3.descending); // [2, 1, 0]
三条规则:nullish 值(null/undefined)被排到末尾且秩为 NaN(第一例中缺少 x 的元素即如此);并列值取相同秩,定义为“该值第一次出现的位置”("b" 两次都得 1);第二参数可传 accessor(如 d => d.x)或比较器(如 d3.descending),缺省为 d3.ascending。
离散程度:variance 与 deviation
variance 返回总体方差的无偏估计(即样本方差,除以 n-1),实现采用 Welford 在线算法(单次遍历、数值稳定);少于两个数字时返回 undefined,忽略 undefined/NaN:
d3.variance(penguins, (d) => d.body_mass_g)
deviation 则是对应的标准差,定义为“偏差校正方差(bias-corrected variance)的平方根”,同样少于两个数字返回 undefined、同样忽略缺失值。两者都支持 accessor,效果等价于先 Array.from。
谓词检验:every 与 some
d3.every(new Set([1, 3, 5, 7]), x => x & 1) // true
d3.some(new Set([0, 2, 3, 4]), x => x & 1) // true
两者与 Array.prototype.every/some 语义等价,但接受任意可迭代对象(示例直接用 Set),并且短路求值:every 遇到第一个非真值即返回,some 遇到第一个真值即返回。
空输入行为速查表
d3 汇总函数对“没有可用数据”的返回值并不统一,实践中最常踩坑的正是这里。按 summarize.md 的原始约定整理如下:
| 函数 | 无可比较/无数字时返回 |
|---|---|
min / max |
undefined |
least / greatest |
undefined |
leastIndex / greatestIndex |
-1 |
extent |
[undefined, undefined] |
mode |
undefined |
sum |
0 |
mean / median |
undefined |
cumsum |
全零数组 |
variance / deviation(少于 2 个数) |
undefined |
据此,基于 mean/median/min/max 的下游逻辑(如比例尺 domain)都应当先判空,而不是假定一定拿到数字。
小结:把 summarize 放进 D3 工作流
回到 d3-array 模块索引 的视角,“Summarizing data —— Compute summary statistics” 只是 d3-array 的十个主题之一,但在可视化流水线中它出现的频率最高:extent 喂给比例尺 domain 并配合 nice() 圆整(ticks)、quantile 支撑分位数比例尺(quantile scale)、median 与 quantile 用于分箱阈值和分组排序(bin、group)。掌握本文的 22 个函数——尤其是“忽略 null/NaN”“accessor 与单参数比较器重载”“*Index 变体”与 R-7 分位数约定这几条通用规则,并对照 API 总览 中的 Summarize 章节索引检索具体函数,就能覆盖绝大多数“画图之前先统计”的场景;对精度敏感的场景再切换到 add.md 的 fsum/fcumsum 高精度版本。
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