▤书架 ← 返回索引
▶目录

阶段 09 讲义

数学 11h:线代 · 矩阵运算与逆矩阵
编程 9h:元组与字符串深入


第一部分 · 数学

阶段 8 我们学了行列式,算出来是"一个数"。这一阶段学矩阵,它本身是"一张表"。

主角是逆矩阵。你可以先把它理解成"矩阵里的除法":有了它,$AX=b$ 就能像 $2x=6$ 那样"两边同除"解出来。

但矩阵比数麻烦得多。不是每个矩阵都有逆,乘法不能交换顺序,连"同除"都要小心。这一阶段就是把这些坑摸清。

1 矩阵是什么

直觉上,矩阵就是一张矩形的数表。

$$A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}, \qquad B = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}$$

记号约定:

说法 含义 例子
$m \times n$ 矩阵 $m$ 行、$n$ 列 $B$ 是 $2\times3$
$a_{ij}$ 第 $i$ 行、第 $j$ 列的元素 上面的 $a_{12}=2$
方阵 行数 = 列数 $A$ 是 $2$ 阶方阵
行向量 / 列向量 只有一行的 / 只有一列的矩阵 —

💡 行列式 vs 矩阵(阶段 8 讲过,这里再强调一次):行列式用竖线 $|\;|$,算出来是一个数;矩阵用圆括号或方括号,本身是一张表。写 $|A|$ 或 $\det(A)$ 的时候算的是矩阵的行列式,那才是数。

矩阵更深一层的身份,是一个线性变换。

一个矩阵乘以一个向量,相当于把这个向量旋转、拉伸或投影到别的地方。比如

$$\begin{pmatrix} 0 & -1 \\ 1 & 0 \end{pmatrix}\begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} 0 \\ 1 \end{pmatrix}$$

左边的矩阵把 $(1,0)$ 转到了 $(0,1)$,它是个旋转 $90°$ 的机器。

⭐ 换个角度看,矩阵不只是"存数据的表格",它是对空间做操作的动作。
认知科学里,感官输入到行为输出、神经活动到血氧信号,都可以建模成这种"操作"。

2 矩阵加法与数乘(简单)

同型(行数、列数都一样)的两个矩阵才能相加,对应位置相加:

$$\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} + \begin{pmatrix} 5 & 6 \\ 7 & 8 \end{pmatrix} = \begin{pmatrix} 1+5 & 2+6 \\ 3+7 & 4+8 \end{pmatrix} = \begin{pmatrix} 6 & 8 \\ 10 & 12 \end{pmatrix}$$

数乘:一个数乘矩阵,每个元素都要乘:

$$3\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} = \begin{pmatrix} 3 & 6 \\ 9 & 12 \end{pmatrix}$$

💡 这两条和行列式不一样。行列式是"某行乘 $k$,整个行列式乘 $k$"(阶段 8 性质 3),不是每个元素乘。

3 矩阵乘法 ⭐⭐(重点,最容易错)

规则一句话:左边取"行",右边取"列",对应相乘再相加。

$$\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}$$

算第 1 行第 1 列的位置:取左边第 1 行 $(1,2)$,右边第 1 列 $(0,1)$,

$$1\times0 + 2\times1 = 2$$

算第 1 行第 2 列:左边第 1 行 $(1,2)$ × 右边第 2 列 $(1,0)$,

$$1\times1 + 2\times0 = 1$$

四个位置都算完:

$$\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 4 & 3 \end{pmatrix}$$

维度规则(先查这个,再动手算):

$$(m\times n)\cdot(n\times p) = (m\times p)$$

左边列数必须等于右边行数,否则根本不能相乘。

3.1 ⚠️ 三条铁律

铁律一:顺序不能换

$$\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix} = \begin{pmatrix} 2 & 1 \\ 4 & 3 \end{pmatrix}, \qquad \begin{pmatrix} 0 & 1 \\ 1 & 0 \end{pmatrix}\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} = \begin{pmatrix} 3 & 4 \\ 1 & 2 \end{pmatrix}$$

同一对数,换个顺序,结果完全不同。在数的世界里 $3\times5=5\times3$,矩阵世界里这条不成立。

⭐ $AB \ne BA$ 是常态,$AB=BA$ 才是巧合。

铁律二:不满足消去律

在数的世界里,$ab=ac$ 且 $a\ne0$,就能约掉 $a$ 得 $b=c$。矩阵里不行:

