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

书里四行 R:读数据、lm() 拟合、summary() 看摘要、abline() 画线。这台机器上没有 R,跑不了。 同一件事用 numpy 的 polyfit 干,m = 0.0898、b = 0.700,和手算差 6.8e-16——只是浮点误差。 回归线只对数据覆盖的那段负责。150 分也能算出一个数,那个数没有任何依据。

📝 本章和前面几章的关系

上一篇把公式手算了一遍。这一篇看工具:书用 R,这里用 numpy,算的是同一条线。 书里这段还有 Excel 的「添加趋势线」,同一个算法的另一个入口,这里一并对照。

一、书里的四行 R

读数据 read.csv(),拟合 lm(raise ~ points),看摘要 summary(),画线 abline()。 四行里真正干活的只有一行:lm()。其余三行是输入、查看、展示。 这台机器上没有 R:Rscript 不存在,装上也不是这台机器的事。所以那四行只能原样贴出来。

平台原样,不等于知识点缺席。lm() 内部做的就是上一篇那两个公式。

二、lm() 到底做了什么

你写的 它做的 这里怎么等价
read.csv() 把表格读进内存 脚本里直接生成列表
lm(y ~ x) 算斜率和截距 两个公式手算,或 polyfit
summary() 给系数、标准误、r² 自己算 r² 和 RMSE
abline() 把线画到现有图上 ax.plot 画一段直线

第二行是核心,其余三行都是围着它转的。 summary() 里那些标准误、p 值,是给「这条线可信不可信」用的,属于第 11 章的范围。 这里先把系数和 r² 对上,够用了。

三、同一件事在这里怎么干

numpy 的 polyfit(x, y, 1) 一次算完,返回 [斜率, 截距]。 结果 m = 0.0898、b = 0.700,和手算差 6.8e-16。这个差是浮点数存不下无限小数造成的。 三套工具,一条线:R 的 lm()、numpy 的 polyfit、手写的四行,数学上是同一个东西。

所以「会不会用某个软件」不是关键,「知不知道它在算什么」才是。

四、回归方程和散点图是一回事

斜率 0.0898 是正的,对应散点图里点云往右上走。斜率为负,点云就往右下走。 残差平方和越小,点贴得越紧,r² 就越大,图上看起来越像一条线。 所以看散点图时心里该有两个判断:斜不斜、散不散。它们直接对应 m 和 r²。

图和方程不是两种东西,是同一件事的两种写法。

五、范围外的 x:外插

数据里分数最低 40、最高 95。拿 120 去问,方程照样吐数——11.47%。 拿 150 去问,给 14.17%。拿 20 去问,给 2.50%。 这三个数都是「把直线画长一点」算出来的,不是预测。这种用法叫外插。

最危险的不是 150 那种明显荒唐的,是 20 那种——2.50% 看着完全合理,所以没人会怀疑它。 判断标准很简单:x 落在数据范围内,叫内插;落在外面,叫外插。外插的结论必须标出来。

六、动手 Demo:让 R 创建回归对象,以及它的边界

先原样贴出书里的四行 R(本沙箱没有 R,不执行)。 再用 numpy 的 polyfit 算同一个结果,和手算的比一比。 最后把几个范围外的 x 丢进去,看方程怎么答。

import numpy as np

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)))

XS = np.array([d[0] for d in DATA], dtype=float)
YS = np.array([d[1] for d in DATA], dtype=float)

# 手算
mx = sum(XS) / len(XS)
my = sum(YS) / len(YS)
m1 = ((XS - mx) * (YS - my)).sum() / ((XS - mx) * (XS - mx)).sum()
b1 = my - m1 * mx

# 工具
m2, b2 = np.polyfit(XS, YS, 1)
print("手算 m=%.4f b=%.3f" % (m1, b1))
print("polyfit m=%.4f b=%.3f" % (m2, b2))
print("两者之差 %.1e" % (abs(m1 - m2) + abs(b1 - b2)))

print()
for q in (20, 40, 95, 120, 150):
    tag = "内插" if 40 <= q <= 95 else "外插"
    print("%-4d 分 -> %5.2f%%   %s" % (q, b1 + m1 * q, tag))

