☰
Python数值分析实战:超定方程组、最小二乘拟合与石油产量预测
2026/10/11 22:18:45 网站建设 项目流程

简介:这份资源是广东工业大学数值分析课程实验四的上机实验报告,面向正在学习数值分析、Python科学计算的高校学生,尤其适合需要完成超定方程组与最小二乘拟合相关作业的读者。报告围绕三个核心问题展开:求超定方程组的最小二乘解、给定观察数据求形如y=c0+c1/x的最小二乘拟合函数,以及石油产量预测建模,完整呈现了从算法原理、正规方程推导到Python代码实现与结果分析的全过程。压缩包内仅含1个docx文档,约112KB,内容涵盖题目描述、算法介绍、源代码与结果分析,结构紧凑、便于直接参考。目前已有211人学习下载。读者可从中获得超定方程组最小二乘解的完整求解思路、numpy线性代数求解器的实际用法、线性组合模型拟合的推导步骤,以及将数值方法应用于实际预测问题的综合案例,适合作为课程实验报告模板与复习参考。

1. 从一份数值分析实验包说起:超定方程组、最小二乘拟合与石油产量预测

如果你手头正好有一份 Python 数值分析实验包,里面塞着超定方程组求解、最小二乘拟合、石油产量预测三道题,那你大概率会先翻到代码那几页,看看能不能直接跑。我拿到这份实验包时也是这个反应——先跑通,再回头补理论。这份资源的核心价值在于:它把数值分析里最常被工程化使用的三块内容——超定方程组的最小二乘解、非线性拟合的线性化处理、多项式拟合的残差评估——用可运行的 Python 代码串了起来。适合正在学数值分析、需要交实验报告的学生,也适合想快速回顾最小二乘实现细节的开发者。代码依赖 numpy、sklearn、scipy、matplotlib,环境配好就能复现。

2. 超定方程组的最小二乘解:正规方程与 numpy 求解

2.1 为什么不能直接求逆,而要转成正规方程

超定方程组的定义很直白:方程个数 m 大于未知数个数 n。比如实验里方程组 a 有 4 个方程、2 个未知数,方程组 b 有 5 个方程、3 个未知数。这种方程组一般没有精确解,因为约束太多,未知数不够用。工程上遇到这种情况,通常不是去硬解,而是找一个“尽量满足所有方程”的近似解,也就是让残差平方和最小。

数学上的结论是:x* 是 Ax=b 的最小二乘解,当且仅当 x* 是正规方程 AᵀAx=Aᵀb 的解。这个定理把 m×n 的超定问题降成了 n×n 的方阵问题,只要 AᵀA 可逆,就能用常规线性求解器解出来。我一般会先检查 AᵀA 的条件数,如果条件数太大,直接解正规方程会放大数值误差,这时候可以考虑用 QR 分解或者 numpy 的 lstsq,但实验包里用的是正规方程路线,对这道题的数据规模来说够用。

2.2 方程组 a 和 b 的完整求解代码

实验包里的代码直接给出了两组方程组的矩阵和右端项,下面是我整理后的可运行版本,加了必要注释:

import numpy as np # 超定方程组 a:4 个方程,2 个未知数 A = np.array([[2, 4], [3, -5], [1, 2], [2, 1]]) b = np.array([[11], [3], [6], [7]]) # 构造正规方程 A^T A x = A^T b ATA = np.dot(A.T, A) ATb = np.dot(A.T, b) # 求解 x = np.linalg.solve(ATA, ATb) print("方程组 a 的最小二乘解:", x) # 预期输出:x1 ≈ 3.04029304, x2 ≈ 1.24175824 # 超定方程组 b:5 个方程,3 个未知数 A1 = np.array([[1, -2, 4], [1, -1, 1], [1, 0, 0], [1, 1, 1], [1, 2, 4]]) b2 = np.array([[0], [1], [2], [1], [0]]) ATA1 = np.dot(A1.T, A1) ATb1 = np.dot(A1.T, b2) x1 = np.linalg.solve(ATA1, ATb1) print("方程组 b 的最小二乘解:", x1) # 预期输出:x1 ≈ 1.65714286, x2 ≈ 0, x3 ≈ -0.42857143

逻辑说明:np.dot(A.T, A)计算的是 AᵀA,np.dot(A.T, b)计算的是 Aᵀb,两者构成正规方程。np.linalg.solve要求系数矩阵是方阵且可逆,这里 AᵀA 是 n×n 的对称正定矩阵,正常情况下可逆。参数方面,A 和 b 的维度必须匹配,A 是 m×n,b 是 m×1,否则矩阵乘法会报维度错误。