$$\begin{pmatrix} 1 & 0 \\ 0 & 0 \end{pmatrix}\begin{pmatrix} 1 & 1 \\ 1 & 1 \end{pmatrix} = \begin{pmatrix} 1 & 0 \\ 0 & 0 \end{pmatrix}\begin{pmatrix} 1 & 1 \\ 5 & 5 \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 0 & 0 \end{pmatrix}$$

左边相等,但右边的两个矩阵明显不一样(一个是 $\begin{pmatrix}1&1\\1&1\end{pmatrix}$,另一个是 $\begin{pmatrix}1&1\\5&5\end{pmatrix}$)。

原因在于 $\begin{pmatrix}1&0\\0&0\end{pmatrix}$ 把第二行"抹掉"了,所以第二行是什么都无所谓。这个矩阵没有逆(下一节讲)。

💡 为什么会在意这个:回归分析里我们经常要"两边同乘 $X^T$"然后约掉,只有在相关矩阵可逆时才能这么做。这正是阶段 8 讲的多重共线性问题在矩阵语言下的样子。

铁律三:有零因子

两个都不是零矩阵的矩阵,乘起来可以是零矩阵:

$$\begin{pmatrix} 0 & 1 \\ 0 & 0 \end{pmatrix}\begin{pmatrix} 0 & 1 \\ 0 & 0 \end{pmatrix} = \begin{pmatrix} 0 & 0 \\ 0 & 0 \end{pmatrix}$$

数的世界里"两数相乘为零 ⇒ 至少一个为零",矩阵里不成立。

4 转置

转置 $A^T$ 就是把行列互换(第 $i$ 行变成第 $i$ 列)。

$$\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}^T = \begin{pmatrix} 1 & 4 \\ 2 & 5 \\ 3 & 6 \end{pmatrix}$$

两条重要性质:

性质 说明
$(A^T)^T = A$ 转两次转回来
$(A+B)^T = A^T + B^T$ 加法可拆
$(AB)^T = B^T A^T$ ⚠️ 顺序反了!

⚠️ $(AB)^T = B^TA^T$ 而不是 $A^TB^T$,这是初学者最常写错的一条。
记忆法是把它想成照镜子,"先穿袜子再穿鞋"在镜子里变成"先脱鞋再脱袜子",顺序必然颠倒。

5 单位矩阵与对角矩阵

单位矩阵 $I_n$ 的主对角线全是 1,其余全是 0。

$$I_2 = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}, \qquad I_3 = \begin{pmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{pmatrix}$$

它的作用就像数字里的 $1$:

$$AI = IA = A$$

对角矩阵只有主对角线可能非零。$n$ 个数的对角矩阵,作用就是把每个分量分别拉伸相应倍数。

6 逆矩阵 ⭐⭐(本阶段的核心)

6.1 定义

对方阵 $A$,如果存在矩阵 $B$ 使得

$$AB = BA = I$$

那么称 $A$ 可逆,$B$ 就是 $A$ 的逆矩阵,记作 $A^{-1}$。

直觉上它就是矩阵里的倒数。$A$ 做了一次变换,$A^{-1}$ 把它变回来。

⚠️ 两个前提:必须是方阵,而且 $\det(A) \ne 0$。
$\det(A) = 0$ 的矩阵叫奇异矩阵,没有逆,就像数字里 $0$ 没有倒数。

6.2 $2\times2$ 逆矩阵公式(必须背下来)

$$A = \begin{pmatrix} a & b \\ c & d \end{pmatrix} \quad\Longrightarrow\quad A^{-1} = \frac{1}{ad-bc}\begin{pmatrix} d & -b \\ -c & a \end{pmatrix}$$

口诀:主对角互换,副对角变号,除以行列式。

6.3 完整例子

求 $A = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}$ 的逆矩阵。

第一步:算行列式

$$\det(A) = 1\times4 - 2\times3 = 4 - 6 = -2 \ne 0 \quad ✓ \text{可逆}$$

第二步:套公式(主对角 $1,4$ 互换;副对角 $2,3$ 变号)

$$A^{-1} = \frac{1}{-2}\begin{pmatrix} 4 & -2 \\ -3 & 1 \end{pmatrix} = \begin{pmatrix} -2 & 1 \\ \dfrac{3}{2} & -\dfrac{1}{2} \end{pmatrix}$$

第三步:验算(这一步别省,它救过很多人)

$$AA^{-1} = \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix}\begin{pmatrix} -2 & 1 \\ \frac{3}{2} & -\frac{1}{2} \end{pmatrix} = \begin{pmatrix} -2+3 & 1-1 \\ -6+6 & 3-2 \end{pmatrix} = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix} = I \quad ✓$$

6.4 逆矩阵的性质(和"倒数"对照着记)

