跳到正文
册页

图表

Histogram - 直方图

一个分布长什么形状。

用法

什么时候用它

要弄懂一个分布——两个聚集、一道硬底、在超时值上堆成一摞。几个分布要并排比较,那是 BoxPlot 的活。

来自@misoto22/folio/charts

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

说明

一个分布的形状——分量压在哪里、往哪边偏、里面是不是不止一个峰。

它回答的正是 <BoxPlot> 从构造上答不了的那些:两簇聚集、一道硬下限、一堆积压在超时值上的观测。要把好几个分布并排比较时,去拿箱线图;要把某一个分布看懂时,拿这一个。

直方图的形状是它分箱宽度的属性,而不只是它数据的属性。同一批数字切成八个分箱和切成八十个分箱,是两张不同的图;两个峰之间的那道空隙,挪一下分箱边界就能造出来,也能抹掉。这不是待修的缺陷,分箱本来就是这么回事,能守住的办法是说清楚这张图是按哪条规则画的——这里的默认是 Freedman–Diaconis——并且在相信某个特征之前多看几个宽度。不等宽的分箱还多一个陷阱:在 frequency 下,底层发生率相同时,宽一倍的分箱就站得高一倍,而 mode="density" 存在的意义就是把这件事纠正过来。

Recharts 在这里挣得的位置是坐标轴、网格和 tooltip,但不包括那些柱子:柱状图的柱子是按分类定位的,而直方图的柱子是由一个连续的度量来定位、并且决定宽度的。所以横轴是数值轴,每根柱子都由它自己的两条边画出来——正因如此,一个不等宽的分箱才能保持它真实的宽度,而不是被压平成邻居旁边一个等宽的槽位。

结构

Histogram anatomy
部件说明
图框必填ChartFigure 的那个 <figure>,当分箱之后没有桶留下时会给出空状态——零个有限观测得到的就是这个结果。
柱子必填<Histogram.Bars>,每根都由它所属那个桶自己的两条边画出来,而不是塞进一个等宽的分类槽位,所以宽度不均的桶才能是它真实的宽度。radius 默认是 0,和 BarChart 不同:一个圆角会在两个本该相接的桶之间画出一道缝。
度量轴必填<Histogram.XAxis>,数值轴,从第一个桶的下边界一直走到最后一个桶的上边界。
计数轴<Histogram.YAxis>。在 mode="frequency" 下它是计数;在 mode="density" 下它是计数除以 n 再除以宽度,此时所有柱子围出的面积为 1。
分箱规则它不是屏幕上的任何一个标记,却是这张图里后果最重的部分:bins 接受一个桶数、或者明确的边界,默认是 Freedman-Diaconis 并封顶在 200 个桶,四分位距为零时退回 Sturges。明确给出的边界同时也是一个范围——落在第一条和最后一条边界之外的观测没有桶可归,它会被计进 tooltip 的占比、以及表格里的 Below 或 Above 行,而不是被丢掉。
隐藏数据表那张 sr-only 表格把每个桶的两条边印成行表头,计数印在旁边——这是一张分箱图能给出的唯一精确读数,因为每根柱子代表的是一个区间、而不是一个值。落在明确边界之外的观测有自己的 Below 行和 Above 行,因为它们在任何地方都没有柱子。

实践建议

推荐

  • 把分箱规则写在页面上。同一批数切成 8 个桶和切成 80 个桶,是两张不同的图,而两个峰之间的那道谷,挪动一条边界就能造出来、也能抹掉——所以在相信一个特征之前,多看几种宽度。
  • 只要桶宽不均,就设 mode="density"。在 frequency 下,同样的底层密度,宽一倍的桶会高一倍,而从指标后端拿来的、预先计数好的桶,正是一头栽进这个陷阱里的。
  • values 和 data 给一个,绝不要两个都给。两个都来时 data 胜出,于是 values 数组既没有被谁分箱、也没有被谁画出来,而且任何地方都没有警告。