2.3 结果验证与残差检查

解出 x 之后,别急着写报告,先做一步残差检查。计算residual = A @ x - b,然后看残差的二范数。如果残差很大,说明数据本身噪声大或者模型不合适;如果残差接近零,说明方程组可能本来就是相容的,只是方程数多。实验里方程组 a 的解代入后残差平方和是一个有限值,说明确实是最小二乘意义下的最优解。这一步在工程上很重要,因为最小二乘解不保证每个方程都满足,只保证整体偏差最小。

提示:如果 AᵀA 接近奇异,np.linalg.solve会抛出LinAlgError,这时候改用np.linalg.lstsq(A, b, rcond=None)更稳妥,它内部用 SVD 分解,数值稳定性更好。

3. 最小二乘拟合:从非线性模型到线性组合的转化

3.1 为什么 y = c0 + c1/x 不能直接线性拟合

题目六给的数据是 x = [1, 2, 4, 5],y = [0.33, 0.40, 0.44, 0.45],要求拟合 y = c0 + c1/x。这个函数对 c0 和 c1 是线性的,但对 x 不是线性的,所以不能直接套用线性回归。常见做法是做变量替换:令 X = 1/x,Y = 1/y,原函数变成 1/y = c0 + c1·(1/x),也就是 Y = c0 + c1·X。这样就把问题转化成了标准线性回归,基函数是 φ0(x)=1 和 φ1(x)=1/x。

这个转化有个前提:y 不能为零,否则 1/y 无定义。实验数据里 y 都是正数,没问题。转化之后,正规方程组的系数矩阵和右端向量都可以用求和形式写出来,实验报告里给了详细的求和表,这里不重复。关键是理解:非线性拟合的线性化不是万能药,它改变了误差度量的方式。原始问题最小化的是 y 空间的残差平方和,转化后最小化的是 1/y 空间的残差平方和,两者不等价。如果 y 的数值范围很大,这种转化会引入偏差。我一般会先做转化拟合,再把结果代回原函数算残差,对比一下是否可接受。

3.2 用 sklearn 做线性回归的完整流程

实验包里用了 sklearn 的 LinearRegression,代码结构清晰,我整理成可直接复现的版本:

import numpy as np from sklearn.linear_model import LinearRegression import matplotlib.pyplot as plt # 原始数据 X = np.array([[1, 0.33], [2, 0.40], [4, 0.44], [5, 0.45]]) # 变量替换:X_n = 1/x, Y_n = 1/y X_n = 1 / X x_train = X_n[:, :1] # 1/x y_train = X_n[:, 1:2] # 1/y # 转成一维数组,sklearn 要求 a = np.array(x_train).reshape(-1) b = np.array(y_train).reshape(-1) # 线性回归 lineModel = LinearRegression() lineModel.fit(x_train, y_train) # 提取系数和截距 c1 = lineModel.coef_[0][0] # 斜率,对应 c1 c0 = lineModel.intercept_[0] # 截距,对应 c0 print("1/y = %.4f * (1/x) + %.4f" % (c1, c0)) # 预期:c1 ≈ 1.00931, c0 ≈ 2.0143 # 画图 y_predict = lineModel.predict(x_train) plt.scatter(a, b, c="blue", label="原始数据") plt.plot(x_train, y_predict, c="red", label="拟合直线") plt.xlabel('1/x') plt.ylabel('1/y') plt.legend() plt.show()

逻辑说明:X_n = 1 / X对矩阵每个元素取倒数,得到两列:第一列是 1/x,第二列是 1/y。x_train和y_train分别取这两列。LinearRegression.fit内部用最小二乘法解正规方程,和手写 AᵀAx=Aᵀb 等价。coef_是斜率,intercept_是截距。参数方面,fit的输入要求是二维数组,所以用reshape(-1)转一维只在画图时用,训练时保持二维。

3.3 拟合结果代回原函数验证

得到 c0 和 c1 后,拟合函数是 y = 2.0143 + 1.00931/x。代回原始 x 值算预测 y,再和真实 y 对比,可以算残差。实验数据点少,拟合曲线和散点基本吻合。如果残差出现系统性偏差(比如所有预测值都偏大),说明模型形式选错了,可能需要换基函数。这一步是很多同学容易跳过的,但它是判断拟合是否合理的关键。