矩阵 数字 注意
$(A^{-1})^{-1} = A$ $(a^{-1})^{-1} = a$ 一致
$\det(A^{-1}) = \dfrac{1}{\det(A)}$ 无对应 逆的行列式是倒过来
$(A^T)^{-1} = (A^{-1})^T$ 无对应 转置和求逆可交换
$(AB)^{-1} = B^{-1}A^{-1}$ $(ab)^{-1} = b^{-1}a^{-1}$ ⚠️ 顺序反了

数值验证(拿 $P = \begin{pmatrix}1&2\\3&4\end{pmatrix}$,$Q = \begin{pmatrix}0&1\\1&0\end{pmatrix}$ 实算):

检查 结果
$PQ = \begin{pmatrix}2&1\\4&3\end{pmatrix}$ det $(PQ) = 2$
$(PQ)^{-1}$ $\begin{pmatrix}\frac{3}{2}&-\frac{1}{2}\\-2&1\end{pmatrix}$
$Q^{-1}P^{-1}$ $\begin{pmatrix}\frac{3}{2}&-\frac{1}{2}\\-2&1\end{pmatrix}$ ✓ 相等
$P^{-1}Q^{-1}$ $\begin{pmatrix}0&1\\1&0\end{pmatrix}$... ✗ 不相等
$\det(P) = -2$,$\det(P^{-1})$ $-\frac{1}{2}$ ✓

$(AB)^{-1} = B^{-1}A^{-1}$ 这一条必须记住顺序,它和 $(AB)^T = B^TA^T$ 是同一个"顺序颠倒"的规律。

7 用逆矩阵解方程组

有了逆矩阵,方程组 $AX = b$ 就能"两边同乘 $A^{-1}$":

$$AX = b \quad\Longrightarrow\quad \underbrace{A^{-1}A}_{I}X = A^{-1}b \quad\Longrightarrow\quad X = A^{-1}b$$

这就是矩阵版的两边同除。

7.1 完整例子

求解 $\begin{cases} 2x + y = 5 \\ x - 3y = -1 \end{cases}$

写成矩阵形式:

$$A = \begin{pmatrix} 2 & 1 \\ 1 & -3 \end{pmatrix}, \qquad X = \begin{pmatrix} x \\ y \end{pmatrix}, \qquad b = \begin{pmatrix} 5 \\ -1 \end{pmatrix}$$

第一步:算行列式

$$\det(A) = 2\times(-3) - 1\times1 = -7 \ne 0 \quad ✓$$

第二步:求逆(主对角互换,副对角变号)

$$A^{-1} = \frac{1}{-7}\begin{pmatrix} -3 & -1 \\ -1 & 2 \end{pmatrix} = \begin{pmatrix} \frac{3}{7} & \frac{1}{7} \\ \frac{1}{7} & -\frac{2}{7} \end{pmatrix}$$

第三步:乘 $b$

$$X = A^{-1}b = \begin{pmatrix} \frac{3}{7} & \frac{1}{7} \\ \frac{1}{7} & -\frac{2}{7} \end{pmatrix}\begin{pmatrix} 5 \\ -1 \end{pmatrix} = \begin{pmatrix} \frac{15-1}{7} \\ \frac{5+2}{7} \end{pmatrix} = \begin{pmatrix} 2 \\ 1 \end{pmatrix}$$

验根:$2(2)+1 = 5$ ✓,$2 - 3(1) = -1$ ✓

💡 和阶段 8 的克拉默法则比较:结果一样(都是 $\frac{D_i}{D}$),但逆矩阵法更"结构化"。它把"解方程组"变成了"求一个矩阵",$A^{-1}$ 一旦求出,换任何 $b$ 都能立刻得到解。
这在统计里很关键:$X^TX$ 的逆只有一个,但 $y$ 会变(换一批被试就要重解一次)。

8 三阶逆矩阵:伴随矩阵法

$2\times2$ 有口诀,$3\times3$ 就得用公式:

$$A^{-1} = \frac{1}{\det(A)}\operatorname{adj}(A)$$

其中 $\operatorname{adj}(A)$ 叫伴随矩阵:把每个元素换成它的代数余子式,再转置。

代数余子式 $A_{ij}$ 是划掉第 $i$ 行第 $j$ 列,剩下的行列式,再乘 $(-1)^{i+j}$。

8.1 完整例子

求 $B = \begin{pmatrix} 1 & 2 & 3 \\ 0 & 1 & 4 \\ 5 & 6 & 0 \end{pmatrix}$ 的逆。

第一步:算行列式(按第一行展开)

