书里四行 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 秒实验
- 如果数据里只有 60 到 90 分的人,把线延伸到 40 分会更危险还是更安全?为什么?
- 把 polyfit 的第三个参数改成 2(二次拟合),r² 会升还是会降?升了就该用二次模型吗?
- 想一想:为什么「外插危险」这句话,对时间序列数据的杀伤力比对横截面数据更大?
常见误区
- 把工具当知识:会用 lm() 不等于知道它在算最小二乘,换个软件就抓瞎。
- 外插不给标记:把范围外的预测和范围内的预测混在一张表里交出去。
- 拿 summary() 的星号当结论:显著性说的是「这个斜率不太可能是 0」,不是「这个模型好用」。
- 看到 polyfit 和手算不完全相等就以为算错了:浮点误差是常态,量级在 1e-16 就该放过。
自测三问(点开对答案)
1. lm() 和 polyfit 算的是同一个东西吗? —— 是。都是最小二乘拟合,给出的斜率和截距在浮点精度内一致。差别只在接口和附带输出。 2. 什么叫外插?为什么它危险? —— 用数据范围外的 x 去问模型。模型只在那段范围内被数据约束过,范围外只是把线延长,没有依据。 3. 为什么 20 分给出的 2.50% 比 150 分给出的 14.17% 更危险? —— 因为 2.50% 看起来完全合理,不会引起怀疑;14.17% 一眼就荒唐,反而会被拦下来。线有了、工具也有了。最后一篇把方程翻译成人话,看它能拿来说什么、不能拿来说什么。
- ☐ (本篇完,下一篇 10.5 算法就是那个方程)