📈 第10章 回归
📄 一句话概括

「我想多拿钱」不是一句能回答的话,换成「绩效点数和加薪幅度是什么关系」才是。 预测的本质就是找一个能把 x 变成 y 的式子。最笨的式子是常数,误差 1.72 个百分点。 按分数分六档取平均,误差降到 1.00,降了 41.9%——分数确实带着信息,但分档有台阶。

📝 本章和前面几章的关系

第 9 章处理「一堆数怎么先看形状」。第 10 章换一个目标:从一个量预测另一个量。 这一章的 60 条记录是假数据,用自带的线性同余发生器造的。 不调 random 模块,同一份代码跑一百遍,数一模一样——文里每个数都能自己复算。

一、把「我要加薪」变成一个能回答的问题

第 1 章那个动作,这里还要再做一遍:把手上的抱怨翻成一个能对着数据回答的问句。 「我想多拿钱」没法回答,因为它不含任何可以比较的东西。 换成这一句就好办:过去三年,绩效点数和加薪幅度之间是什么关系?

这个问法有两个好处:两个量都能拿到数,而且它们之间的关系可以算。 有了这层关系,就能反推一个式子出来:扔进一个点数,吐出加薪幅度。这就是加薪计算器。 预测的本质就是找一个能把 x 变成 y 的式子。难点不在写式子,在挑哪个式子。

二、第一版算法:一个常数

最笨的算法是:不看任何信息,所有人一律按平均加薪幅度加。 这 60 个人的平均是 6.76%。于是计算器对谁都吐 6.76%。 它当然错,但错得有一个可量的数:均方根误差 1.72 个百分点。

1.72 这个数不是随便来的——它就是加薪幅度自己的标准偏差(1.74,除以 n−1 的版本)的近似。 意思很直白:什么信息都不用,误差就等于这堆数本身的散布。 所以它是个基准线。后面任何一个算法,误差不比 1.72 小,就等于白算。

三、第二版:按分数分档

加一条信息:这个人的绩效点数落在哪一档。 把 40 到 96 切成六段,每段算一个平均加薪幅度,同段的人一律按该段平均算。 六段的平均分别是 4.58%、5.78%、6.77%、7.20%、8.04%、9.22%——一路往上,没有回头。

误差从 1.72 降到 1.00,降了 41.9%。这就是「分数有用」的量化版本。 注意这一步只加了一条规则、一个分档表,没有引入任何新数据。信息用对了,误差自己就下来了。

四、误差是怎么量出来的

每个点都有一个「实际值」和一个「预测值」,两者相减就是这次预测错了多少。 错有正有负,直接加起来会互相抵掉,看起来像没误差。所以先平方,把符号抹掉。 平方还有一个副作用:错得越多罚得越重。差 2 个点的处罚是差 1 个点的四倍。

全部平方加起来,除以个数,再开方——把量纲还原回「百分点」,这就是均方根误差。 它的读法是:平均而言,预测偏了这么多。不是「最多偏这么多」,也不是「保证不偏更多」。

五、分档的毛病:台阶

第二版有个躲不掉的毛病:档与档之间是台阶。 59 分和 60 分被分到两档,预测值从 4.58% 跳到 5.78%,一下子跳了 1.2 个百分点。 可现实里,59 分和 60 分差一分,加薪不可能差 1.2 个点。

原因是:分档把连续的分数切成了几块,块内的差别被抹平,块间的差别被放大。 要消掉这个台阶,就得让预测值随着分数连续地变。下一节先画图,看这条连续的线长什么样。

六、动手 Demo:加薪计算器的第一版和第二版

60 条记录:绩效点数 40 到 95,去年加薪幅度 2.5% 到 10.6%。 先算「一律取平均」的误差,再算「按分数分六档」的误差,两个数直接比。

S = [20261102]

def rnd():
    S[0] = (S[0] * 1103515245 + 12345) % 2147483648
    return S[0] / 2147483648.0

DATA = []
for i in range(60):
    x = 40 + int(round(i * 55 / 59.0))
    y = 1.2 + 0.085 * x + (rnd() - 0.5) * 3.2
    DATA.append((x, round(y, 1)))

YS = [d[1] for d in DATA]

def rmse(a, p):
    return (sum((u - v) ** 2 for u, v in zip(a, p)) / float(len(a))) ** 0.5

my = sum(YS) / len(YS)
print("平均加薪 %.2f%%" % my)
print("第一版 RMSE %.2f" % rmse(YS, [my] * len(YS)))