$$\det(B) = 1\cdot\begin{vmatrix}1&4\\6&0\end{vmatrix} - 2\cdot\begin{vmatrix}0&4\\5&0\end{vmatrix} + 3\cdot\begin{vmatrix}0&1\\5&6\end{vmatrix}$$

$$= 1(0-24) - 2(0-20) + 3(0-5) = -24 + 40 - 15 = 1$$

行列式正好是 1,这是个"设计过"的好例子,因为除以 1 最省事。

第二步:求伴随矩阵(9 个代数余子式再转置),结果:

$$\operatorname{adj}(B) = \begin{pmatrix} -24 & 18 & 5 \\ 20 & -15 & -4 \\ -5 & 4 & 1 \end{pmatrix}$$

第三步:除以行列式($\det = 1$,所以逆矩阵就是它本身)

$$B^{-1} = \begin{pmatrix} -24 & 18 & 5 \\ 20 & -15 & -4 \\ -5 & 4 & 1 \end{pmatrix}$$

验算:$B \cdot B^{-1} = I_3$ ✓(我用 sympy 实算过)

⚠️ 手算 3 阶逆矩阵非常累(9 个代数余子式,每个都是 2 阶行列式)。
实际计算用"高斯-约当消元",把 $[A \mid I]$ 用行变换化成 $[I \mid A^{-1}]$。

和阶段 8 克拉默法则的结论一致:伴随矩阵法的价值在理论(给出显式公式),不在计算。真要算就用消元法或直接 numpy.linalg.inv。

9 常见的坑

坑 说明
以为 $AB = BA$ 一般不相等。写"两边同乘 $A$"时必须说清是左乘还是右乘
$(AB)^T$ 写成 $A^TB^T$ 应该是 $B^TA^T$,顺序要颠倒
$(AB)^{-1}$ 写成 $A^{-1}B^{-1}$ 应该是 $B^{-1}A^{-1}$,同上
维度不匹配就硬乘 先查 $(m\times n)(n\times p)$,左边列数 = 右边行数
$\det=0$ 还去求逆 奇异矩阵无逆。先算行列式,为 0 就停下
非方阵求逆 只有方阵才谈得上逆矩阵
以为"两边同时约掉 $A$"总成立 只有 $A$ 可逆时才能约。这正是阶段 8 共线性的根源
逆矩阵公式漏除行列式 $\frac{1}{ad-bc}$ 是灵魂,漏了就全错

10 🧠 这跟认知科学有什么关系

这一节的对应关系是我用代码实算验证过的,但心理学结论请对照文献。

协方差矩阵的逆就是精度矩阵(precision matrix),是多元统计和脑连接分析的核心对象。

10.1 先看事实

我生成了 200 个"被试"的两个测量值(比如两项认知任务的反应时),让它们高度相关:

量 实算结果
$x_1$ 与 $x_2$ 的相关系数 $0.9516$
协方差矩阵 $\Sigma$ $\begin{pmatrix}0.8668 & 0.8497 \\ 0.8497 & 0.9198\end{pmatrix}$
$\det(\Sigma)$ $0.0753$
$\Sigma$ 的条件数 $40.4$
$\Sigma^{-1}$(精度矩阵) $\begin{pmatrix}12.21 & -11.28 \\ -11.28 & 11.51\end{pmatrix}$

10.2 精度矩阵为什么重要

① 它的对角线元素就是偏方差(partial variance)。

$\Sigma^{-1}$ 的第 $i$ 个对角元素是 $1/\text{Var}(x_i \mid \text{其他所有变量})$,也就是扣掉其他变量的影响之后,$x_i$ 还剩多少变异。

② 它的非对角元素直接对应偏相关。

偏相关公式是

$$r_{ij\cdot\text{其余}} = \frac{-(\Sigma^{-1})_{ij}}{\sqrt{(\Sigma^{-1})_{ii}(\Sigma^{-1})_{jj}}}$$

注意那个负号。上面算出来非对角是 $-11.28$(负的),代入负号后偏相关是正的。这个符号约定是初学者最容易搞反的地方。

③ 非对角元素为 0 ⇔ 条件独立。

这是高斯图模型(Gaussian Graphical Model)的地基:两个节点之间没有边,等价于"给定其他所有节点后"两者条件独立。

⭐ 这解释了一个反直觉的现象:两个变量边际相关很强(上面 $0.95$),但偏相关可能接近 0,只要中间有个"中介变量"把它们的关系解释掉了。
"相关"和"偏相关"回答的是不同的问题,这在认知科学的因果推断里是核心议题。

10.3 为什么"相关高"会毁掉回归

