一条直线只要两个数:斜率 m 和截距 b。挑哪一条,取决于「最贴合」怎么定义。 最小二乘的定义是:让所有点的竖直距离平方和最小。算出来 m = 0.0898、b = 0.700。 这条线的 r² 是 0.703,RMSE 是 0.94——比按档取平均的 1.00 又小了一点。
上一篇画了散点和折线。这一篇把折线换成一条直线,并且把「怎么算」写清楚。 公式只有两个,四行代码就能算出来。这里不用现成的拟合函数,先手算一遍。
一、一条直线只要两个数
写成 y = b + m·x。b 是截距,m 是斜率。两个数定一条线,这是初中的事。 平面上能穿过这团点的直线有无穷多条。问题从来不是「找一条」,是「挑哪一条」。 随手画一条也能穿过点云,但它不可复现:换个人画就是另一条。
要让「挑」这件事有答案,必须先把「好」定义成一个数。这就是最小二乘干的活。
二、怎么算「最贴合」
每个点到直线都有一段竖直距离,叫残差:实际值减去预测值。 点在线上方,残差为正;在线下方,残差为负。 直接把残差加起来是错的——正负会互相抵消,一条穿过点云中心的烂线也能加出 0。
所以先平方再加:符号问题解决了,而且离得越远的点罚得越重。 让这个平方和最小的那条线,就是最小二乘回归线。「最小二乘」这个名字就是「最小平方和」。 注意是竖直距离,不是点到直线的垂直距离。竖直距离量的是「预测错了多少」,这才是我们要的。
三、手算斜率和截距
斜率 m = 每个点 (x−x̄)(y−ȳ) 加起来,除以每个点 (x−x̄)² 加起来。 截距 b = ȳ − m·x̄。先算斜率,再把平均点代回去,线一定穿过 (x̄, ȳ)。 这 60 条数据算出来:m = 0.0898,b = 0.700。
方程:加薪幅度 = 0.700 + 0.0898 × 绩效点数。 代进去验一下:60 分预测 6.09%,90 分预测 8.78%。中间 30 分,涨 2.69 个点。
四、这条线有多准
残差平方和 52.93,总平方和 178.30。两个数一比,就知道线吃掉了多少变异。 决定系数 r² = 1 − 52.93/178.30 = 0.703。 读法:加薪幅度的差异里,有 70% 能被绩效点数解释。剩下 30% 不在分数里。
RMSE = 0.94 个百分点。上一版分档是 1.00,回归线又小了一点。 为什么只小一点?因为回归线比六档折线多用的信息就那么一点点——它只是把台阶抹平了。
五、这条线的三个限制
第一,它只对 40 到 95 分这段负责。范围外的事,它不给依据。 第二,它只对「大致是直线」的关系负责。点云要是弯的,直线两头都错。 第三,它只对「和这 60 个人像的人」负责。换个岗位序列,m 和 b 都得重算。
这三条不是免责声明,是使用说明。不写清楚适用范围,r² 再大也不能用。
六、动手 Demo:回归线是让竖直距离平方和最小的那条
不调现成的拟合函数,用两个公式手算斜率和截距。 再算残差平方和、总平方和、r²、RMSE,最后把线画到散点上。
import os
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
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 = [d[0] for d in DATA]
YS = [d[1] for d in DATA]
n = len(XS)
mx = sum(XS) / float(n)
my = sum(YS) / float(n)
sxy = sum((x - mx) * (y - my) for x, y in zip(XS, YS))
sxx = sum((x - mx) * (x - mx) for x in XS)
m = sxy / sxx
b = my - m * mx
print("斜率 m = %.4f 截距 b = %.3f" % (m, b))
print("方程:加薪 = %.3f + %.4f * 分数" % (b, m))
print("60 分预测 %.2f%% 90 分预测 %.2f%%" % (b + m * 60, b + m * 90))
pred = [b + m * x for x in XS]
ss_res = sum((y - p) * (y - p) for y, p in zip(YS, pred))
ss_tot = sum((y - my) * (y - my) for y in YS)
print("残差平方和 %.2f 总平方和 %.2f" % (ss_res, ss_tot))
print("r2 = %.3f" % (1 - ss_res / ss_tot))
print("RMSE = %.2f" % ((ss_res / n) ** 0.5))
fig, ax = plt.subplots(figsize=(5.0, 3.2), dpi=110)
ax.scatter(XS, YS, s=12, color="#c8d4e8")
ax.plot([38, 98], [b + m * 38, b + m * 98], color="#a5442f", linewidth=1.8)
ax.set_xlabel("performance points", fontsize=8)
ax.set_ylabel("raise percent", fontsize=8)
ax.set_title("least squares line", fontsize=9)
fig.tight_layout()
fig.savefig("fit.png")
plt.close(fig)
print("回归图 %d 字节" % os.path.getsize("fit.png"))
实跑输出:
--------------------------------------------------------------------
回归线是让竖直距离平方和最小的那条
--------------------------------------------------------------------
① 一条直线只要两个数
--------------------------------------------------------------------
y = b + m·x。b 是截距(x 为 0 时 y 落在哪),m 是斜率(x 每多 1,y 多多少)。
平面上有无穷多条直线能穿过这团点。问题不是「找一条」,是「挑哪一条」。
② 挑的标准:竖直距离的平方和最小
--------------------------------------------------------------------
每个点都有一条到直线的竖直距离,叫残差。有的在上,有的在下,符号相反直接相加会互相抵消。
所以先平方再加:正的负的都变成正的,而且离得越远的点罚得越重。
让这个平方和最小的那条线,就是最小二乘回归线。
! 注意是竖直距离,不是垂直距离。竖直距离量的是「预测错了多少」,这才是我们要的。
③ 手算斜率和截距
--------------------------------------------------------------------
斜率 m = 协方差 / x 的方差。截距 b = ȳ - m·x̄。两个式子,四行代码。
算出来 m = 0.0898,b = 0.700。
方程:加薪幅度 = 0.700 + 0.0898 × 绩效点数
代进去验一下:60 分的人预测 6.09%,90 分的人预测 8.78%。
④ 这条线有多准
--------------------------------------------------------------------
残差平方和 52.93,总平方和 178.30。
决定系数 r² = 1 - 残差/总 = 0.703。
它的意思:加薪幅度的差异里,有 70% 能被绩效点数解释。
剩下的 30% 是分数解释不了的——部门、岗位、谈判、运气都在里面。
RMSE = 0.94 个百分点,比第二版的 1.00 又小了一点。
⑤ 画上去看
--------------------------------------------------------------------
回归线叠在散点上,存成 PNG:19875 字节。
线是从点云中间穿过去的,上下两侧的点大致一样多——这是最小二乘的一个必然结果。
线一定穿过 (x̄, ȳ):平均分数 67.5 分对应平均加薪 6.76%。这是最小二乘的必然结果,不是巧合。 r² 是 0.703,不是 0.9。说明还有三成的差异在分数之外,这一点后面会一直用到。
30 秒实验
- 把 y 轴换成「加薪的绝对金额」而不是百分比,斜率会怎么变?r² 会变吗?
- 如果把这 60 个点全部乘以 2,斜率和截距各变成多少?r² 呢?
- 想一想:为什么 r² 高不代表模型对?举一个 r² 很高但没用的例子。
常见误区
- 用垂直距离算残差:那是最短距离,不是预测误差。要量「错多少」,只能用竖直距离。
- 把 r² 当准确率:r² 说的是「解释了多少差异」,不是「多少预测是对的」。
- 残差直接相加判断好坏:正负抵消,任何穿过中心的线都能加到 0。
- 算完线就宣布能用:不写适用范围、不报误差,这条线只是一道算术题。
自测三问(点开对答案)
1. 最小二乘里的「二乘」指什么? —— 残差先平方再求和。平方是为了消掉正负号,同时让偏离更远的点受更重的惩罚。 2. 斜率 0.0898 是什么意思? —— 绩效点数每多 1 点,加薪幅度平均多 0.0898 个百分点;多 10 点就是 0.90 个百分点。 3. r² = 0.703 该怎么读? —— 加薪幅度的差异里约 70% 能被分数解释,剩下 30% 在分数之外。它不是说「70% 的预测是对的」。线算出来了。书里这一步是交给 R 的 lm(),下一篇看它到底做了什么,以及它的边界在哪。
- ☐ (本篇完,下一篇 10.4 让 R 创建一个回归对象)