8. 线性方程和矩阵代数#

8.1. 概述#

经济学和金融学中的许多问题都需要解线性方程。

在本讲座中,我们将讨论线性方程及其应用。

为了说明线性方程的重要性,我们从一个两种商品的供需模型开始。

两种商品的情况非常简单,可以手动计算求解。

但我们经常需要考虑包含多种商品的市场。

在多种商品的情况下,我们面对的是大型线性方程组,有许多方程和未知数。

为了处理这样的系统,我们需要两样知识:

  • 矩阵代数(以及如何使用它的知识)以及

  • 将矩阵代数应用于感兴趣问题的计算机代码。

本讲座涵盖了这些步骤。

我们将使用以下函数库:

import numpy as np
import matplotlib.pyplot as plt

import matplotlib as mpl
FONTPATH = "fonts/SourceHanSerifSC-SemiBold.otf"
mpl.font_manager.fontManager.addfont(FONTPATH)
plt.rcParams['font.family'] = ['Source Han Serif SC']

8.2. 两种商品的例子#

在本节中,我们将讨论一个简单的两种商品例子,并通过以下两种方法解决:

  1. 纸笔计算

  2. 矩阵代数

正如我们将看到的,第二种方法更具普遍性。

8.2.1. 纸笔计算方法#

假设我们有两种相关的商品,比如:

  • 丙烷和乙醇,或

  • 大米和小麦等。

为了简化问题,我们将它们标记为商品0和商品1。

每种商品的需求取决于两种商品的价格:

(8.1)#q0d=10010p05p1q1d=50p010p1

(我们假设当任一商品的价格上涨时需求会下降,但其他情况也是可能的。)

让我们假设供给由以下方程给出:

(8.2)#q0s=10p0+5p1q1s=5p0+10p1

当供给等于需求时(q0s=q0dq1s=q1d),市场达到均衡。

这产生了以下线性方程组:

(8.3)#10010p05p1=10p0+5p150p010p1=5p0+10p1

我们可以用纸笔计算得到:

p0=4.41p1=1.18.

将这些结果代入(8.1)(8.2)中,可得均衡数量:

q0=50q1=33.82.

8.2.2. 展望未来#

在两种商品的情况下,纸笔计算方法很容易。

但如果有很多种商品呢?

对于这样的问题,我们需要矩阵代数。

在用矩阵代数解决问题之前,让我们先回顾一下向量和矩阵的基础知识,包括理论和计算。

8.3. 向量#

一个长度为n向量就是一个由n个数字组成的序列(或数组,或元组),我们将其写作x=(x1,,xn)x=[x1,,xn]

我们可以将这些序列横向或纵向写出。

但当我们使用矩阵运算时,我们默认假设向量是列向量。

所有n维向量的集合用Rn表示。

Example 8.1

  • R2是平面 — 即所有(x1,x2)对的集合。

  • R3是三维空间 — 即所有(x1,x2,x3)向量的集合。 向量通常在视觉上表示为从原点到某点的箭头。

这里是一个可视化示例。

Hide code cell source
fig, ax = plt.subplots()
# 通过原点建立坐标轴
for spine in ['left', 'bottom']:
    ax.spines[spine].set_position('zero')
for spine in ['right', 'top']:
    ax.spines[spine].set_color('none')

ax.set(xlim=(-5, 5), ylim=(-5, 5))

vecs = ((2, 4), (-3, 3), (-4, -3.5))
for v in vecs:
    ax.annotate('', xy=v, xytext=(0, 0),
                arrowprops=dict(facecolor='blue',
                shrink=0,
                alpha=0.7,
                width=0.5))
    ax.text(1.1 * v[0], 1.1 * v[1], str(v))
plt.show()
_images/79c3890876c858c5ec575329cb3865b52c7c6c467e739a12240e6ce334a41202.png

8.3.1. 向量运算#

有时我们需要修改向量。

对向量最常见的两种运算是加法和标量乘法,我们现在来描述这两种运算。

当我们对两个向量进行加法运算时,我们是逐元素相加。

Example 8.2

[42]+[33]=[4+32+3]=[71].

一般来说,

x+y=[x1x2xn]+[y1y2yn]:=[x1+y1x2+y2xn+yn].

我们可以在R2中将向量加法可视化如下。

Hide code cell source
fig, ax = plt.subplots()
# 通过原点建立坐标轴
for spine in ['left', 'bottom']:
    ax.spines[spine].set_position('zero')