上面的条件数是 $40.4$,还不算灾难。但 $x_1$ 和 $x_2$ 的相关再往上推(比如 $0.999$),$\det(\Sigma) \to 0$,条件数 $\to\infty$,$\Sigma^{-1}$ 的数值彻底不可信。

这和阶段 8 的多重共线性是同一件事:回归系数 $\hat\beta = (X^TX)^{-1}X^Ty$,有个逆矩阵在中间。逆矩阵一旦数值不稳,系数就崩。

现象 矩阵语言 检查方法
自变量高度相关 $(X^TX)$ 接近奇异 算条件数
系数符号翻转、数值爆炸 逆矩阵数值不稳 换一批样本看系数稳不稳
两个变量"谁贡献多少"说不清 精度矩阵非对角占主导 看相关矩阵 + 偏相关

⚠️ 诚实说明:
- 上面的相关系数、行列式、条件数、精度矩阵都是我用 numpy 实算的(可复现),"协方差矩阵的逆是精度矩阵""非对角为 0 ⇔ 条件独立"是数学/统计事实。
- 但"认知任务之间相关高意味着什么""偏相关接近 0 该不该下因果结论"是研究问题,请以多元统计教材和方法论文献为准。我保证数学,不保证研究规范。


11 数学作业 S9

第 9 题参考代码:

import numpy as np

# --- 作业 3:2x2 求逆 ---
A = np.array([[2., 1.],
              [5., 3.]])
Ainv = np.linalg.inv(A)
print("A^-1 =\n", Ainv)
print("A @ A^-1 =\n", A @ Ainv)          # 应该接近单位阵

# 手算公式对照:1/(ad-bc) * [[d, -b], [-c, a]]
a, b, c, d = A[0, 0], A[0, 1], A[1, 0], A[1, 1]
det = a * d - b * c
manual = np.array([[d, -b], [-c, a]]) / det
print("手算公式 =\n", manual)
print("和 numpy 一致?", np.allclose(Ainv, manual))

# --- 作业 6:三种解法对照 ---
M = np.array([[3., -1.],
              [1.,  2.]])
rhs = np.array([7., 0.])

sol_inv = np.linalg.inv(M) @ rhs          # 逆矩阵法
sol_solve = np.linalg.solve(M, rhs)       # 专门解方程(推荐)

# 克拉默法则:逐列替换
D = np.linalg.det(M)
cramer = []
for i in range(2):
    Mi = M.copy()                         # ← 必须 copy,否则会改到 M
    Mi[:, i] = rhs
    cramer.append(np.linalg.det(Mi) / D)

print("逆矩阵法 :", sol_inv)
print("solve    :", sol_solve)
print("克拉默   :", cramer)

# 代入验根
assert np.allclose(M @ sol_solve, rhs), "验根失败"
print("验根通过")

💡 三种解法选哪个:
解一次方程组,用 np.linalg.solve(最快最稳)。
要反复用同一个 $A$ 解不同的 $b$,就先算一次 inv(A) 存起来。
克拉默法则只在考试和证明里用(阶段 8 讲过的原因:$O(n\cdot n!)$)。


第二部分 · 编程

12 元组:不能改的列表

元组(tuple)和列表长得像,但用圆括号,而且创建之后不能修改。

point = (3, 5)              # 元组
lst = [3, 5]                # 列表

print(point[0])             # 3        ← 取值和列表一样
print(point[1])             # 5

lst[0] = 10                 # ✓ 列表可以改
print(lst)                  # [10, 5]

point[0] = 10               # ✗ TypeError: 'tuple' object does not support item assignment

为什么要有"不能改"的东西?三个理由:

理由 说明
安全 传给别人时不怕被改坏
能当字典的键 列表不行,元组行(阶段 8 讲过"可哈希")
语义清楚 "坐标""RGB 颜色"这种天生不该变的东西,用元组表达意图
# 元组能当字典的键,列表不能
d = {(1, 2): "左上角", (3, 4): "右下角"}
print(d[(1, 2)])            # 左上角

⚠️ 单个元素的元组要加逗号:(5) 是整数 5,(5,) 才是元组。
这个坑非常隐蔽,建议永远写上逗号。

print(type((5)))            # <class 'int'>    ← 不是元组!
print(type((5,)))           # <class 'tuple'>  ← 这才是

12.1 元组解包(最常用的功能)

point = (3, 5)
x, y = point                # 一次拆成两个变量
print(x, y)                 # 3 5

# 交换两个变量(不用临时变量!)
a, b = 1, 2
a, b = b, a
print(a, b)                 # 2 1