注意:sklearn 的 LinearRegression 默认 fit_intercept=True,会自动估计截距。如果你手动构造了全 1 列作为基函数,需要设 fit_intercept=False,否则会重复计算截距。

4. 石油产量预测:多项式拟合的阶数选择与残差分析

4.1 直线、抛物线、立方曲线的拟合对比

实验四给的是 1994 到 2003 年共 10 个石油产量数据点,要求分别用直线、抛物线、立方曲线拟合,然后做残差分析,最后预测 2010 年产量。这道题的核心不是“哪个拟合最好”,而是“怎么用残差指标判断拟合优劣”。实验报告里用了三个指标:二范数的平方、残差的标准差、残差绝对值的均值。三个指标都指向立方曲线最好,直线最差。

为什么立方曲线更好?因为石油产量数据有加速增长的趋势,直线只能描述匀速增长,抛物线可以描述加速但方向固定,立方曲线能描述更复杂的曲率变化。但阶数不是越高越好,4 次多项式虽然拟合残差更小,但预测 2010 年时可能出现过拟合导致数值爆炸。实验里 4 次多项式的预测值没有列出来,但从代码看它也被计算了,只是没进最终对比表。

4.2 用 numpy.polyfit 和 scipy.leastsq 两种方式实现

实验包用了两种方法:np.polyfit做多项式拟合,scipy.optimize.leastsq做自定义函数拟合。下面是我整理后的核心代码:

import numpy as np import matplotlib.pyplot as plt from scipy.optimize import leastsq # 数据准备 x = np.arange(0, 10, 1, dtype='float') # 年份偏移,1994 对应 0 y = np.array([67.052, 68.008, 69.803, 72.024, 73.400, 72.063, 74.669, 74.487, 74.065, 76.777], dtype='float') # 多项式拟合:1 次、2 次、3 次 for deg in [1, 2, 3]: coeffs = np.polyfit(x, y, deg) p = np.poly1d(coeffs) y_pred = p(x) residual = y - y_pred print(f"{deg} 次多项式:") print(f" 系数:{coeffs}") print(f" 二范数平方:{(residual**2).sum():.4f}") print(f" 残差标准差:{residual.std():.4f}") print(f" 残差绝对值均值:{abs(residual).mean():.4f}") print(f" 2010 年预测:{p(2010 - 1994):.4f}") # 自定义函数拟合示例:线性函数 def linear_fun(s, x): k, b = s return k * x + b def dist(s, fun, x, y): return fun(s, x) - y param0 = [0, 0] var = leastsq(dist, param0, args=(linear_fun, x, y)) print("leastsq 线性拟合参数:", var[0])

逻辑说明:np.polyfit(x, y, deg)返回从高次到低次的系数,np.poly1d把系数转成可调用函数。leastsq的用法是传入残差函数dist、初始参数param0和额外参数args,它返回最优参数和协方差信息。参数方面,deg控制多项式阶数,param0是初始猜测,对线性函数来说随便给两个数就能收敛。

4.3 残差指标的计算与解读

残差分析是这道题的重点。二范数的平方是残差平方和,直接反映拟合误差总量;残差标准差是残差方差的平方根,反映误差的离散程度;残差绝对值均值反映平均偏差。三个指标从不同角度描述拟合质量,实验里立方曲线三项都最优。但要注意:残差小不代表预测准。立方曲线在 2010 年的预测值是 99.0420,直线是 83.3823,抛物线是 74.4106。抛物线预测值反而下降,和实际增长趋势矛盾,所以被排除。立方曲线预测值最高,但增长幅度是否合理需要结合业务判断。

提示:np.polyfit在数据点少、阶数高时容易过拟合,建议阶数不超过数据点数的平方根。10 个点用 3 次多项式是安全的,用 5 次以上就要警惕。

5. 避坑与排查:数值分析实验里最容易翻车的五个地方

5.1 矩阵维度不匹配导致 solve 报错

现象:运行np.linalg.solve(ATA, ATb)时抛出LinAlgError: Last 2 dimensions of the array must be square。原因:ATA 不是方阵,通常是 A 的转置写错或者 A 本身不是二维数组。解决:打印A.shape确认是 (m, n),ATA.shape应该是 (n, n)。如果 A 是一维数组,用A.reshape(-1, 1)转成列向量。

5.2 变量替换后忘记转回原空间