for spine in ['right', 'top']:
    ax.spines[spine].set_color('none')

ax.set(xlim=(-2, 10), ylim=(-4, 4))
# ax.grid()
vecs = ((4, -2), (3, 3), (7, 1))
tags = ('(x1, x2)', '(y1, y2)', '(x1+x2, y1+y2)')
colors = ('blue', 'green', 'red')
for i, v in enumerate(vecs):
    ax.annotate('', xy=v, xytext=(0, 0),
                arrowprops=dict(color=colors[i],
                shrink=0,
                alpha=0.7,
                width=0.5,
                headwidth=8,
                headlength=15))
    ax.text(v[0] + 0.2, v[1] + 0.1, tags[i])

for i, v in enumerate(vecs):
    ax.annotate('', xy=(7, 1), xytext=v,
                arrowprops=dict(color='gray',
                shrink=0,
                alpha=0.3,
                width=0.5,
                headwidth=5,
                headlength=20))
plt.show()
_images/cf94fcba4b4127f164fc27c53e6ac5807eb11dfe3232006ad2a4ff7090580de3.png

标量乘法是一种将向量 x 与一个标量进行元素级别相乘的运算。

Example 8.3

2[37]=[2×32×7]=[614].

更一般地,它取一个数 γ 和一个向量 x,得到

γx:=[γx1γx2γxn].

标量乘法在下图中进行了说明。

Hide code cell source
fig, ax = plt.subplots()
# 通过原点建立坐标轴
for spine in ['left', 'bottom']:
    ax.spines[spine].set_position('zero')
for spine in ['right', 'top']:
    ax.spines[spine].set_color('none')

ax.set(xlim=(-5, 5), ylim=(-5, 5))
x = (2, 2)
ax.annotate('', xy=x, xytext=(0, 0),
            arrowprops=dict(facecolor='blue',
            shrink=0,
            alpha=1,
            width=0.5))
ax.text(x[0] + 0.4, x[1] - 0.2, '$x$', fontsize='16')

scalars = (-2, 2)
x = np.array(x)

for s in scalars:
    v = s * x
    ax.annotate('', xy=v, xytext=(0, 0),
                arrowprops=dict(facecolor='red',
                shrink=0,
                alpha=0.5,
                width=0.5))
    ax.text(v[0] + 0.4, v[1] - 0.2, f'${s} x$', fontsize='16')
plt.show()
_images/7d4f44d624e4e6aa100c364bfb484a900b98a7da18b16f03c807189bce663acd.png

在Python中,向量可以用列表或元组表示, 例如 x = [2, 4, 6]x = (2, 4, 6)

然而,更常见的是用 NumPy数组来表示向量。

使用NumPy数组的一个优点是标量乘法和加法的语法非常自然。

x = np.ones(3)            # 三个元素为一的向量
y = np.array((2, 4, 6))   # 将 (2, 4, 6) 转换为 NumPy 数组
x + y                     # 每个元素相加
array([3., 5., 7.])
4 * x                     # 标量乘法
array([4., 4., 4.])

8.3.2. 内积和范数#

向量 x,yRn内积定义为

xy=[x1x2xn][y1y2yn]=x1y1+x2y2++xnyn:=i=1nxiyi.

向量 x范数表示其”长度”(即,其与零向量的距离),定义为

x:=xx:=(i=1nxi2)1/2.

表达式 xy 可以被理解为 xy 之间的”距离”。

内积和范数可以按以下方式计算

np.sum(x*y)      # x和y的内积
12.0
x @ y            # 另外一种计算内积的办法
12.0
np.sqrt(np.sum(x**2))  # x的范数,方法一
1.7320508075688772
np.linalg.norm(x)      # x的范数,方法二
1.7320508075688772

8.4. 矩阵运算#

当我们讨论线性价格方程组时,我们提到了使用矩阵代数。

矩阵代数类似于算术代数。

让我们回顾一些细节。

8.4.1. 加法和标量乘法#

就像向量一样,我们可以对矩阵进行加法、减法和标量乘法。

标量乘法和加法是向量情况的推广:

Example 8.4

3[21305]=[639015].

一般来说,对于任意数 γ 和任意矩阵 A

γA=γ[a11a1kan1ank]:=[γa11γa1kγan1γank].

Example 8.5

考虑这个矩阵加法的例子,

[1573]+[12109]=[134712].

一般来说,

A+B=[a11a1kan1ank]+[b11b1kbn1bnk]:=[a11+b11a1k+b1kan1+bn1ank+bnk].

