📈 第4章 数据图形化
📄 一句话概括

图形化的根本不在配色和排版,在比较成不成立。 比较要成立,得同时满足三条:同口径、同基线、同尺度。 截断坐标轴是这三条里最常被破的一条。 同一组数,基线从 0 换成 600,30 单的差别能画出 4 倍的观感。 一张图里能同时比较的量越多,它越有用——但前提是每个比较都还站得住。

📝 本章和前面几章的关系

前三篇一直在解决「装几个变量」的问题。这一篇反过来,讲「装进去之后怎么比才不算骗人」。 这一篇的三个 Demo 数都是现算的:柱子高度比、放大倍数,都能自己复现。

一、比较成立的三条硬条件

同口径:两个数的分母得一样。转化率的分母是访客还是浏览量,不能一半一半。 同基线:两根柱子的起点得一样。一根从 0 起、一根从 600 起,长度就没有可比性。 同尺度:两张图的纵轴范围得一样。一张到 700、一张到 650,曲线陡不陡全是缩放说了算。

条件 破了的典型做法 后果
同口径 一个用访客、一个用浏览量算转化率 差距可能翻倍
同基线 柱子从 600 起画 30 单看成 4 倍
同尺度 两张图纵轴范围不同 平地看成悬崖

这三条不是审美问题,是逻辑问题。破了任何一条,图上的比较就不成立,读图的人得到的结论一定错。

二、截断坐标轴:30 单画出 4 倍的观感

4 月订单 610,5 月 640。差 30 单,4.9%。 基线从 0 起:两根柱子高度比 1.049 比 1,读起来几乎一样高。 基线从 600 起:两根柱子高度比 4.0 比 1,读起来像是一下蹿上去了。

同一组数,同一套配色,观感放大了 3.8 倍。数据一个都没改,改的只是 y 轴的起点。 这不是说截断一定是错的。图 B 确实把 30 单的波动形态画清楚了——它只是不适合用来「比大小」。

三、什么时候截断是允许的

看趋势细节时可以截:比如 610 到 640 之间那点波动,从 0 起画就是一条平线,什么都看不出来。 比大小时不能截:人眼比的是柱子的物理长度,不是柱子上的数字。 规矩只有一条:截断就必须在轴上标出来,并且在标题或注释里说清。截了不说,就是骗人。

折线图还有个更隐蔽的版本:纵轴不从 0 起,是折线图的常态。 所以看折线图的第一件事,是看左下角那个数是多少。

四、一次只比一对,是最安全的比较

两条柱子比长度,是精度最高的比较方式。三条以上就开始费劲,超过七八条基本靠扫。 所以图不是装得越多越好。「能同时比较的量越多越有用」这句话有个前提:每一对比较都还看得清。 装不下的时候有两个办法:一是分面,把大类拆成几张小图;二是换图形,用位置代替长度、角度。

五、能同时比较的量越多,这张图越有用

回头看前一篇的气泡图:一张图上能比三件事——城市之间的转化率高低、客单价高低、订单量大小。 再往上一层是分面:四张小图并排,读者能在「格与格之间」和「格内」同时比较, 这是二维纸面上信息量最大的排法。 但每加一个变量,就多一个被误读的机会。加到第四个、第五个,就得问自己:读者真的会看吗?

图的容量由读者的注意力决定,不由画图软件的功能决定。

六、动手 Demo:正确比较:别让坐标轴骗人

同一组数(610 和 640)画两张图,只改 y 轴的起点,量一量柱子高度的比值。 比值不是肉眼估的,是按「柱高占整个轴的比例」算出来的。

import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

def save(name, ylim, color, title):
    fig, ax = plt.subplots(figsize=(5.0, 3.2), dpi=110)
    ax.bar([0, 1], [610, 640], width=0.5, color=color)
    ax.set_ylim(*ylim)
    ax.set_xticks([0, 1])
    ax.set_xticklabels(["Apr", "May"], fontsize=8)
    ax.set_title(title, fontsize=9)
    fig.tight_layout()
    fig.savefig(name)
    plt.close(fig)
    print("%s 存成 %d 字节" % (name, os.path.getsize(name)))