函数返回多个值时,返回的就是元组:

def min_max(nums):
    return min(nums), max(nums)     # 返回一个元组

lo, hi = min_max([3, 1, 4, 1, 5])
print(lo, hi)                       # 1 5

💡 a, b = b, a 为什么成立:右边 b, a 先被打包成一个元组 $(2,1)$,然后左边解包赋给 $a,b$。中间不经过任何临时变量,因为元组是不可变的,打包那一刻值就固定了。

12.2 元组 vs 列表:一张表

列表 [] 元组 ()
能改吗 能 不能
能当字典键吗 不能 能
方法多吗 多(append/sort/…) 少(只有 count/index)
什么时候用 会变的一串东西 不会变的一组值

⚠️ 元组"不可变"是浅层的。如果元组里装了个列表,那个列表还是能改:

t = (1, [2, 3])
t[1].append(4)              # ✓ 可以!改的是里面的列表
print(t)                    # (1, [2, 3, 4])
t[1] = [9]                  # ✗ 不行,不能给元组换元素

这和阶段 7 的"浅拷贝"是同一个坑:不可变只保证"不能换掉盒子里的东西",不保证"盒子里的东西自己不变"。

13 字符串深入(一):切片

字符串是有顺序的字符序列,所以能切片。

切片三件套是 s[开始:结束:步长],含开始、不含结束。

s = "cognition"
#    0123456789     ← 索引(从 0 开始)

print(s[0])         # c        第 0 个
print(s[-1])        # n        倒数第 1 个
print(s[0:4])       # cogn     位置 0,1,2,3(不含 4!)
print(s[:4])        # cogn     省略开始 = 从头
print(s[4:])        # ition    省略结束 = 到末尾
print(s[:])         # cognition  全部(等于 s.copy,但字符串本来不可变)
print(s[::2])       # cgiin    每隔一个取一个
print(s[::-1])      # noitingoc  倒序!

⚠️ "含头不含尾"是 Python 的铁律,切片、range() 都这样。
理由是:这样 s[i:i+3] 就正好取 3 个字符,不用写成 s[i:i+3-1]。

切片不会越界报错(这点和索引取值不同):

s = "abc"
print(s[0:100])     # abc      ✓ 超出的部分自动截断,不报错
print(s[100])       # ✗ IndexError: string index out of range

13.1 字符串是不可变的

"不可变"的意思是:所有"修改"字符串的操作,都是返回一个新字符串。

s = "hello"
s.upper()               # 返回 "HELLO",但没改变 s
print(s)                # hello    ← s 还是原样!

s = s.upper()           # 要"改"就得重新赋值
print(s)                # HELLO

⚠️ 这是初学者最常犯的错之一:
写 s.upper() 然后奇怪为什么 s 没变。必须写成 s = s.upper()。
这和阶段 7 讲的"可变对象"是同一件事的两面:字符串不可变,所以方法都返回新值。

14 字符串深入(二):常用方法

我想干什么 写法 结果
全部大写 / 小写 s.upper() / s.lower() 返回新字符串
去掉两端空白 s.strip() " hi " → "hi"
去掉两端指定字符 s.strip(".,") 去掉两端的 . 和 ,
替换 s.replace("a", "b") 全部替换
按分隔符切开 s.split(",") 返回列表
按空白切开 s.split() 无参数 = 按任意空白切
拼接 ",".join(lst) 列表 → 字符串
找子串位置 s.find("it") 返回下标,找不到返回 -1
数出现次数 s.count("a") 返回次数
判断开头 / 结尾 s.startswith("cog") / s.endswith("tion") True/False
判断全是数字 s.isdigit() "123" → True

最常用的两个是 split 和 join(它们互为逆操作)。

line = "张三,18,认知科学"
parts = line.split(",")         # 字符串 → 列表
print(parts)                    # ['张三', '18', '认知科学']

back = ",".join(parts)          # 列表 → 字符串
print(back)                     # 张三,18,认知科学

💡 join 的顺序容易记反:是 "分隔符".join(列表),不是 列表.join("分隔符")。
记法是:你先写好粘合剂,再把东西粘上去。

split() 不带参数时会自动处理连续空格(读数据文件时非常有用):

print("  a   b  c ".split())     # ['a', 'b', 'c']    ← 多个空格当一个
print("  a   b  c ".split(" "))  # ['', '', 'a', '', '', 'b', '', 'c', '']   ← 灾难

15 字符串深入(三):f-string 格式化

f-string 是拼字符串的首选方式(Python 3.6+)。

name = "张三"
age = 18
print(f"{name} 今年 {age} 岁")            # 张三 今年 18 岁