在后一种情况下,矩阵必须具有相同的形状才能使定义有意义。

8.4.2. 矩阵乘法#

我们还有一个相乘两个矩阵的约定。

矩阵乘法的规则推广了上面讨论的内积的概念。

如果 AB 是两个矩阵,那么它们的乘积 AB 的形成是通过取 A 的第 i 行和 B 的第 j 列的内积作为其第 i,j 个元素。

如果 An×k 的,Bj×m 的,那么要相乘 AB,我们需要 k=j,而得到的矩阵 ABn×m 的。

Example 8.6

这里是一个 2×2 矩阵乘以 2×1 向量的例子。

Ax=[a11a12a21a22][x1x2]=[a11x1+a12x2a21x1+a22x2]

作为一个重要的特殊情况,考虑将 n×k 矩阵 Ak×1 列向量 x 相乘。

根据前面的规则,结果是一个 n×1 列向量。

(8.4)#Ax=[a11a12a1kai1ai2aikan1an2ank]n×k[x1x2xk]k×1:=[a11x1+a22x2++a1kxkai1x1+ai2x2++aikxkan1x1+an2x2++ankxk]n×1

下面展示了两个矩阵的乘法。

AB=[a11a12a21a22][b11b12b21b22]:=[a11b11+a12b21a11b12+a12b22a21b11+a22b21a21b12+a22b22]

有许多教程可以帮助你进一步可视化这个操作,例如

Note

与数字乘积不同,ABBA 通常不相等。

一个重要的特殊情况是单位矩阵,它在主对角线上有 1,其他地方都是 0:

I=[1001]

作为练习请验证以下内容:

  • 如果 An×k 矩阵,Ik×k 单位矩阵,那么 AI=A,并且

  • 如果 In×n 单位矩阵,那么 IA=A

8.4.3. NumPy中的矩阵#

NumPy 数组也被用作矩阵,并且对所有标准矩阵运算都有快速、高效的函数和方法。

你可以通过以下方式从元组的元组(或列表的列表)手动创建它们

A = ((1, 2),
     (3, 4))

type(A)
tuple
A = np.array(A)

type(A)
numpy.ndarray
A.shape
(2, 2)

shape 属性是一个给出行数和列数的元组 — 更多讨论请参见这里

要获得 A 的转置,使用 A.transpose() 或更简单地使用 A.T

有许多方便的函数用于创建常见矩阵(零矩阵、单位矩阵等) — 请参见这里

由于默认情况下操作是按元素执行的,标量乘法和加法具有非常自然的语法。

A = np.identity(3)    # 3 x 3 单位矩阵
B = np.ones((3, 3))   # 3 x 3 元素为一的矩阵
2 * A
array([[2., 0., 0.],
       [0., 2., 0.],
       [0., 0., 2.]])
A + B
array([[2., 1., 1.],
       [1., 2., 1.],
       [1., 1., 2.]])

我们用 @ 来进行矩阵乘法。

Note

其中 A @ B 是矩阵乘法, 但是 A * B是每个元素之间的运算。

8.4.4. 矩阵形式的两种商品模型#

我们现在可以重新审视两种商品模型,并通过矩阵代数数值求解 (8.3) 方程。

这涉及一些额外的步骤,但这种方法广泛适用,我们将在求解包含更多商品时用上。

首先,我们将 (8.1) 重写为

(8.5)#qd=Dp+hwhereqd=[q0dq1d]D=[105110]andh=[10050].

回想一下,pR2 是两种商品的价格。

(请检查 qd=Dp+h 是否表示与 (8.1) 相同的方程。)

我们将 (8.2) 重写为

(8.6)#qs=Cpwhereqs=[q0sq1s]andC=[105510].

现在供给和需求的相等可以表示为 qs=qd,或

Cp=Dp+h.

我们可以重新排列这些项得到

(CD)p=h.

如果所有项都是数字,我们可以求解价格为 p=h/(CD)

矩阵代数允许我们做类似的事情:我们可以使用 CD 的逆矩阵来求解均衡价格:

(8.7)#p=(CD)1h.

在我们实施解决方案之前,让我们考虑一个更一般的问题。

8.4.5. 更多商品#

考虑有更多商品的需求系统是很自然的。

例如,即使在能源商品中也有许多不同的商品, 包括原油、汽油、煤炭、天然气、乙醇和铀。

这些商品的价格是相关的,所以一起研究它们是有意义的。

