跳到正文
册页

图表

BoxPlot - 箱线图

一个度量在各个分类上的分布范围。

用法

什么时候用它

「这东西波动有多大」,而且要一次看六个。要看单个分布的形状,用 Histogram;观测点少到能全画出来,用 ScatterChart。

来自@misoto22/folio/charts

TSX
import { BoxPlot } from '@misoto22/folio/charts'

说明

一个度量在每个分类上的分布——中位数、中间那一半、触及的范围,以及落在范围之外的那些点。

当问题是「这东西波动有多大」时,就该拿它;而且它回答这个问题所花的墨水,只有一个分类一张直方图的十分之一。六条响应时间分布画成六个箱子,一屏就排得下;画成六张直方图,根本排不下。

箱线图藏起来的是多峰。 一个箱子就是五个数,而五个数分不出一个峰和两个峰。一个双峰分布——一条走缓存的快路径和一条走数据库的慢路径,一个平均值里裹着两拨人——画出来的箱子,和一个中心落在同一位置的平滑分布一模一样,读者没有任何办法从图上看出来:箱子正中间那个值,几乎没有谁真的取到过。它同时还藏起了样本量:六个点画出的箱子和六千个点画出的箱子长得毫无分别,所以 count 值得一路带着,也所以在要比较中位数时值得把缺口打开——缺口是真的会去读 count 的。当问题是某一条分布的形状时,改用 <Histogram>;当观测少到可以全部画出来时,改用 <ScatterChart>,把点一个个摆上去。

Recharts 在这里是配得上的:这些箱子需要一根带真实刻度的共享数值轴、一根分类轴、一层网格和一个 tooltip——这已经是一张笛卡尔图表的大半了。它没有的是箱形标记,所以每个箱子都是画在一根区间柱上的自定义图形——柱子负责提供分类带和刻度,而里面那个字形是我们自己的。

结构

BoxPlot anatomy
部件说明
图框必填ChartFigure 的那个 <figure>,当没有分类能通过解析时会给出空状态。empty={false} 则保留坐标轴,留给那种“空本身就是结论”的图。
箱子必填<BoxPlot.Boxes>:一个分类一根从最小值跨到最大值的区间条,完全不上色,图形画在它上面。Recharts 提供分类带和刻度;箱体、中位数横线、须线和离群点都是这个包自己画的。
值轴<BoxPlot.YAxis>,而且它刻意不锚在零上——箱线图比较的是分布,为了在柱长上老实而把取值域拉到零,只会把每个箱子压成同一条像素带。它该讲的那份老实,是一根带标签的坐标轴,而它有。
凹口notch 在中位数处把箱体收进去,收进的量是 1.58 倍 IQR 除以 n 的平方根——一个画在它所属形状内部的置信区间。它要求每个箱子都有 count;没有 count 的箱子画出来是方的,而且对此只字不提。
离群点showOutliers,默认打开。超出 Tukey 的 1.5 倍 IQR 栅栏的点,一个点画一个,而须线随后停在栅栏之内最极端的那个观测上、而不是停在栅栏本身,所以没有哪根须线会声称一个数据里并不存在的读数。
隐藏数据表必填那张 sr-only 表格带着每个分类完整的五个数,外加离群点的个数,所以不看图形也完全读得懂这张图。

实践建议

推荐

  • 每个箱子都要带上 count。六个观测画出来的箱子和六千个观测画出来的一模一样,而 count 也是凹口要读的东西——没有它,notch 会被接受,然后悄无声息地什么都不做。
  • 预先算好的汇总,要说明它是哪一套分位数规则算出来的。这个组件在汇总原始数值时用的是 R type 7,在 [1, 2, 3, 4] 上它把下四分位数放在 1.75,而“取下半部分中位数”那套规则放在 1.5——同一份数据在两套规则下就是两张不同的图。
  • 手上有原始数值时,就把原始数值交给它,别给汇总。一趟下来它会套上 Tukey 栅栏、把离群点分出去、并把 count 填好,所以那五个数和那些点不会各走各的。

