书里这一段是装 R、读数据、跑 hist()。 这台机器上没有 R,但分箱的逻辑一模一样:定边界、数落点、画柱子。 箱宽是画图人定的,不是数据自带的。同一组数,箱数 4 看起来像单峰,箱数 30 全是毛刺。 任何直方图都附带一个没写出来的决定:箱宽。看图时该问一句「换个宽度还成立吗」。
上一篇画了 bins=10 的版本。这一篇专门折腾箱宽,看形状怎么被它左右。 书里用 R 的 hist(),这台机器上没有 R,也没有 Excel。 做法改写清楚:R 那几步(装、读、画)在文末逐条对照,能等价的部分用标准库 + matplotlib 复现。
一、书里那三步,这里怎么走
| 书里的步骤 | R 的做法 | 这里的做法 |
|---|---|---|
| 装软件 | 下载安装 R | 本沙箱不执行(没装 R) |
| 读数据 | read.csv() | 直接在脚本里用列表生成 |
| 画直方图 | hist(x) | 标准库分箱 + matplotlib 出图 |
三行对照表的意思是:方法能复现,环境不能。装 R 只是省掉手写分箱这一步。 如果你自己电脑上装了 R,hist(DATA) 一行就能出图,结论和下面完全一致。
二、箱数一变,形状就变
箱数 4(每箱 6.25 个点):计零 0 个,局部峰 0 个——整张图是一条递减的斜坡。 箱数 10(每箱 2.5 个点):计零 2 个,局部峰 2 个——双峰带凹陷出来了。 箱数 30(每箱 0.83 个点):计零 14 个,局部峰 7 个——满眼毛刺。
同一组数,三种形状。这不是数据在变,是切法在变。
三、太少和太多分别损失什么
箱太少:把不同的群体并进一格。两个峰被并成一个大土包,看起来像「单峰」,结构全丢了。 箱太多:每格只有一两个人,柱子高低全靠偶然,噪声被当成结构。 经验上,箱数取「样本量的平方根」附近是个不错的起点。60 个样本,开方约 7.7,取 8 到 10 都合理。
这只是起点,不是规矩。真正该做的是换几个宽度看看,形状稳不稳。
四、为什么宽度是选择不是事实
数据的真实取值是离散的(每 0.1 一个台阶),箱宽是人为划的。 换个宽度,峰的位置会挪一点,峰的数量可能从 1 变 2 再变 3。 所以「这组数据是双峰的」这句话,严谨的写法是「在箱宽 2.5 时呈现双峰」。
任何直方图都附带一个没写出来的决定:箱宽。写报告时把它写出来,是最低要求。
五、两张图并排放
箱数 4 的版本存成一张 PNG,箱数 10 的版本存成另一张,字节数分别是 10639 和 11525。 并排放着看,谁都想不到是同一组数。 这不是图在骗人,是图只回答「按这个切法长什么样」。 要回答「数据本身长什么样」,得把几种切法都摆出来。
六、动手 Demo:区间宽度决定了你看见什么
同一组数,箱数分别取 4、6、10、20、30,比较「计零箱数」和「局部峰个数」。 两个指标都从计数里数出来,不靠眼睛。
BASE = [round(2.5 + 0.1 * i, 1) for i in range(40)]
NEGO = [round(8.0 + 0.25 * i, 1) for i in range(15)]
DATA = BASE + NEGO + [18.0, 20.0, 22.0, 24.0, 26.0]
LO, HI = 2.0, 27.0
def hist(vals, lo, hi, bins):
w = (hi - lo) / float(bins)
counts = [0] * bins
for v in vals:
if lo <= v < hi:
counts[min(int((v - lo) / w), bins - 1)] += 1
return counts
for b in (4, 6, 10, 20, 30):
c = hist(DATA, LO, HI, b)
nz = sum(1 for x in c if x == 0)
peaks = 0
for i in range(1, b - 1):
if c[i] > c[i - 1] and c[i] > c[i + 1]:
peaks += 1
print("箱数 %2d 每箱宽 %.2f 计零 %2d 局部峰 %d"
% (b, (HI - LO) / b, nz, peaks))
print()
print("样本量 %d,开方 %.1f——箱数取 8 到 10 附近是个常见起点"
% (len(DATA), len(DATA) ** 0.5))
实跑输出:
--------------------------------------------------------------------
区间宽度决定了你看见什么
--------------------------------------------------------------------
① 同一组数,换个箱数
--------------------------------------------------------------------
箱数 4(每箱宽 6.25 个百分点):计零 0 个,局部峰 0 个
箱数 6(每箱宽 4.17 个百分点):计零 0 个,局部峰 0 个
箱数 10(每箱宽 2.50 个百分点):计零 2 个,局部峰 2 个
箱数 20(每箱宽 1.25 个百分点):计零 7 个,局部峰 4 个
箱数 30(每箱宽 0.83 个百分点):计零 14 个,局部峰 7 个
② 箱太少看不出结构,箱太多看出噪声
--------------------------------------------------------------------
箱数 4:太粗,两个峰被并成一个大土包,看起来像「单峰」。
箱数 30:每个箱宽不到 1 个百分点,柱子高低全靠一两个人,看着全是毛刺。
箱数 10 左右,两个峰和中间缺口都能看见——这就是为什么很多默认设置用 10。
③ 为什么宽度是「选择」不是「事实」
--------------------------------------------------------------------
数据的真实取值是离散的(每 0.1 一个台阶),箱宽是画图人定的。
换个宽度,峰的位置会挪一点,峰的数量可能从 1 变 2 再变 3。
! 任何直方图都附带一个没写出来的决定:箱宽。看图的时候要问一句「换个宽度还成立吗」。
④ 书里那段 R 的活,这里怎么干
--------------------------------------------------------------------
书里教你装 R、把数据读进 R、用 hist() 画图。这台机器上没有 R,也没有 Excel。
这里的做法:用标准库分箱(上面那个 hist 函数十行),用 matplotlib 画图。
分箱的逻辑和 R 的 hist() 是一样的:定边界、数落点、画柱子。装个 R 只是省了手写这一步。
⑤ 一张图,两个宽度
--------------------------------------------------------------------
箱数 4 的版本存成 PNG:10639 字节。它看起来像单峰。
把两张图并排放,谁都想不到是同一组数。
「局部峰」的判定是「比左右两格都高」,这是最粗的一种数峰办法,够用但不完美。 注意计零箱数从 0 涨到 14:箱越细,空格越多。空格多不等于数据有结构,只等于切得细。
30 秒实验
- 把峰的定义改成「比左右两格平均高」,各个箱数下的峰数会怎么变?
- 样本量从 60 加到 600(把每条记录重复 10 次),同样箱数下形状会变吗?
- 想一想:为什么报告里写「双峰分布」时,应该同时交代箱宽?
常见误区
- 默认箱数就走:默认值只是别人的选择,不一定是这组数据的合适切法。
- 箱越细越「精确」:切太细看到的是噪声,不是结构。
- 只说形状不说箱宽:换个人换个宽度,结论可能就变了。
- 拿 R 的默认当标准:R 的 hist() 有自己的默认分箱规则,也只是规则之一。
自测三问(点开对答案)
1. 箱数太少和太多,分别损失什么? —— 太少会把不同群体并成一格,丢掉结构;太多会让每格只剩一两个人,把噪声当结构。 2. 「箱数是选择不是事实」这句话怎么理解? —— 数据的取值是给定的,切法是画图人定的。换切法形状就变,所以形状的结论要带上切法一起说。 3. 书里的 R 那一步,这里怎么等价复现? —— R 的 hist() 做的就是定边界、数落点、画柱子。用标准库分箱 + matplotlib 出图,结果一致。形状稳住了,还有一个问题:把数据拆成几组看,形状会不会完全不同?下一篇拆子集。
- ☐ (本篇完,下一篇 9.4 子集与谈判回报)