对于大型系统,纸笔方法会变得非常耗时。

但幸运的是,上面描述的矩阵方法基本上保持不变。

一般来说,我们可以将需求方程写为 qd=Dp+h,其中

  • qd 是一个 n×1 的向量,表示 n 种不同商品的需求量。

  • D 是一个 n×n 的”系数”矩阵。

  • h 是一个 n×1 的常数值向量。

类似地,我们可以将供给方程写为 qs=Cp+e,其中

  • qs 是一个 n×1 的向量,表示相同商品的供给量。

  • C 是一个 n×n 的”系数”矩阵。

  • e 是一个 n×1 的常数值向量。

为了找到均衡,我们求解 Dp+h=Cp+e,或

(8.8)#(DC)p=eh.

那么,n 种不同商品的价格向量是

p=(DC)1(eh).

8.4.6. 一般线性系统#

上述问题的一个更一般版本看起来如下。

(8.9)#a11x1+a12x2++a1nxn=b1an1x1+an2x2++annxn=bn

这里的目标是解出”未知数” x1,,xn

我们给定系数 a11,,ann 和常数 b1,,bn

注意,我们处理的是未知数数量等于方程数量的情况。

这是我们最有可能找到明确定义解的情况。

(其他情况被称为超定欠定方程组 — 我们将在后续讲座中讨论这些情况。)

用矩阵形式表示,方程组 (8.9) 变为

(8.10)#Ax=bwhereA=[a11a1nan1ann]andb=[b1bn].

例如,(8.8) 具有这种形式,其中

A=DC,b=ehx=p.

当考虑诸如 (8.10) 这样的问题时,我们至少需要问以下一些问题:

  • 解是否真的存在?

  • 如果解存在,我们应该如何计算它?

8.5. 解方程组#

再次回顾方程组 (8.9),我们在此重新写为

(8.11)#Ax=b

我们面临的问题是找到一个向量 xRn,使其解决 (8.11) ,其中 bA 是给定的。

我们可能并不总能找到一个唯一的向量 x 来解决 (8.11)

我们在下面举例说明两种这样的情况。

8.5.1. 无解#

考虑由以下给出的方程组:

x+3y=32x+6y=8.

可以手动验证这个系统没有可能的解。

为了说明为什么会出现这种情况,让我们绘制这两条直线。

fig, ax = plt.subplots()
x = np.linspace(-10, 10)
plt.plot(x, (3-x)/3, label=f'$x + 3y = 3$')
plt.plot(x, (-8-2*x)/6, label=f'$2x + 6y = -8$')
plt.legend()
plt.show()
_images/171bc7305cc4c31ffd7a3988e6e4bec781432a59064f6ea34adc6539fb227408.png

显然,这些是平行线,因此我们永远无法找到一个点 xR2 使得这些线相交。

因此,这个系统没有可能的解。

我们可以将这个系统用矩阵形式重写为

(8.12)#Ax=bwhereA=[1326]andb=[38].

可以注意到,矩阵 A 的第 2(2,6) 只是第 1(1,3) 的标量倍数。

在这种情况下,矩阵 A 的行被称为线性相关的。

Note

读者可以在这里找到关于线性相关和线性无关的详细解释。

但在接下来的内容中不需要这些细节。

8.5.2. 多解#

现在考虑,

x2y=42x+4y=8.

任何满足 x=2y4 的向量 v=(x,y) 都将解决上述系统。

由于我们可以找到无限多个这样的向量,这个系统有无穷多个解。

这是因为对应矩阵的行

(8.13)#A=[1224].

是线性相关的 — 你能看出为什么吗?

我们现在对 (8.11) 中的 A 施加条件,以排除这些问题。

8.5.3. 非奇异矩阵#

对于每个方阵,我们都可以指定一个唯一的数,称为行列式

对于 2×2 矩阵,行列式由以下公式给出:

[abcd]=adbc.

如果 A 的行列式不为零,我们就说 A非奇异的

当且仅当 A 的行和列是线性无关的,方阵 A 才是非奇异的。

关于矩阵逆的更详细解释可以在这里找到。

你可以自己检查 (8.12)(8.13) 中具有线性相关行的矩阵是奇异矩阵。

这为我们提供了一个有用的单数值标准,用来判断一个方阵是否可逆。

特别地,方阵 A 具有非零行列式,当且仅当它具有逆矩阵 A1,满足 AA1=A1A=I

因此,如果我们用 A1 左乘 Ax=b 的两边,我们得到