避免

  • 一个箱子分不出一个峰和两个峰。一条同时含着缓存路径和数据库路径的延迟序列,画出来的箱子和一个中心相同的光滑分布画出来的一模一样,而那个箱子的正中间,是一个几乎没有任何取值落在上面的数。形状才是问题时,那是 Histogram。
  • values 数组为空的分类会被直接丢掉——没有箱子,没有刻度,也没有表格行——因为汇总它得不到任何可画的东西。要了七个箱子,图上渲染出六个,而对第七个只字不提。
  • 一个观测画出来的是一个没有箱子的箱子:q1、中位数和 q3 是同一个数,IQR 为零,整个图形塌成一根横线。它不是错误,也不是一个分布,所以要在调用处挡掉。

示例

默认

四个分布,占的墨水和四根柱子一样多,交进来的是原始观测值——图表会把它们排序、按 R 的 type 7 取四分位数,并让每根须停在仍落在 Tukey 1.5 倍 IQR 栅栏之内的最极端读数上,于是悉尼的 402 和法兰克福的 588 会掉出来变成点,而不是把须拉到一个没人测到过的值上。五个数说不出来的是形状:一个有两个峰的地区,画出来的箱子和一个中心落在同一位置的平滑分布一模一样。它们也说不出样本量,因为十个读数画出的箱子和一万个读数画出的箱子长得毫无分别——而这正是 count 和 BoxPlot.Boxes 的 notch 存在要修的东西。数值轴没有锚在零上,因为为了在柱长上诚实而把它拽到零,会把每一个箱子压平成一条像素带。

Response time by regionOne box per edge location, 24 hours
Response time by region
nameMinimumLower quartileMedianUpper quartileMaximumOutliers
Sydney182195.75207219.252361
Singapore244259271.5287.753180
Frankfurt310326.5341358.753791
Oregon268280.25290.5305.753410

方向

长分类名正是 horizontal 存在所针对的场景:放在柱子底下,ap-southeast-2 得旋转才装得下,而放在一行旁边就能横着读完。坐标轴只组合一次、然后自己解析自己——XAxis 和 YAxis 各自读取 orientation,在分类角色和数值角色之间互换,而网格也会翻转它画哪一组线——所以那个开关只改一个属性,树里别的什么都没变。这也是为什么这里两根轴都没带 label:一个单位属于哪根轴,是随 orientation 变的,而一个钉在 YAxis 上的标签,最后会去命名那些地区。

Response time by region
Response time by region
nameMinimumLower quartileMedianUpper quartileMaximumOutliers
ap-southeast-2182195.75207219.252361
ap-southeast-1244259271.5287.753180
eu-central-1310326.5341358.753791
us-west-2268280.25290.5305.753410

带缺口

已经算好了才送来的摘要,这是它的另一种形态——一个数仓回给你的是分位数,从来不是原始行。count 是画一个缺口所需要的东西:那个收窄是 1.58 倍 IQR 除以根号 n,所以两个不重叠的缺口大致是一个「这两个中位数不同」的 95% 检验,而 Variant B 的缺口很宽,是因为它的样本是 190,对着别人的 4,000。这正是朴素的箱子所藏起来的那个比较,因为 190 个读数画出的箱子和 4,200 个读数画出的箱子长得毫无分别。这个缺口被夹在四分位数以内,而不是被允许把箱子折得里外翻转,所以一个小到做不了这个比较的样本,会收成一条细缝,而不是读起来像一处渲染故障。

Checkout latency by variantNotch width falls with the square root of the sample
Checkout latency by variant
nameMinimumLower quartileMedianUpper quartileMaximumOutliers
Control1802142362623180
Variant A1762082292553100
Variant B1842262582913560

类型

TSX
export type BoxPlotOrientation = 'vertical' | 'horizontal'
export type BoxPlotDatum = BoxPlotSample | (BoxPlotSummary & { values?: never })

无障碍

  • 一个箱子就是五个数,而五个数分不出一个峰和两个峰。双峰分布画出来的箱子,和一个中心相同的光滑分布一模一样——组件把这一点写进了自己的说明里,而不是塞在脚注里。
  • 它同样隐藏样本量:六个点画出来的箱子和六千个点画出来的一样。所以 count 值得带上,而在比较中位数时一定要打开 notched。
  • 原始数值用 Tukey 栅栏归纳,这件事写在页面上而不是默认读者知道——换一套栅栏规则,同一份数据会画出不同的离群点。
  • 隐藏的表格视图带着每个分类完整的五个数,所以不看图形也读得懂这张图。