随笔

最小二乘法:用 Python 拟合一条直线

从残差平方和出发推导直线拟合公式,并用可运行的 Python 示例画出观测点、拟合直线和残差。

引用本文

APA

Zhang, C. (2026, November 13). 最小二乘法:用 Python 拟合一条直线. Chester Zhang. https://chesterzhang.site/zh/writing/least-squares

GB/T 7714

Chester Zhang. 最小二乘法:用 Python 拟合一条直线[EB/OL]. (2026-11-13)[2026-11-13]. https://chesterzhang.site/zh/writing/least-squares.

BibTeX

@online{zhang2026least,
  author = {Zhang, Chester},
  title = {最小二乘法:用 Python 拟合一条直线},
  year = {2026},
  month = nov,
  date = {2026-11-13},
  organization = {Chester Zhang},
  url = {https://chesterzhang.site/zh/writing/least-squares},
  urldate = {2026-11-13},
}

给定一组点,怎样找一条最能代表它们的直线?最小二乘法把这个问题变成了一个可计算的目标。我们用五个观测点推导公式,再用 Python 算出直线并画出残差。

把“最贴近”写成一个目标

设第 ii 个观测点为 (xi,yi)(x_i,y_i),拟合直线为 y^=ax+b\widehat y=ax+b。在同一个 xix_i 处,观测值与拟合值之间的竖直差 ei=yi−(axi+b)e_i=y_i-(ax_i+b) 称为残差。

S(a,b)=∑i=1n(yi−axi−b)2.S(a,b)=\sum_{i=1}^{n}\bigl(y_i-ax_i-b\bigr)^2.(1)

最小二乘法选择让 S(a,b)S(a,b) 最小的斜率和截距。平方后,正负残差不会抵消。较大的残差也会得到更大的惩罚,因此离群点可能明显拉动拟合直线。

为什么拟合直线经过样本中心

记 xˉ\bar x 和 yˉ\bar y 为样本均值。把误差拆成中心化部分和常数部分,再利用中心化变量的和为零,可以消去交叉项。

S(a,b)=∑i=1n[(yi−yˉ)−a(xi−xˉ)]2+n(yˉ−axˉ−b)2.S(a,b)= \sum_{i=1}^{n}\bigl[(y_i-\bar y)-a(x_i-\bar x)\bigr]^2 +n(\bar y-a\bar x-b)^2.(2)

固定 aa 时,第一项不变,第二项在 b=yˉ−axˉb=\bar y-a\bar x 时达到零。所以拟合直线必经过 (xˉ,yˉ)(\bar x,\bar y)。把这个截距代回去,对 aa 求导并令导数为零,得到

a=∑i=1n(xi−xˉ)(yi−yˉ)∑i=1n(xi−xˉ)2,b=yˉ−axˉ.a=\frac{\sum_{i=1}^{n}(x_i-\bar x)(y_i-\bar y)} {\sum_{i=1}^{n}(x_i-\bar x)^2}, \qquad b=\bar y-a\bar x.(3)

只要 xix_i 不全相同,分母就为正,剩下的是开口向上的二次函数,所以得到唯一的全局最优解。若所有 xix_i 都相同,数据只能确定在这个横坐标处的最佳预测值 yˉ\bar y,无法单独确定斜率和截距。

用 Python 拟合并画出残差

点击代码块的运行按钮。散点是观测值,实线是拟合直线,虚线段标出各点的竖直残差。代码直接计算上面的公式,并打印残差平方和。

import matplotlib.pyplot as plt

x = [0, 1, 2, 3, 4]
y = [1.0, 2.1, 2.7, 4.2, 5.1]

n = len(x)
x_mean = sum(x) / n
y_mean = sum(y) / n
sxx = sum((xi - x_mean) ** 2 for xi in x)
if sxx == 0:
    raise ValueError("At least two distinct x values are required.")

sxy = sum((xi - x_mean) * (yi - y_mean) for xi, yi in zip(x, y))
slope = sxy / sxx
intercept = y_mean - slope * x_mean
fitted = [slope * xi + intercept for xi in x]
residuals = [yi - fit for yi, fit in zip(y, fitted)]
sse = sum(residual ** 2 for residual in residuals)

print(f"Slope: {slope:.3f}")
print(f"Intercept: {intercept:.3f}")
print(f"Sum of squared residuals: {sse:.4f}")

fig, ax = plt.subplots(figsize=(6.4, 3.8), layout="constrained")
ax.plot(x, fitted, color="#1d4ed8", label="Least-squares line")
ax.vlines(x, fitted, y, colors="#b45309", linestyles="--",
          linewidth=2, label="Residuals")
ax.scatter(x, y, color="#0f172a", zorder=3, label="Observations")
ax.set(xlabel="x", ylabel="y", title="Least-squares fit")
ax.grid(alpha=0.18)
ax.legend(loc="upper left")

这组数据得到 y^=1.030x+0.960\widehat y=1.030x+0.960,残差平方和为 0.13900.1390。可以把最后一个观测值从 5.15.1 改成 8.08.0 再运行一次,观察右侧的点如何把斜率拉高。拟合优度只说明直线对这组数据的贴合程度。用它预测观测范围之外的值时,还要判断线性关系是否有理由继续成立。

提出更正

提出更正

发现错误了吗?引用原文,并写下应改成什么。只有我会读到。

如果留下署名,即表示你同意:我采纳更正后,它可以公开显示在书的更正列表中。

阅读设置即选即用 · 仅保存在此设备

主题
墨水
桌面

仅保存在此浏览器中。

键盘快捷键

/
搜索全站
f
在本文中查找
j
下一节
k
上一节
n
下一条脚注
p
上一条脚注
t
目录
s
阅读设置
z
开启或关闭专注模式
g
术语表
?
本快捷键列表
Esc
关闭卡片、面板和弹出层

在输入框中打字时,快捷键不会生效。