(8.14)#x=A1b.

这是对 Ax=b 的解答 — 这就是我们要寻找的解。

8.5.4. 使用NumPy求解线性方程#

根据上述例子中,我们得到了矩阵方程:

p=(CD)1h.

其中 CDh(8.5)(8.6) 给出。

这个方程类似于 (8.14),其中 A=(CD)1b=h,且 x=p

我们现在可以使用NumPy的linalg子模块求解均衡价格。

所有这些程序都是经过时间考验和高度优化的FORTRAN代码的Python前端。

C = ((10, 5),      # 矩阵 C
     (5, 10))

现在我们把它录入到NumPy数组中。

C = np.array(C)
D = ((-10, -5),     # 矩阵 D
     (-1, -10))
D = np.array(D)
h = np.array((100, 50))   # 向量 h
h.shape = 2,1             # 将h转换为列向量
from numpy.linalg import det, inv
A = C - D
#检查A是否为奇异矩阵(行列式是否为零),是否可逆
det(A)
340.0000000000001
A_inv = inv(A)  #计算逆矩阵
A_inv
array([[ 0.05882353, -0.02941176],
       [-0.01764706,  0.05882353]])
p = A_inv @ h  #均衡价格
p
array([[4.41176471],
       [1.17647059]])
q = C @ p  # 均衡数量
q
array([[50.        ],
       [33.82352941]])

注意,我们得到的解与纸笔计算的情况相同。

我们还可以使用 solve(A, h) 来求解 p,如下所示。

from numpy.linalg import solve
p = solve(A, h)  # 均衡价格
p
array([[4.41176471],
       [1.17647059]])
q = C @ p  # 均衡数量
q
array([[50.        ],
       [33.82352941]])

观察我们如何通过 inv(A) @ y 或使用 solve(A, y) 来求解 x=A1y

后一种方法使用了一种不同的算法,在数值上更加稳定,因此应该是默认选项。

8.6. 练习#

Exercise 8.1

让我们考虑一个有3种商品的市场 - 商品0、商品1和商品2。

每种商品的需求取决于其他两种商品的价格,由以下公式给出:

q0d=9015p0+5p1+5p2q1d=60+5p010p1+10p2q2d=50+5p0+5p15p2

(这里,当自身价格上涨时需求下降,但当其他商品价格上涨时需求增加。)

每种商品的供给由以下公式给出:

q0s=10+20p0q1s=15+15p1q2s=5+10p2

当供给等于需求时,市场达到均衡,即 q0d=q0sq1d=q1sq2d=q2s

  1. 将市场均衡条件设置为线性方程组。

  2. 使用矩阵代数求解均衡价格。分别使用 numpy.linalg.solveinv(A) 方法来做这个。比较这两种解法。

Exercise 8.2

在讲座的早些时候,我们讨论了Ax=b这个方程组没有解的情况。

在这种情况下,Ax=b被称为不相容方程组。

面对不相容系统时,我们尝试找到最佳的”近似”解。

有多种方法可以做到这一点,其中一种是最小二乘法

假设我们有一个不相容系统

(8.15)#Ax=b

其中A是一个m×n矩阵,b是一个m×1列向量。

对于(8.15)最小二乘解是一个n×1列向量x^,使得对于所有其他向量xRnAx^b的距离小于Axb的距离。

即,

Ax^bAxb

可以证明,对于方程组Ax=b,最小二乘解x^

(8.16)#x^=(ATA)1ATb

现在考虑一种商品的线性需求曲线的一般方程:

p=mnq

其中p是商品的价格,q是需求量。

假设我们正试图估计mn的值。

我们通过重复观察价格和数量(例如,每个月)来做到这一点,然后选择mn来拟合pq之间的关系。

我们有以下观察结果:

价格

需求量

1

9

3

7

8

3

要求需求曲线p=mnq通过所有这些点,得到以下三个方程:

1=m9n3=m7n8=m3n

因此,我们得到一个方程组Ax=b,其中A=[191713]x=[mn]b=[138]。 可以验证这个系统没有解。

(问题在于我们有三个方程但只有两个未知数。)

因此,我们将尝试找到x的最佳近似解。

  1. 使用(8.16)和矩阵代数找到最小二乘解x^

  2. 使用numpy.linalg.lstsq找到最小二乘解,并比较结果。

8.6.1. 延伸阅读#

numpy.linalg 子模块的文档可以阅读这里

如果对更高级的线性代数知识感兴趣可以继续阅读这里