现象:拟合出的 c0 和 c1 代回原函数后,预测值和真实值差很远。原因:在 1/y 空间拟合,残差最小化的是 1/y 的误差,不是 y 的误差。如果 y 的数值范围大,转化会放大偏差。解决:拟合完成后,用原始 y 和预测 y 算残差,如果残差不可接受,考虑直接用非线性最小二乘(scipy.optimize.curve_fit)在原空间拟合。

5.3 polyfit 系数顺序记反

现象:用np.poly1d构造的函数图像完全不对。原因:np.polyfit返回的系数是从高次到低次排列,比如 2 次多项式返回 [a, b, c] 对应 ax² + bx + c。如果手动拼接函数时按低次到高次写,就会出错。解决:直接用np.poly1d(coeffs)生成函数,不要手动拼。

5.4 残差标准差和二范数平方混用

现象:报告里写“残差标准差最小的是立方曲线”,但表格里填的是二范数平方的值。原因:两个指标量纲不同,二范数平方是平方和,标准差是均方根,数值大小没有可比性。解决:明确每个指标的定义,二范数平方看总量,标准差看离散度,绝对值均值看平均偏差。三个指标一起看,不要只挑一个。

5.5 预测年份偏移量算错

现象:2010 年预测值代入的是 2010 而不是 2010-1994。原因:拟合时 x 用的是np.arange(0, 10),对应 1994 到 2003 的偏移量,预测时也要用偏移量。解决:统一用偏移量,或者拟合时直接用真实年份。我一般习惯用真实年份,避免来回换算。

6. 进阶技巧:用 curve_fit 做原空间非线性拟合与交叉验证

实验包里的拟合都是线性化或多项式路线,但实际工程中更常见的是直接在原空间做非线性最小二乘。scipy.optimize.curve_fit是更通用的工具,它不需要手动构造正规方程,直接传入模型函数和初始参数即可。下面是一个用 curve_fit 拟合 y = c0 + c1/x 的例子:

import numpy as np from scipy.optimize import curve_fit import matplotlib.pyplot as plt # 原始数据 x_data = np.array([1, 2, 4, 5], dtype=float) y_data = np.array([0.33, 0.40, 0.44, 0.45], dtype=float) # 定义模型函数 def model(x, c0, c1): return c0 + c1 / x # 拟合 popt, pcov = curve_fit(model, x_data, y_data, p0=[1, 1]) print("c0 = %.4f, c1 = %.4f" % (popt[0], popt[1])) # 计算残差 y_pred = model(x_data, *popt) residual = y_data - y_pred print("残差平方和:%.6f" % (residual**2).sum()) # 画图 x_plot = np.linspace(0.8, 5.5, 100) plt.scatter(x_data, y_data, c='blue', label='原始数据') plt.plot(x_plot, model(x_plot, *popt), c='red', label='拟合曲线') plt.xlabel('x') plt.ylabel('y') plt.legend() plt.show()

逻辑说明:curve_fit的第一个参数是模型函数,第二个是 x 数据,第三个是 y 数据,p0是初始参数猜测。它内部用 Levenberg-Marquardt 算法迭代求解,不需要手动构造正规方程。pcov是参数的协方差矩阵,对角线元素的平方根是参数标准差,可以用来评估参数估计的不确定性。参数方面,p0的选择会影响收敛速度,如果模型复杂,建议先用线性化方法估一个初值。

对比实验包里的线性化方法,curve_fit 的优势是直接在原空间最小化残差,避免了变量替换带来的偏差。但它的缺点是可能陷入局部最优,对初值敏感。我一般会先用线性化方法快速估参,再把结果作为 curve_fit 的初值,这样既快又稳。

另一个进阶技巧是交叉验证。实验数据只有 10 个点,直接看残差容易过拟合。可以把数据分成训练集和验证集,比如用 1994 到 2001 年拟合,用 2002 到 2003 年验证,看预测误差。如果验证误差远大于训练误差,说明模型过拟合。这个方法在石油产量预测里特别有用,因为时间序列数据不能随机划分,必须按时间顺序切分。

注意:curve_fit的p0如果给得太离谱,可能返回OptimizeWarning或者不收敛。遇到这种情况,先检查模型函数是否写错,再尝试多组初值。

从那以后我每次做最小二乘拟合,都会强制走一遍“线性化估初值 → curve_fit 精调 → 残差检查 → 交叉验证”的流程,不再直接信一组系数。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询