实跑输出:

--------------------------------------------------------------------
让 R 创建回归对象,以及它的边界
--------------------------------------------------------------------

  ① 书里怎么让 R 干这件事
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | salary <- read.csv("salary.csv")
  | m <- lm(raise ~ points, data = salary)
  | summary(m)
  | abline(m, col = "red")
  lm() 干的就是上面那两步:拟合系数、给出摘要。这台机器上没有 R,跑不了。


  ② 同一件事在这里怎么干
  --------------------------------------------------------------------
  numpy 的 polyfit 一次算完:m = 0.0898,b = 0.700。
  和手算的 m = 0.0898、b = 0.700 对得上——差 6.8e-16,是浮点误差。
  ! 工具换了,公式没换。R 的 lm()、numpy 的 polyfit、手写四行,算的是同一条线。

  ③ 回归方程和散点图是一回事
  --------------------------------------------------------------------
  斜率 m = 0.0898 的正负,就是散点图里点云的倾斜方向。
  残差平方和越小,说明点贴得越紧,r² 就越大。
  所以看散点图时心里该有一条线:它斜不斜、点散不散。这两个判断直接对应 m 和 r²。

  ④ 拿范围外的 x 去问
  --------------------------------------------------------------------
  数据里分数最低 40、最高 95。拿 120 去问,方程照样会吐一个数。

| 输入的点数 | 方程给的值 | 数据覆盖过吗 |
|:---:|:---:|---|
| 20 | 2.50% | 不是 |
| 40 | 4.29% | 是 |
| 95 | 9.23% | 是 |
| 120 | 11.47% | 不是 |
| 150 | 14.17% | 不是 |

  120 分给 11.47%,150 分给 14.17%——两个数都是直线延长出来的,没有任何依据。
  20 分给 2.50%,看着还挺像回事。这正是外插最危险的地方:错得很像对的。
  ! 数据没覆盖的地方,回归线只是「把直线画长一点」,不是预测。这叫外插。

  ⑤ 回归的适用范围
  --------------------------------------------------------------------
  它只对这段数据覆盖的范围负责:40 到 95 分。
  它只对「大致是直线」的关系负责。点云要是弯的,直线会两头都错。
  它只对「和这批人像的人」负责。换个行业、换个岗位序列,斜率和截距都得重算。

polyfit 和手算的差是 6.8e-16,量级在浮点精度上。这不是「大致一样」,是「同一条线」。 外插那三行里,20 分给的 2.50% 最像真的——恰恰是它最该被标出来。

30 秒实验

  1. 如果数据里只有 60 到 90 分的人,把线延伸到 40 分会更危险还是更安全?为什么?
  2. 把 polyfit 的第三个参数改成 2(二次拟合),r² 会升还是会降?升了就该用二次模型吗?
  3. 想一想:为什么「外插危险」这句话,对时间序列数据的杀伤力比对横截面数据更大?

常见误区

  1. 把工具当知识:会用 lm() 不等于知道它在算最小二乘,换个软件就抓瞎。
  2. 外插不给标记:把范围外的预测和范围内的预测混在一张表里交出去。
  3. 拿 summary() 的星号当结论:显著性说的是「这个斜率不太可能是 0」,不是「这个模型好用」。
  4. 看到 polyfit 和手算不完全相等就以为算错了:浮点误差是常态,量级在 1e-16 就该放过。
自测三问(点开对答案) 1. lm() 和 polyfit 算的是同一个东西吗? —— 是。都是最小二乘拟合,给出的斜率和截距在浮点精度内一致。差别只在接口和附带输出。 2. 什么叫外插?为什么它危险? —— 用数据范围外的 x 去问模型。模型只在那段范围内被数据约束过,范围外只是把线延长,没有依据。 3. 为什么 20 分给出的 2.50% 比 150 分给出的 14.17% 更危险? —— 因为 2.50% 看起来完全合理,不会引起怀疑;14.17% 一眼就荒唐,反而会被拦下来。

线有了、工具也有了。最后一篇把方程翻译成人话,看它能拿来说什么、不能拿来说什么。

相关笔记