避免

  • 不要把 tooltip 里的占比读成在各根柱子里的占比。它是在样本里的占比,所以一组桶加起来是 96%,说的是另外那 4% 落到了你自己给的边界之外——而这恰恰是一个按画出来的桶去算的占比永远给不出的读数,因为那种算法总是加到 100。
  • 不要用它来比较多个分布。两张直方图叠在一起会互相遮挡,六张并排又放不下;那是 BoxPlot 的事,它每个分布只花十分之一的墨。
  • 不要以为自动规则保住了你要的分辨率。Freedman-Diaconis 是拿四分位距去除的,所以一个中间很紧、尾巴很长的分布,会要求画出几万根不到一个像素宽的柱子——200 个桶的封顶把它变成一张粗糙的直方图,而不是一个卡死的标签页,而一张粗糙的直方图是另一张图。
  • 不要把只有一个取值的分布读成一个形状。所有观测都落在同一个数上时它照样画得出来:两条边界变成那个值加减二分之一,结果是一根老实的柱子,而它不是一个分布。

示例

默认

一个分布的形状,而这正是箱线图从构造上就展示不了的:这 75 个样本里有两个峰——一条热缓存路径和一条冷的——而同样这些数字画出来是一个整整齐齐的箱子,对哪一个都只字未提。放任不管时,图表按 Freedman–Diaconis 分箱,即 2 倍 IQR 乘以 n 的负三分之一次方,它读的是四分位距而不是全距,所以一个远处的离群点没法把这幅画拉成一根高柱加四十个空槽。要像这里的描述那样,说清是哪条规则画出了这张直方图,因为这个形状既是数据的属性,也同样是分箱宽度的属性。

Request durationBinned by Freedman–Diaconis
Request duration
labelCount
62 – 10337
103 – 1446
144 – 18522
185 – 2265
226 – 2673
267 – 3082

分箱宽度

同样这些数字,来四遍。bins 收的是等宽分箱的个数,而挪动它就是在挪动这幅画:五个的时候两个峰并成一个,四十个的时候形状消解成一排计数为一的柱子,而 auto 把这个选择交还给 Freedman–Diaconis。这不是一个该被调掉的缺陷——分箱本来就是这么回事——所以能守住的办法,是像标题那样说清是哪条规则画出了这幅画,以及在相信「两个峰之间那道空隙」这类特征之前,多看几个宽度。

Request duration — Freedman–Diaconis
Request duration — Freedman–Diaconis
labelCount
62 – 10337
103 – 1446
144 – 18522
185 – 2265
226 – 2673
267 – 3082

密度

已经数好了才送来的分箱,宽度是某个指标后端挑的,而且随着数值增大越来越宽。在 frequency 下,100 到 250 那个分箱读起来像一个真实的肩部,而它之所以在那儿,只是因为那个分箱比第一个宽十五倍——一个更宽的分箱,在同样的底层发生率下会收进更多观测。density 把每个计数除以 n 乘以分箱宽度,于是这些柱子围出的面积为 1,那条尾巴也就塌回数据真正说的样子。只要分箱不等宽,就去拿它;只要两张样本量不同的直方图必须相互比较,也去拿它。

Request duration — frequency
Request duration — frequency
labelCount
0 – 10480
10 – 25610
25 – 50540
50 – 100420
100 – 250310
250 – 500120
500 – 1,00040

类型

TSX
export type HistogramMode = 'frequency' | 'density'

无障碍

  • 形状是分箱宽度的属性,不只是数据的属性:同一批数切成 8 个桶和切成 80 个桶,是两张不同的图。画出这张图的规则——默认 Freedman–Diaconis——写在页面上。
  • 横轴是数值轴,每根柱子由它自己的两条边画出来,所以宽度不均的桶就是它真实的宽度,而不会被压成和邻居一样的等宽槽位。
  • mode="density" 修的正是不等宽分箱制造的陷阱:在 frequency 下,同样的底层密度,宽一倍的桶会高一倍。
  • 隐藏的表格视图印出每个桶的两条边和它的计数——这是一张分箱图唯一能给出的精确读数。