格式控制在科研里极其常用,对齐表格、控制小数位都要靠它:

x = 3.14159265

print(f"{x:.2f}")           # 3.14        保留 2 位小数
print(f"{x:.4f}")           # 3.1416      保留 4 位小数
print(f"{x:10.2f}")         #       3.14  总宽 10,右对齐
print(f"{x:<10.2f}|")       # 3.14      | 左对齐
print(f"{x:>10.2f}|")       #       3.14| 右对齐
print(f"{x:^10.2f}|")       #    3.14   | 居中
print(f"{1234567:,}")       # 1,234,567   千分位分隔
print(f"{0.856:.1%}")       # 85.6%       百分比(自动 ×100)

对齐加小数位是打印数据表的标配:

print(f"{'被试':<6}{'反应时':>10}{'正确率':>10}")
print("-" * 26)
for name, rt, acc in [("A01", 0.5234, 0.95), ("A02", 1.2345, 0.80)]:
    print(f"{name:<6}{rt:>10.2f}{acc:>10.1%}")

输出:

被试          反应时      正确率
--------------------------
A01         0.52      95.0%
A02         1.23      80.0%

💡 :>10 表示"占 10 格、右对齐",:10 默认右对齐。< 左对齐,^ 居中。
中文字符占 2 格宽,所以含中文的表头对齐会有点偏,这是终端字体的问题,不是你写错了。


16 🔧 技能卡 9:字符串怎么切、怎么格式化

技能卡 6 教你读别人的函数签名;这张卡教你处理"文本数据",科研里 90% 的原始数据都是文本。

16.1 决策一:我要"一段字符串"里的哪部分?

我想要 用什么 例子
第 $i$ 个字符 s[i] "abc"[1] → 'b'
从 $a$ 到 $b$(不含 $b$) s[a:b] "abcde"[1:3] → 'bc'
去掉前 3 个 s[3:] 省略结束
去掉后 3 个 s[:-3] 负索引
倒过来 s[::-1] 步长 $-1$
每隔一个取 s[::2] 步长 2

16.2 决策二:我要"拆开成好几块"还是"拼起来"?

需求 用什么 记法
字符串 → 列表 s.split("分隔符") 切开
列表 → 字符串 "分隔符".join(lst) 粘上(分隔符写前面!)

16.3 决策三:我要把变量"塞进"字符串

需求 写法
最常用 f"{变量}"
保留 2 位小数 f"{x:.2f}"
占 10 格右对齐 f"{x:>10}"
百分数 f"{x:.1%}"

16.4 三条新手铁律

① "含头不含尾",切片、range 都这样。s[0:3] 取的是 0、1、2。

② 字符串不可变,所有"修改"方法都返回新值,必须重新赋值:

s = "hello"
s.upper()           # ✗ 白写了,s 没变
s = s.upper()       # ✓ 正确

③ 先想清楚"数据长什么样"再动手。处理文本数据前,先 print 一行看看:

line = " 张三 , 18 , 认知科学 "
print(repr(line))                    # 先看清有没有多余空格
parts = [p.strip() for p in line.split(",")]
print(parts)                         # ['张三', '18', '认知科学']

⚠️ repr() 是你的好朋友:它会把字符串的真实内容(包括空格、换行符)显示出来。
直接 print 看不出 " 18 " 里有没有空格,repr 一眼就能看到。

💡 处理实验数据的第一原则是不要猜,先看。
print(repr(第一行))、print(type(值))、print(len(列表)),这三个动作能省掉你 80% 的调试时间。


17 🧠 这跟认知科学有什么关系(编程侧)

概念性内容,无法用代码验证,请对照文献。

字符串处理对应实验数据的"清洗"环节,而清洗占据你未来 70% 的工作时间。

认知科学实验的原始数据几乎都是文本:

实验平台 产出格式
E-Prime .txt / .csv 制表符分隔
PsychoPy .csv 逗号分隔
jsPsych .csv / .json
问卷(Qualtrics 等) .csv(常含大量空行和引号)
眼动 / EEG 自定义文本格式

这些数据几乎从不是"干净"的:多余空格、编码问题、缺失值标记成 "NA" 或 ""、表头有中文、时间戳格式不统一。

上面学的三个工具正好对应清洗的三个步骤:

清洗步骤 用的工具 典型问题
按行/按列切开 split() 分隔符是逗号还是制表符?
去掉多余空白 strip() " 18 " 和 "18" 不是一回事
转换类型 int() / float() 读进来全是字符串,得转成数字