save("axis0.png", (0, 700), "#7f9fd0", "axis from 0")
save("axis600.png", (600, 650), "#d08f7f", "axis from 600")

def ratio(lo, hi, ymin, ymax):
    return ((hi - ymin) / float(ymax - ymin)) / ((lo - ymin) / float(ymax - ymin))

r0 = ratio(610, 640, 0, 700)
r6 = ratio(610, 640, 600, 650)
print()
print("基线 0   柱高比 %.3f 比 1" % r0)
print("基线 600 柱高比 %.1f 比 1" % r6)
print("真实差别 %.1f%%,视觉放大 %.1f 倍" % ((640 / 610.0 - 1) * 100, r6 / r0))

实跑输出:

--------------------------------------------------------------------
正确比较:别让坐标轴骗人
--------------------------------------------------------------------

  ① 两个只差 5% 的数
  --------------------------------------------------------------------
  新军队的 4 月订单 610,5 月 640。差 30 单,4.9%。
  同一组数,画两张图,看起来能差出好几倍。

  ② 图 A:基线从 0 起
  --------------------------------------------------------------------
  两根柱子的高度比:1.049 比 1
  读起来:几乎一样高。存成 PNG 9082 字节

  ③ 图 B:基线从 600 起
  --------------------------------------------------------------------
  两根柱子的高度比:4.0 比 1
  读起来:5 月像是一下蹿上去了。存成 PNG 9476 字节

  ④ 同一组数,视觉差多少
  --------------------------------------------------------------------

| 画法 | 柱子高度比 | 读者感受 |
|---|---|---|
| 基线 0 | **1.05 比 1** | 几乎没变 |
| 基线 600 | **4.0 比 1** | 像蹿了一大截 |

  真实差别是 4.9%,截断坐标轴把视觉差放大了 3.8 倍。
  ! 截断坐标轴本身不是错,错的是不告诉读者你截了。

  ⑤ 什么时候可以截断
  --------------------------------------------------------------------
  看趋势的细节时,可以截——比如 610 到 640 这 30 单的波动形态。
  比大小时,不能截——因为人眼比的是柱子长度,不是数字。
  规矩很简单:截断必须在轴上标出来,并在标题里说清。

4.0 比 1 这个数不是估的:基线 600、轴顶 650,柱子高度分别是 40 和 10,正好 4 倍。 换根轴就能把 4.9% 变成 4 倍观感,这就是为什么「看图说话」必须先看轴。

30 秒实验

  1. 把基线改成 590、轴顶 660,柱高比会变成多少?用上面的 ratio 算一下。
  2. 如果两根柱子改成折线的两个点,截断带来的误导会更严重还是更轻?
  3. 想一想:折线图几乎从不从 0 起,为什么它反而没被当成骗人?

常见误区

  1. 截断不标注:轴上不写起点、标题不提,读者只能按比例去读,一定读错。
  2. 两张图纵轴不一样还并排放:这是最容易犯又最难自察的一种,因为每张图单独看都「没问题」。
  3. 比大小却用面积:气泡大小比的是面积,人眼估不准,换成柱长就清楚了。
  4. 只怪画图的人:读图的人也有责任——看柱子先看 y 轴起点,看折线先看左下角。
自测三问(点开对答案) 1. 比较成立的三条硬条件是什么? —— 同口径(分母一致)、同基线(起点一致)、同尺度(轴范围一致)。 2. 为什么截断坐标轴会把差别放大? —— 人眼读的是柱子的物理长度。起点抬高后,两根柱子都要剪掉同一截,剩下的比例被放大。 3. 折线图为什么几乎都不从 0 起? —— 因为它看的是变化形态,不是绝对量。但代价是:陡峭程度由轴的范围决定,不能直接用来比幅度。

比较的规矩讲完了。下一篇把变量加到三个以上,看多元图形怎么装、代价是什么。

相关笔记