BINS = [(40, 50), (50, 60), (60, 70), (70, 80), (80, 90), (90, 96)]
pred = []
for x, y in DATA:
    for lo, hi in BINS:
        if lo <= x < hi:
            seg = [b for a, b in DATA if lo <= a < hi]
            pred.append(sum(seg) / len(seg))
            break

r1 = rmse(YS, [my] * len(YS))
r2 = rmse(YS, pred)
print("第二版 RMSE %.2f" % r2)
print("降幅 %.1f%%" % ((r1 - r2) / r1 * 100))

实跑输出:

--------------------------------------------------------------------
加薪计算器的第一版和第二版
--------------------------------------------------------------------

  ① 把「我要加薪」变成一个能分析的题
  --------------------------------------------------------------------
  「我想多拿钱」不是一个能回答的问题。第 1 章那个动作,这里还要再做一遍。
  换成一个能回答的问法:过去三年,绩效点数和加薪幅度之间是什么关系?
  有了这层关系,就能反推出一个式子:给一个点数,吐出加薪幅度。这就是加薪计算器。
  ! 预测的本质就是找一个能把 x 变成 y 的式子。找法不止一种,好坏可以量出来。

  ② 第一版算法:所有人都一样
  --------------------------------------------------------------------
  最笨的算法:不看任何信息,所有人一律按平均 6.76% 加。
  它的误差(均方根):1.72 个百分点。
  y 自己的标准偏差是 1.74——两个数几乎一样,因为这一版什么信息都没用上,就是拿中心顶上去。
  ! 任何新算法的 RMSE 都必须比 1.72 小,否则它连「取平均」都不如。

  ③ 第二版:按分数分档,档内取平均
  --------------------------------------------------------------------

| 分数段 | 人数 | 档内平均 | 最低 | 最高 |
|:---:|---:|---:|---:|---:|
| 40 ~ 50 | 11 | 4.58% | 3.4% | 6.7% |
| 50 ~ 60 | 10 | 5.78% | 4.6% | 7.1% |
| 60 ~ 70 | 11 | 6.77% | 5.1% | 8.3% |
| 70 ~ 80 | 11 | 7.20% | 6.1% | 9.4% |
| 80 ~ 90 | 11 | 8.04% | 6.7% | 9.3% |
| 90 ~ 96 | 6 | 9.22% | 7.7% | 10.6% |

  这一版只用了一条规则:同一个分数段的人,加薪幅度按该段的平均算。
  它的误差(均方根):1.00 个百分点。

  ④ 两版差多少
  --------------------------------------------------------------------
  第一版 1.72 个百分点,第二版 1.00 个百分点,降了 41.9%。
  说明分数确实带着信息——知道一个人在哪个档,比什么都不知道强。
  但第二版有个毛病:档与档之间是台阶。60 分的人和 59 分的人被算成同一档,69 分的又跳一档。

  ⑤ 还缺什么
  --------------------------------------------------------------------
  缺一条连续的线:分数每多 1 点,加薪该多多少,能直接读出来的那种。
  这条线就是回归线。下一节先把它画在图上看一眼。

那个 rmse 函数是整个模型评价的核心:先平方、再平均、最后开方,三步一步都不能省。 1.72 到 1.00 这一步,全靠「分数」这一列信息。没有任何新数据进来,只是把已有的数用上了。

30 秒实验

  1. 把分档从六档改成三档(40~60、60~80、80~96),误差会变成多少?
  2. 如果把分档改成「一人一档」(60 档),误差会不会降到 0?为什么那样做没有意义?
  3. 想一想:为什么 RMSE 要先平方再开方,直接用「绝对值的平均」行不行?

常见误区

  1. 拿一个误差数就当模型好坏:RMSE 只说明平均偏多少,不说明偏在哪些人身上。
  2. 不看基准线就宣布模型有效:不谈「取平均」的 1.72,就没法说 1.00 是不是真的好。
  3. 分档越多越精确:档多了每档样本变少,档内平均就变成了噪声。
  4. 把预测值当成承诺:计算器吐的是「平均而言」,不是「你一定拿这么多」。
自测三问(点开对答案) 1. 为什么第一版的误差恰好等于 y 的标准偏差? —— 因为它对所有人给同一个数,等于没有用到 x 的任何信息。预测值的偏差就退化成 y 本身的散布。 2. 第二版只加了一条规则,误差为什么能降四成? —— 因为绩效点数确实和加薪幅度有关。把这条关系用上一部分,能解释的变异就少了一块。 3. 分档的台阶问题怎么消掉? —— 让预测值随分数连续变化,而不是分段跳。这就是下一节的回归线。

台阶要消掉,得先看清楚这团点长什么样。下一篇画散点图,再在它上面画一条连续的线。

相关笔记