在一堆散点数据面前,我经常被问到一个很朴素的问题:这些点大概沿着一条直线走,能不能帮我把这条直线画出来?不管是分析投放数据、校准仪器,还是想根据历史记录估计下一个结果,直线拟合往往是大家最早遇到的数学工具。最小二乘法(直线拟合)就是这条直线背后的标准答案:它在所有候选直线里,找一条整体偏离所有点最小的线,并且给出了可以直接套用的斜率公式和截距公式。
这篇笔记适合三类人:正在学统计但被公式绕晕的学生,工作中拿到一组数据想做趋势判断的分析员,以及写代码想快速实现拟合但不知道原理的开发者。我不打算只堆公式,而是把“为什么用平方”“为什么是这个公式”“手算一遍会发生什么”都讲透。读完之后,你能自己手动算出一条拟合线,也能在软件里验证结果,还知道哪些坑必须避开。
1. 在动手之前,先把直线拟合的场景想清楚
1.1 我经常碰到的“画一条线”问题
以某团队的产品迭代为例,他们把版本迭代次数记为 x,把上线后某一关键指标记作 y。连续几个版本下来,得到一组坐标点:版本越推越勤,指标大体在涨,但中间有波动。领导者问:下一个版本大概能涨到多少?这时候如果只用最后一个点去推,会被偶然波动骗到;如果随便连两个点,选不同的点结果就完全不一样。最小二乘法给了一条大家都能接受的规则:在所有直线里,选择一条与全部数据点整体偏离最小的线,作为对这个趋势的概括。
类似的场景还有很多。传感器校准需要建立原始读数与实际物理量之间的换算关系;库存管理想根据月份估算销量;医疗监测需要从连续记录里看指标是在上升还是下降。这些任务本质上都是同一个问题:我有一组 (x, y) 数据,想用一条直线 y = kx + b 去描述它们的关系。这里 k 是斜率,代表 x 每增加一个单位,y 平均变化多少;b 是截距,代表 x 为 0 时这条线的位置。
所以才说直线拟合是最基础的建模方式。它结构最简单,参数只有两个,计算有现成公式,结果也好解释。把它理解透之后,再去学多项式拟合、多元线性回归,思路都是一脉相承的。
1.2 最小二乘法解决的核心问题
最小二乘法的核心思路,是给“偏离程度”定一个度量方式。对于任意一条候选直线 y = kx + b,每个数据点都会在竖直方向产生一个偏差,也就是真实 y 值与这条线计算出来的值之差。这个差值在数学上叫残差,公式写出来就是 e_i = y_i - (kx_i + b)。
把所有点的残差都计算出来以后,自然希望它们越小越好。问题是,有些点在线上方,残差为正;有些点在线下方,残差为负。如果不平方直接把所有残差加起来,正负会互相抵消,可能出现一条明显很偏的线,算出来的总偏差却是 0。这显然不合理。最小二乘法选择把所有残差先平方再加总,得到一个总损失:损失越小,说明这条直线整体越贴近数据点。于是问题就变成了:找一对参数 (k, b),让总损失最小。
为什么强调“竖直方向”?因为在这个标准模型里,我们默认 x 是已知的、可控的变量,y 是带噪声的结果。比如实验时你设定浓度是 1、2、3、4、5,测定结果是 2.3、3.1、5.2,等等,那么误差主要发生在测量结果 y 上。这时关心的是“给一个 x,我预测 y 到哪个值最合理”,所以比较的是竖直距离。如果换一个场景,x 和 y 都可能出错,或者你根本分不清谁是自变量谁是因变量,那就不能简单用最小二乘法,而要考虑总最小二乘或其他方法。这个区别在后面排查问题时会再次出现。
2. 数学原理:残差、平方和与两个核心公式
2.1 损失函数:为什么要用平方而不是绝对值
把每个点的残差平方再求和,就得到损失函数:
S(k, b) = Σ(y_i - kx_i - b)²
这里的平方有两个重要作用。第一,它正负残差不会抵消,每个点都会真正参与贡献。第二,平方会让偏离较大的点对结果产生更大的影响。举个例子:一个点偏离 1 个单位,平方贡献是 1;一个点偏离 2 个单位,平方贡献是 4。所以说最小二乘法本质上对大的误差更敏感,这也是后面讨论离群点时必须注意的地方。
有人会问,为什么不用绝对值再求和?这样也在正负差异感。绝对值和在实际中确实存在,被称为最小绝对偏差法,它没那么容易被极端值带跑。但最小绝对偏差的损失函数在 0 点不可导,没有一个像最小二乘法这样简单的封闭解,计算上远不如二次函数方便。对入门和绝大多数工程问题来说,平方损失是实用性和可解释性的最佳折中。
你还可以把 S 看成是参数 k 和 b 的一张二维曲面。这个曲面是一个开口向上的二次抛物面,所以它只有一个最低点,不会像复杂模型那样陷入多个局部最优。这就是为什么最小二乘法总能稳定地算出唯一结果。
2.2 斜率公式和截距公式的推导过程
要找 S 的最小值,数学上的标准做法是对两个参数分别求偏导数,然后令偏导数等于 0。这就像在山谷里找最低点:沿着 k 方向的坡度为零,同时沿着 b 方向的坡度也为零,你就停在了谷底。
先对 b 求偏导:
∂S/∂b = -2Σ(y_i - kx_i - b) = 0
整理后得到:
Σy_i - kΣx_i - nb = 0
所以 b = ȳ - kx̄,也就是拟合直线一定会穿过数据点的平均值点 (x̄, ȳ)。这个结论很直观:一堆点的重心位置,理应落在最能代表它们的直线上。
再对 k 求偏导:
∂S/∂k = -2Σ x_i(y_i - kx_i - b) = 0
把前面得到的 b 代入这个式子,通过代数整理,可以得到:
k = Σ(x_i - x̄)(y_i - ȳ) / Σ(x_i - x̄)²
这个式子比原始形式更有直觉:分子衡量的是 x 和 y 是否同步变化,分母衡量的是 x 本身变化幅度。如果 x 和 y 一起增大,分子为正,斜率就是正的;如果 x 不变,分母为 0,根本没有斜率可言。
为了手工计算方便,还可以把公式改写成基于原始求和的等价形式:
k = (nΣx_i y_i - Σx_i Σy_i) / (nΣx_i² - (Σx_i)²)
这里 n 是数据点个数。两个公式算出来的结果完全一样,只是第二种不需要先求均值,适合一步步在纸上算。
2.3 两个公式的直觉和适用前提
公式讲完,有几个前提条件必须清楚。第一,分母不能是 0,也就是说 x 的取值要有变化。如果做了 5 次实验但 x 全部一样,那根本谈不上观察 x 对 y 的影响,拟合一条依赖 x 的直线没有意义。第二,这套推导默认误差独立且方差相对稳定,虽然在实际使用中并不是每次都严格满足,但我们至少要检查一下残差是否呈现明显的规律。
还有一点很容易被忽视:如果把 x 和 y 对调,再拟合一条直线,得到的并不是原来那条线的反函数。原因是两次最优化使用的距离方向不同。所以到底哪个变量当 x,哪个当 y,取决于问题的实际背景,而不是随意安排。否则你算出来的“关系”可能并不符合你真正想回答的问题。
3. 完整手算一遍:五组数据,一个可以照抄的实例
3.1 原始数据与表头设计
理论看再多,不如亲手算一次。我用一组简单的数据演示,数据本身就是我随便编的,但这个流程对任何真实数据都成立。假设有 5 个样本点:
| 序号 | x | y |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 3 |
| 3 | 3 | 5 |
| 4 | 4 | 4 |
| 5 | 5 | 6 |
从坐标上看,x 和 y 大体都随序号上升,但第 4 个点明显比第 3 个点低,所以数据并不完全在一条直线上。要让所有点都精确落在一条直线,在现实里几乎不可能,最小二乘法就是在这些矛盾中找一个最均衡的方案。
为了手算方便,我建议先列一张扩展表,把要用的 x²、xy、预测值、残差都放在同一张表里。这样不仅不会漏算,还能在算完后直接看到每个点的贡献。
3.2 六步手算流程
第一步,计算基本总量。n = 5,Σx = 1+2+3+4+5 = 15,Σy = 2+3+5+4+6 = 20,Σx² = 1+4+9+16+25 = 55,Σxy = 2+6+15+16+30 = 69。顺便可以算出均值:x̄ = 15 / 5 = 3,ȳ = 20 / 5 = 4。
第二步,套用总合公式求斜率。k = (nΣxy - ΣxΣy) / (nΣx² - (Σx)²),代入数值:k = (5 × 69 - 15 × 20) / (5 × 55 - 15²) = (345 - 300) / (275 - 225) = 45 / 50 = 0.9。
第三步,求截距。b = ȳ - kx̄ = 4 - 0.9 × 3 = 4 - 2.7 = 1.3。所以拟合直线是 y = 0.9x + 1.3。
第四步,把每个 x 代回去计算预测值。x = 1 时预测值是 0.9 × 1 + 1.3 = 2.2;x = 2 时是 3.1;x = 3 时是 4.0;x = 4 时是 4.9;x = 5 时是 5.8。这一步直接告诉你这条线长什么样。
第五步,计算每个点的残差和残差平方。残差 e = y - 预测值,六个关键数分别是 -0.2、-0.1、1.0、-0.9、0.2,对应的平方是 0.04、0.01、1.00、0.81、0.04。残差平方和 SS_res = 1.90。
第六步,计算总波动和拟合优度 R²。总平方和 SS_tot = Σ(y - ȳ)² = (2-4)² + (3-4)² + (5-4)² + (4-4)² + (6-4)² = 4+1+1+0+4 = 10。于是 R² = 1 - SS_res / SS_tot = 1 - 1.90 / 10 = 0.81。
3.3 残差怎么看,R²怎么解释
很多人算出 R² = 0.81 会说“模型解释了 81% 的变化”,这句话在统计上常用,但在实际报告里要格外小心。“解释”不是说指标真的由版本号决定,它只说明在这个样本范围内,x 的变异与 y 的变异有较强的同步关系。剩下 19% 的变化由其他因素、测量误差或随机波动造成。
再看残差列表:x = 3 时 y = 5,预测值只有 4,残差高达 1.0,单这一个点就贡献了残差平方和 1.90 的一大半。这说明第三个点比较特殊,如果它继续偏离,整个拟合线都会被它拉向自己。R² 虽然不错,但只看这个数还不够,必须结合残差一起判断。
我自己手算时有个习惯:计算完一定会画一张散点图和残差图。如果残差围绕 0 随机上下波动,说明直线拟合基本合理;如果残差随 x 增大而增大,或者呈现明显的 U 型,那就要考虑是不是非线性关系了。手算最后一步是检查而不是结束,明白这条线的适用范围,比得到一个漂亮的公式更重要。
4. 实际使用中的常见问题与排查技巧
4.1 离群点会拖着这条线走
有一个场景我见过太多次:一组数据本来趋势很清楚,但最右边混进一个极端值,拟合出的斜率瞬间被压平,或者被抬得非常高。为什么?因为平方误差对远处点的惩罚特别重,一个偏离很远的点,它的平方项会在总损失里占据很大比重,优化算法宁可牺牲其他点,也要尽量照顾这个离群点。
这就是最小二乘法的性格:对离群点敏感。处理时不能为了好看的数据就随便删点。比如在实验记录中,如果这个点是因为仪器故障、录入错误造成的,可以剔除并说明原因;如果它本身就是一个极端但真实的产品表现,那它很可能包含重要信息,反而值得单独研究。我会建议先画散点图,再拟合一次,然后把可疑点剔除后重新拟合,看结论是否变化。如果斜率变化很大,说明模型已经被这个点控制了,需要谨慎解释。
离群点还可能让 R² 虚高。一个离群点同时拉大了总平方和,也让回归线使劲靠近它,残差平方和并没有同比例变大,最后 R² 竟然不低,但中间区域拟合得并不好。所以判断拟合质量,别只看 R²,要配合残差图。
4.2 数据量级与数值稳定性问题
还有一个隐藏很深的坑:x 的数值如果非常大,直接套公式会产生数值问题。比如时间戳通常是十一位的数字,Σx 和 Σx² 会异常庞大,手算容易算错,用普通软件计算也有浮点精度损失的风险。这种情况不要怀疑最小二乘法的数学原理,要怀疑你的计算路径。
解决办法也很简单:先把 x 减去均值,也就是做中心化,用 x' = x - x̄ 代替原来的 x 计算斜率。由于斜率其实只依赖相对变化,中心化后算出来的 k 和原来完全一样。最后再根据 b = ȳ - kx̄ 换回原始变量下的截距。如果 x 的范围非常大,还可以进一步让 x 除以它的标准差,换算时再把截距换回来。这是数值计算里非常常规的操作,但在基础教程里很少强调。
我在实际项目中还遇到过一种更隐蔽的情况:x 在 100 附近,y 在 10000 附近,量级相差太大,计算残差平方和时会感觉数值不太稳定。处理思路一样,先中心化或者统一缩放,然后再拟合。
4.3 数据本身不是直线,但你以为它是
最小二乘法本身并不管数据是不是线性的,你给它一组明显弯曲的数据,它照样会硬生生画出一条直线。最典型的例子是增长型曲线:早期增长缓慢,中期快速上升,后期进入平台期。如果你只取中段数据来做线性拟合,可能看起来很完美,回归线几乎穿过所有点,但外推到两端就完全失效。
怎么发现这个问题?最直接的方法是看残差图。如果残差在 x 较小时普遍为负,在 x 中间时为正,在 x 较大时又变负,也就是呈 U 型或倒 U 型,说明直线假设不成立。这时候有两条路:一是换多项式拟合,加一个 x² 项;二是把数据分段,每段单独拟合。我个人的建议是,除非有明确理由,否则不要为了“让 R² 好看”而硬解释成直线,因为模型的解释能力比拟合数字更重要。
有一个常见误区是“只要 R² 高,模型就是对的”。R² 高只能说明直线和当前样本贴合,不代表关系本质是线性的,也不代表预测可靠。它和你的业务判断是两回事。
4.4 内插与外推的边界
最小二乘法拟合出来的直线是一个局部规律,它的第一用途是概括趋势,其次才是预测。内插指的是在现有 x 范围内估计 y,这个相对安全;外推指的是跑到 x 范围之外去预测,比如数据只有 1 到 5,却去预测 x = 50 的情况,这就非常危险。
为什么危险?因为拟合线只能告诉你“在这些数据覆盖的范围内,趋势大概是这样”。超过范围后,真实关系可能变成曲线、可能出现平台期、可能反向。不要因为公式里有一条斜线,就认为它可以永远延伸下去。每次报告拟合结果时,我建议在公式旁标注样本范围:实测范围为 x ∈ [1, 5],外推需谨慎。
4.5 常见问题速查表
| 现象 | 可能原因 | 快速检查方法 |
|---|---|---|
| 斜率明显不符合直觉 | 离群点影响 | 画散点图,剔除可疑点后重新拟合 |
| R² 很高但预测总差一点 | 线性假设不成立 | 看残差图是否呈 U 型或漏斗型 |
| x 是很大的时间戳,计算出来不稳定 | 数值范围过大 | 对 x 中心化后再带入公式 |
| 拟合线与数据点平均值不重合 | 原始公式没写对 | 验证 b = ȳ - kx̄ |
| 两个变量对调后结果不是同一关系 | 最小化方向不同 | 分析实际场景,确认谁是被解释变量 |
| 预测值超出数据范围 | 外推 | 标注样本范围,限制预测范围 |
这张表不是教条,它是我在实际使用中反复遇到的真实问题集合。遇到问题时先回到散点图和残差图,大多数坑都能在那里露出马脚。
5. 从纸笔到工具:电子表格和代码都要会
5.1 电子表格里的常规做法
如果数据量不大,手算能帮你建立直觉;但如果数据量很大,还是建议用软件。电子表格软件里有现成的函数:SLOPE、INTERCEPT、RSQ,分别对应已经推导过的公式。比如把 x 放在 A 列、y 放在 B 列,用 =SLOPE(B1:B5, A1:A5) 就能得到斜率 0.9,用 =INTERCEPT(B1:B5, A1:A5) 得到截距 1.3,用 =RSQ(B1:B5, A1:A5) 得到 R² = 0.81。
把结果和手算对照一下,确认一致后,再插入一个散点图,并在图表上添加趋势线和显示公式。这一步不是为了好看,而是为了让你直观看到这条线是不是穿过点的中心区域,有没有被某个极端点拉扯。电子表格的好处是操作快、修改方便,适合快速探索数据;坏处是很多人直接看软件吐出的一串数字,完全没有检查残差和适用边界。
无论你用什么工具,拟合完以后第一件事永远是回到图。看线、看点、看残差,看图比看数字更能发现问题。
5.2 用代码快速实现最小二乘拟合
当数据量达到几十万行,电子表格就不太够用了。这时用代码是最自然的选择。我平时用的组合是 Python 和 NumPy,一行代码就能完成拟合:
import numpy as np x = np.array([1, 2, 3, 4, 5]) y = np.array([2, 3, 5, 4, 6]) slope, intercept = np.polyfit(x, y, 1) print(slope, intercept) r_sq = np.corrcoef(x, y)[0, 1] ** 2 print(r_sq)np.polyfit 的第一个参数是 x,第二个是 y,第三个 1 表示拟合一次多项式,也就是直线。运行结果会和手算完全一致:斜率 0.9,截距 1.3,R² 0.81。如果不想依赖高层函数,想彻底理解每一步,可以自己用前面的公式实现:
x_mean = x.mean() y_mean = y.mean() slope = ((x - x_mean) * (y - y_mean)).sum() / ((x - x_mean) ** 2).sum() intercept = y_mean - slope * x_mean这样的代码虽然简单,但它清清楚楚地对应了纸面上的推导过程。以后遇到没有现成函数的环境,你也能凭这段逻辑快速写出来。我建议初学者至少手写一次这个版本,因为很多高阶包用起来方便,却也容易让人把底层原理忘干净。
5.3 拟合完以后我的固定动作
最后分享几个我每次拟合完都会做的步骤。第一,永远先画散点图,确认数据形态值得拟合直线。第二,检查斜率、截距、R² 三个数是否符合业务直觉。如果一个数据明显让斜率变成负数,而业务上你知道趋势应该是正向的,就一定要停下手来看看是不是计算错误,或者数据里有脏值。第三,画残差图,看残差是否随机分布在 0 附近。这一步能挡住大部分非线性问题。
我踩过一个很实际的坑:有一次做历史数据外推,拟合结果 R² 高达 0.95,我当时很高兴,直接拿它预测后面几个周期。结果数据一到,偏差非常大。回看 Y 型残差图才发现,数据其实有一个明显的非线性拐点,只是在拟合区间附近看起来呈直线。从那以后我牢记一句话:模型是局部工具,不是万能公式。
最小二乘法本身不复杂,公式一页纸就能写完,真正体现水平的是使用它时是否清楚边界、是否验证假设、是否解释了结果。把这条线画出来只是开始,知道它为什么合理、什么时候失效,才真的有参考价值。