一个真实的教训:" 18 " 转 int 会报错(ValueError),必须 strip 之后才行。这在批量处理数据时会卡在第一个文件,而且报错信息指向的行常常不是你真正出错的地方。

💡 "清洗"为什么占 70% 的时间:因为分析的方法论是有限的,而数据的脏法是无限的。
这不是你的能力问题,是这个领域的固有属性。

元组对应心理学里的"结构化记录"。

"一个被试的一次试次"是固定的一组字段:(被试 ID, 试次号, 反应时, 正确与否)。这个组合天生不该被改动(你改掉"第 2 个字段是试次号"这件事,整个数据集就乱了)。用元组表达"这是一条不可变的记录",比用列表更能表达你的意图。

⚠️ 诚实说明:
- 上面的数据格式和清洗步骤是领域常识,但各平台的具体细节(字段名、编码、缺失值约定)请以你用的软件的官方文档为准。
- "清洗占 70% 时间"是行业内流传的经验数字,不是精确统计。
- 这一节是让你提前知道"要花时间在哪",不是让你把它当结论。


18 编程作业 S9

第 6 题参考写法:

def parse_record(line):
    parts = line.split(",")                  # 切成 3 段
    subject = parts[0].strip()               # 去掉空格
    rt = float(parts[1].strip())             # 字符串 → 小数
    correct = bool(int(parts[2].strip()))    # "1"/"0" → True/False
    return (subject, rt, correct)

print(parse_record("A01, 0.523, 1"))
# ('A01', 0.523, True)

💡 为什么先 int 再 bool:bool("0") 是 True(非空字符串都是 True!),必须先转成整数 0,再转 False。这是个非常容易错的坑。

第 10 题参考写法:

def clean_data(lines):
    result = []
    for line in lines:
        line = line.strip()
        if not line:                     # 跳过空行
            continue
        result.append(parse_record(line))
    return result

raw_lines = ["A01, 0.523, 1", "", "  ", "A02, 1.234, 0"]
print(clean_data(raw_lines))
# [('A01', 0.523, True), ('A02', 1.234, False)]

# 检查用例
assert clean_data([]) == []
assert len(clean_data(raw_lines)) == 2        # 空行被跳过了
assert clean_data(raw_lines)[0][0] == "A01"
print("检查用例通过")

第 7 题参考写法:

data = [("A01", 0.523, 0.95), ("A02", 1.234, 0.80), ("A03", 0.789, 1.00)]

print(f"{'被试':<8}{'反应时':>10}{'正确率':>10}")
print("-" * 28)
for name, rt, acc in data:
    print(f"{name:<8}{rt:>10.2f}{acc:>10.1%}")

输出:

被试          反应时      正确率
----------------------------
A01         0.52      95.0%
A02         1.23      80.0%
A03         0.79     100.0%

第 8 题答案:

s = "cognition"
s.upper()               # ⚠️ 返回了新字符串,但没人接住它
print(s)                # cognition   ← s 根本没变

s = s.upper()           # ✓ 必须重新赋值
print(s)                # COGNITION

字符串是不可变的,upper() 返回一个新字符串,原来的 s 一点没动。要"改"就必须赋值回去。
这和阶段 7 的可变性是同一件事的另一面。


本阶段自评

  1. 数学:$(m\times n)$ 和 $(n\times p)$ 相乘,结果是什么形状?为什么左边列数必须等于右边行数?
  2. 数学:$AB$ 和 $BA$ 一般相等吗?举一个本讲义里的反例。
  3. 数学:$(AB)^T$ 等于什么?$(AB)^{-1}$ 等于什么?这两个的"顺序"规律一样吗?
  4. 数学:$2\times2$ 逆矩阵的口诀是什么?写出公式。
  5. 数学:什么条件下矩阵不可逆?此时方程组会发生什么?
  6. 数学:为什么"两边约掉 $A$"在矩阵里不一定合法?这和多重共线性什么关系?
  7. 数学:为什么伴随矩阵法算 3 阶逆矩阵"理论上漂亮、实践上不实用"?
  8. 编程:元组和列表的三个区别是什么?为什么元组能当字典的键?
  9. 编程:s[0:3] 取几个字符?为什么 Python 用"含头不含尾"?
  10. 编程:s.upper() 之后为什么 s 没变?正确写法是什么?
  11. 技能:split 和 join 的方向怎么记?repr() 在调试时有什么用?
  12. 连接:为什么"两个变量相关 0.95,偏相关却可能接近 0"?

全部答得上 → 进阶段 10。
有任何一个答不上 → 回去补对应小节,别跳。

最后更新:2026-09-26