阶段 14 讲义
数学 11h:高数期末复习
编程 9h:综合小项目
第一部分 · 数学
这一阶段不学新东西,只做两件事:把 13 个阶段连成一张网,然后用一套模拟卷把它压实。
复习不是"再看一遍"。这一阶段的认知科学连接会告诉你为什么。
1 全景知识地图
先把骨架立起来。这 13 个阶段的知识点最后归成四条线:
┌─────────────────────────────────┐
│ 极限(阶段 1–2) │
│ 一切的基础:趋于、ε-N、ε-δ │
└───────────────┬─────────────────┘
│
┌───────────────▼─────────────────┐
│ 导数(阶段 3–7) │
│ 变化率 · 链式法则 · 中值定理 │
│ 凹凸性 · 函数作图 │
└───────────────┬─────────────────┘
│
┌───────────────────────────┴───────────────────────────┐
│ │
┌───────▼──────────────────┐ ┌───────────────▼─────┐
│ 积分(阶段 11–12) │ │ 线性代数 │
│ ——导数的逆运算 │ │ (阶段 8–10,13) │
│ 换元 · 分部 · 定积分应用 │ │ 行列式→矩阵→方程组 │
└──────────────────────────┘ │ →向量空间→特征值 │
└─────────────────────┘
两条主线的关系:
| 主线 | 研究对象 | 核心工具 |
|---|---|---|
| 分析(微积分) | 连续变化的量 | 极限 → 导数 → 积分 |
| 代数(线性代数) | 线性结构 | 行列式 → 矩阵 → 特征值 |
它们在大一上是分开学的,但阶段 16 会看到它们如何汇合(SVD 是"矩阵的微积分式分解")。
1.1 逐阶段速查
| 阶段 | 核心内容 | 一句话记住 |
|---|---|---|
| 01 | 数列极限 ε-N | "要多近有多近,从某项之后一直在里面" |
| 02 | 函数极限 ε-δ | 同上,把"某项之后"换成"某点附近" |
| 03 | 导数定义 | $f'(x) = \lim_{\Delta x\to0}\frac{\Delta y}{\Delta x}$ |
| 04 | 求导法则 | 乘积、商、链式法则 |
| 05 | 中值定理 | 洛必达、罗尔、拉格朗日 |
| 06 | 导数的应用 | 单调性、极值 |
| 07 | 凹凸性 | $f''$ 决定弯曲方向 |
| 08 | 行列式 | 解方程组的钥匙 |
| 09 | 矩阵与逆 | 矩阵里的"除法" |
| 10 | 向量组与方程组 | 秩决定解的情况 |
| 11 | 不定积分 | 求导的逆运算,带 $+C$ |
| 12 | 定积分 | 一个数,牛顿-莱布尼茨 |
| 13 | 特征值 | 变换中"方向不变"的那些方向 |
2 核心公式总表
这张表建议打印出来,考前反复看。
2.1 极限与等价无穷小($x \to 0$)
| 公式 | 值 |
|---|---|
| $\lim_{x\to0}\dfrac{\sin x}{x}$ | $1$ |
| $\lim_{x\to0}\dfrac{\tan x}{x}$ | $1$ |
| $\lim_{x\to0}\dfrac{e^x-1}{x}$ | $1$ |
| $\lim_{x\to0}\dfrac{\ln(1+x)}{x}$ | $1$ |
| $\lim_{x\to0}\dfrac{1-\cos x}{x^2}$ | $\dfrac12$ |
| $\lim_{x\to\infty}\left(1+\dfrac1x\right)^x$ | $e$ |
2.2 导数
| 公式 | 结果 |
|---|---|
| $(x^n)'$ | $nx^{n-1}$ |
| $(\sin x)'$ | $\cos x$ |
| $(\cos x)'$ | $-\sin x$ |
| $(e^x)'$ | $e^x$ |
| $(\ln x)'$ | $\dfrac1x$ |
| $(\arctan x)'$ | $\dfrac{1}{1+x^2}$ |
| 链式法则 | $[f(g(x))]' = f'(g(x))\cdot g'(x)$ |
| 乘积法则 | $(uv)' = u'v + uv'$ |
| 商法则 | $\left(\dfrac uv\right)' = \dfrac{u'v-uv'}{v^2}$ |
2.3 积分
| 公式 | 结果 |
|---|---|
| $\int x^n dx$ | $\dfrac{x^{n+1}}{n+1}+C$($n\ne-1$) |
| $\int \dfrac1x dx$ | $\ln\lvert x\rvert+C$ |
| $\int e^x dx$ | $e^x+C$ |
| $\int \sin x\,dx$ | $-\cos x+C$ |
| $\int \cos x\,dx$ | $\sin x+C$ |
| $\int \dfrac{1}{1+x^2}dx$ | $\arctan x+C$ |
| 分部积分 | $\int u\,dv = uv-\int v\,du$ |
| 牛顿-莱布尼茨 | $\int_a^b f = F(b)-F(a)$ |
2.4 线性代数
| 公式 | 结果 |
|---|---|
| 二阶行列式 | $\begin{vmatrix}a&b\\c&d\end{vmatrix} = ad-bc$ |
| $2\times2$ 逆矩阵 | $\dfrac{1}{ad-bc}\begin{pmatrix}d&-b\\-c&a\end{pmatrix}$ |
| 逆矩阵的性质 | $(AB)^{-1}=B^{-1}A^{-1}$,$(A^T)^{-1}=(A^{-1})^T$ |
| 转置 | $(AB)^T = B^TA^T$ |
| 解的情况 | $r_A\ne r_b$ 无解;$r_A=r_b=n$ 唯一解;$r_A=r_b<n$ 无穷多解 |
| 齐次有非零解 | $\iff \text{rank}(A)<n$ |
| 特征方程 | $\det(A-\lambda I)=0$ |
| 特征值性质 | $\sum\lambda=\text{tr}(A)$,$\prod\lambda=\det(A)$ |
3 高频题型与解法套路
考试考的题型是有限的。下面六类覆盖了绝大部分:
3.1 套路一:求极限
顺序:先试直接代入 → 不行就看能不能用等价无穷小 → 再不行洛必达
$$\lim_{x\to0}\frac{\sin 3x}{x} = \lim_{x\to0}\frac{3x}{x} = 3 \quad (\text{实算确认})$$
⚠️ "$0/0$" 或 "$\infty/\infty$" 才能用洛必达。别上来就求导。
3.2 套路二:求导(重点是链式法则)
判断层次:从外往里剥。
$$y = \sin(x^2) \Longrightarrow y' = \cos(x^2)\cdot 2x = 2x\cos(x^2) \quad (\text{实算确认})$$
$$y = x^2e^x \Longrightarrow y' = 2xe^x + x^2e^x = x(x+2)e^x \quad (\text{实算确认})$$
3.3 套路三:不定积分(选方法)
| 看到 | 用什么 |
|---|---|
| 复合函数,内层导数的倍数在场 | 凑微分 |
| 两种类型的乘积($xe^x$、$x\cos x$) | 分部积分 |
| $\ln x$、$\arctan x$ 单独出现 | 分部积分(补 $dx$) |
3.4 套路四:定积分
先求原函数,再代端点,上减下。
$$\int_0^1(x^2+1)dx = \left[\frac{x^3}{3}+x\right]_0^1 = \frac13+1 = \frac43 \quad (\text{实算确认})$$
3.5 套路五:行列式与逆矩阵
三阶行列式:萨吕斯法则(6 项,别漏)。
求逆:$2\times2$ 用口诀"主对角互换、副对角变号、除以行列式"。
$$\begin{pmatrix}2&1\\5&3\end{pmatrix}^{-1} = \frac{1}{1}\begin{pmatrix}3&-1\\-5&2\end{pmatrix} = \begin{pmatrix}3&-1\\-5&2\end{pmatrix} \quad (\text{实算确认})$$
3.6 套路六:特征值
$$\det(A-\lambda I)=0 \Longrightarrow \text{解 }\lambda \Longrightarrow \text{回代求特征向量}$$
$$A = \begin{pmatrix}3&1\\0&2\end{pmatrix} \Longrightarrow \lambda = 3,\ 2 \quad (\text{三角矩阵,直接看对角线})$$
4 高频错误清单(考前必读)
这张表是按"实际出现频率"排的,不是按重要性。
| # | 错误 | 正确做法 |
|---|---|---|
| 1 | $\int \sin x\,dx$ 漏负号 | 是 $-\cos x+C$ |
| 2 | 不定积分忘写 $+C$ | 每次都写 |
| 3 | $(AB)^{-1}$ 写成 $A^{-1}B^{-1}$ | 是 $B^{-1}A^{-1}$,顺序反 |
| 4 | $(AB)^T$ 写成 $A^TB^T$ | 是 $B^TA^T$,顺序反 |
| 5 | 换元后忘了换积分限 | 换了 $u$ 就换上下限 |
| 6 | $[-\cos x]_0^\pi$ 算成 0 | $-\cos\pi = +1$,答案是 2 |
| 7 | 求 $\det(A-\lambda I)$ 时忘了 $\lambda I$ | 只减主对角线 |
| 8 | 特征向量漏写(只写特征值) | 两者必须配对 |
| 9 | 克拉默法则忘记 $D\ne0$ 的前提 | 先算 $D$ |
| 10 | 只算 $\text{rank}(A)$ 不算 $\text{rank}([A\mid b])$ | 两个都要算 |
| 11 | 洛必达用在非不定式上 | 先确认是 $0/0$ 或 $\infty/\infty$ |
| 12 | 求面积时判错"谁在上面" | 先比较或画图 |
⭐ 这 12 条里,第 3、4 条最隐蔽,因为它们在"看起来对"的时候错。
记忆法:逆和转置都会"颠倒顺序",就像"穿脱袜子鞋子"。
5 模拟卷(限时 90 分钟)
请先自己做完,再看下一节的答案。
- [ ] 1. 求 $\lim\limits_{x\to0}\dfrac{\sin 3x}{x}$
- [ ] 2. 求 $\lim\limits_{x\to0}\dfrac{e^x-1}{x}$
- [ ] 3. 求 $y = x^2e^x$ 的导数
- [ ] 4. 求 $y = \sin(x^2)$ 的导数
- [ ] 5. 求 $y = \ln(1+x^2)$ 的导数
- [ ] 6. 求 $\int x\cos x\,dx$
- [ ] 7. 求 $\int_0^1(x^2+1)dx$
- [ ] 8. 求 $\int_1^e \ln x\,dx$
- [ ] 9. 求 $\begin{vmatrix}2&1\\1&3\end{vmatrix}$
- [ ] 10. 求 $\begin{pmatrix}2&1\\5&3\end{pmatrix}^{-1}$
- [ ] 11. 求 $\begin{pmatrix}3&1\\0&2\end{pmatrix}$ 的特征值
- [ ] 12. 求 $\begin{pmatrix}2&1\\1&3\end{pmatrix}$ 的特征多项式
- [ ] 13. 求 $\begin{pmatrix}1&2&3\\2&4&6\\1&1&1\end{pmatrix}$ 的秩
- [ ] 14. 求 $y=x$ 与 $y=x^2$ 在 $[0,1]$ 之间围成的面积
- [ ] 15. 判断 $\int_1^\infty \dfrac{1}{x^2}dx$ 是否收敛,收敛则求值
- [ ] 16. 求 $f(x)=x^2$ 在 $[0,3]$ 上的平均值
- [ ] 17. 求 $\dfrac{d}{dx}\displaystyle\int_0^x e^{-t^2}dt$
- [ ] 18. 用 $A=\begin{pmatrix}1&2\\3&4\end{pmatrix}$、$B=\begin{pmatrix}0&1\\1&0\end{pmatrix}$ 验证 $(AB)^{-1}=B^{-1}A^{-1}$
6 模拟卷答案(全部用 sympy 实算核对)
| 题 | 答案 |
|---|---|
| 1 | $3$ |
| 2 | $1$ |
| 3 | $x(x+2)e^x$ |
| 4 | $2x\cos(x^2)$ |
| 5 | $\dfrac{2x}{x^2+1}$ |
| 6 | $x\sin x+\cos x+C$ |
| 7 | $\dfrac43$ |
| 8 | $1$ |
| 9 | $5$ |
| 10 | $\begin{pmatrix}3&-1\\-5&2\end{pmatrix}$ |
| 11 | $\lambda = 3,\ 2$ |
| 12 | $\lambda^2-5\lambda+5$ |
| 13 | $2$ |
| 14 | $\dfrac16$ |
| 15 | 收敛,值为 $1$ |
| 16 | $3$ |
| 17 | $e^{-x^2}$ |
| 18 | 两边都是 $\begin{pmatrix}\frac32&-\frac12\\-2&1\end{pmatrix}$ ✓ |
第 13 题详解(容易只答一半):
$$A = \begin{pmatrix}1&2&3\\2&4&6\\1&1&1\end{pmatrix}$$
第 2 行 = 第 1 行 × 2(冗余)。行变换后非零行有 2 行 → $\text{rank} = 2$。
第 17 题详解(考的是微积分基本定理,不需要求出原函数):
$$\frac{d}{dx}\int_0^x e^{-t^2}dt = e^{-x^2}$$
注意:$e^{-t^2}$ 没有初等原函数,但这条定理照样成立。
这正是它叫"基本定理"的原因。
第 8 题详解(分部积分 + 定积分):
$$\int_1^e \ln x\,dx = [x\ln x - x]_1^e = (e\cdot1 - e) - (1\cdot0 - 1) = 0-(-1) = 1$$
7 🧠 这跟认知科学有什么关系
这一节模型是我自己用代码搭的(可复现),但它是玩具模型,不是实证数据。
认知科学的结论请以文献为准。
"复习"这件事本身就是认知科学的研究对象,而研究结论说:大多数人的复习方式是错的。
7.1 两个最稳健的发现
| 现象 | 内容 | 经典文献 |
|---|---|---|
| 间隔效应(spacing effect) | 分散复习比集中复习记得久得多 | Ebbinghaus (1885);Cepeda et al. (2006) 元分析 |
| 提取练习(testing effect / retrieval practice) | 主动回忆比反复阅读有效得多 | Roediger & Karpicke (2006) |
"间隔效应"是心理学史上最早被发现的记忆规律之一(Ebbinghaus 1885 年用自己当被试做的)。
7.2 我用代码模拟给你看(注意:玩具模型)
模型设定:记忆强度 $S$ 每天按 $S \to S\cdot e^{-1/\tau}$ 衰减($\tau = 5$ 天);
每次复习把 $S$ 提升一定比例,且有收益递减(越接近 1 提升越少)。
实跑结果(30 天):
| 复习方式 | 第 7 天 | 第 14 天 | 第 30 天 |
|---|---|---|---|
| 不复习 | $0.247$ | $0.061$ | $0.002$ |
| 集中复习(连续 4 天) | $0.449$ | $0.111$ | $0.005$ |
| 间隔复习(每周 1 次) | $1.000$ | $1.000$ | $\mathbf{0.165}$ |
第 30 天时,间隔复习的记忆强度是集中复习的约 37 倍。
再把"提取练习"的巩固效果调强一点(复习同样在 7/14/21 天):
| 方式 | 第 30 天 |
|---|---|
| 重复阅读 | $0.094$ |
| 提取练习 | $\mathbf{0.139}$(高约 47%) |
7.3 ⚠️ 这个模拟能说明什么,不能说明什么
必须说清楚:
| 能说明 | 不能说明 |
|---|---|
| 在"指数衰减 + 收益递减"这个假设下,间隔复习在数值上确实远优于集中复习 | ❌ 真实记忆不是简单的指数衰减 |
| 这个模型可以复现出文献里报告的方向(间隔 > 集中,提取 > 重读) | ❌ 上面那些具体倍数(37 倍、47%)是模型算出来的,不是实验结果 |
| 提供了一个"为什么间隔有用"的直觉 | ❌ 不能用来估计你该隔几天复习 |
真实的记忆曲线有平台期、有干扰、有睡眠依赖的巩固、有情绪调制。
玩具模型把这一切都简化掉了。
⭐ 所以正确的用法是:
这个模拟给你直觉,文献给你结论,你的成绩来自行动。
7.4 对你现在(阶段 14)的具体建议
| 做法 | 为什么 | 怎么做 |
|---|---|---|
| 别只看,要"考自己" | 提取练习 > 重复阅读 | 用本讲义每阶段的「自评」先说答案再看书 |
| 分散,别突击 | 间隔效应 | 每天 1 小时 × 6 天 > 周末突击 6 小时 |
| 主动做作业 | 作业就是提取练习 | 本讲义反复强调"别跳作业"不是为难你 |
| 错题要重做 | 提取失败的地方正是要再提取 | 第 4 节的 12 条错误清单 |
| 睡前回顾 | 睡眠参与记忆巩固 | 睡前 5 分钟过一遍当天要点 |
⭐ 这解释了一个常见的困惑:
"我明明看懂了,为什么考不出来?"
因为"看懂"是识别(recognition),"会做"是提取(recall)。这是两种不同的能力。
看懂答案时的流畅感,会让你误以为已经掌握了。这叫"流畅性错觉"。
唯一可靠的检测方式是:合上书,自己写一遍。⚠️ 诚实说明:
- "间隔效应"和"提取练习"是认知心理学中最稳健、可重复性最好的发现之一,
有大量实验和元分析支持。可以放心采用。
- 但"最佳复习间隔是多久"依赖于你想记多久和材料难度,
研究结论是"间隔应该与目标保持时间成比例",而不是一个固定数字。
- 上面所有具体数值都来自我的玩具模型,不是实证结果。
请以 Cepeda et al. (2006, Psychological Bulletin)、
Roediger & Karpicke (2006, Psychological Science) 等文献为准。
8 数学作业 S14
- [ ] 1. 不看讲义,把第 1 节的"知识地图"自己画一遍(这是提取练习,别偷看)
- [ ] 2. 合上书,默写第 2 节里的积分表(8 条),然后对照
- [ ] 3. 完成第 5 节的模拟卷,限时 90 分钟,中间不查资料
- [ ] 4. 对照答案给自己打分,把错题的编号记下来
- [ ] 5. 对每道错题,写出"错在哪一类"(对照第 4 节的 12 条)
- [ ] 6. 一周后再做一遍错题(间隔效应)
- [ ] 7.(Python)写一个脚本,随机从第 2 节公式表里抽 5 条考自己
第 7 题参考代码:
"""公式自测器:从题库随机抽题,先自己想,再按回车看答案。"""
import random
# 题库:(题目, 答案)
QUESTIONS = [
("∫ sin x dx = ?", "-cos x + C"),
("∫ 1/x dx = ?", "ln|x| + C"),
("∫ e^x dx = ?", "e^x + C"),
("∫ 1/(1+x²) dx = ?", "arctan x + C"),
("(AB)⁻¹ = ?", "B⁻¹A⁻¹"),
("(AB)ᵀ = ?", "BᵀAᵀ"),
("det(A-λI) = ? 求什么", "求特征值的方程"),
("Σλ 等于什么?", "tr(A)(迹)"),
("∫_a^b f dx = ?", "F(b) - F(a)(牛顿-莱布尼茨)"),
("分部积分公式", "∫u dv = uv - ∫v du"),
("lim_{x→0} sin x / x = ?", "1"),
("lim_{x→0} (e^x-1)/x = ?", "1"),
("∫ x^n dx = ? (n≠-1)", "x^(n+1)/(n+1) + C"),
("2×2 逆矩阵口诀", "主对角互换,副对角变号,除以行列式"),
]
def quiz(n=5):
"""抽 n 道题自测。"""
picked = random.sample(QUESTIONS, min(n, len(QUESTIONS)))
right = 0
for i, (q, a) in enumerate(picked, 1):
print(f"\n第 {i} 题:{q}")
input(" 想好了按回车看答案...")
print(f" 答案:{a}")
ok = input(" 答对了吗?(y/n) ").strip().lower().startswith("y")
right += ok
print(f"\n得分:{right}/{len(picked)}")
return right, len(picked)
if __name__ == "__main__":
# 固定随机种子可选,保证可复现
# random.seed(42)
quiz(5)
💡 这就是"提取练习"的代码实现:先自己回忆,再看答案。
把题库换成你自己的薄弱点,它就是你最好的复习工具。
建议每天抽 5 题,比考前突击 2 小时有效得多。
第二部分 · 编程
9 项目说明
阶段 1–13 你学的都是零件。这一阶段把零件装成一台能跑的机器。
项目:反应时数据分析流程
- 输入:一个原始数据文件(含脏数据:坏行、异常反应时)
- 处理:清洗 → 按条件/被试分组统计
- 输出:一个对齐的文本报告
它已经做好并放在你的电脑上了:
编程/项目-反应时分析/
├── README.md 项目说明
├── .gitignore 哪些文件不该进版本库
├── main.py 主流程
├── io_utils.py 数据进出
├── stats_utils.py 统计工具
├── report.py 呈现
├── 自检.py 测试
└── data/
├── raw.csv 原始数据(只读)
└── clean.csv 清洗后的数据(可重新生成)
先跑一遍看看:
cd ~/Desktop/大一/编程/项目-反应时分析
~/miniconda3/bin/python main.py
10 项目结构:每个文件管一摊
对照技能卡 11(拆分代码),这个项目就是它的示范。
| 文件 | 职责 | 不负责什么 |
|---|---|---|
main.py |
编排流程:读 → 清洗 → 分析 → 出报告 | ❌ 不写具体解析逻辑 |
io_utils.py |
数据进出:解析行、读文件、写文件 | ❌ 不做统计 |
stats_utils.py |
统计计算:均值、中位数、标准差、分组 | ❌ 不碰文件 |
report.py |
呈现:把数字排成表格 | ❌ 不计算,只排版 |
自检.py |
测试:验证每个模块 | — |
依赖方向是单向的(技能卡 11 讲过):
main.py ──┬──> io_utils.py
├──> stats_utils.py
└──> report.py ──> stats_utils.py
没有循环导入,这是能跑起来的前提。
11 逐模块讲解
11.1 io_utils.py:数据进来
核心函数 parse_line:
def parse_line(line, lineno=0):
"""把一行 CSV 解析成字典。"""
parts = [p.strip() for p in line.split(",")]
if len(parts) != len(COLUMNS):
raise ValueError(f"字段数应为 {len(COLUMNS)},实际 {len(parts)}")
subject, trial, condition, rt_s, correct_s = parts
try:
rt = float(rt_s)
correct = bool(int(correct_s))
except ValueError as e:
raise ValueError(f"数值转换失败:{e}") from e
return {"subject": subject, "trial": int(trial),
"condition": condition, "rt": rt, "correct": correct}
这一段用到了你在阶段 9–11 学的:
| 用到的 | 来自哪个阶段 |
|---|---|
split(",") 切字符串 |
阶段 9 |
列表推导式 [p.strip() for p in ...] |
阶段 11 |
元组解包 a, b, c, d, e = parts |
阶段 9 |
| 返回字典(用名字取值) | 阶段 8 |
raise ValueError + from e 保留原因 |
阶段 10 |
| f-string 格式化报错信息 | 阶段 9 |
⚠️ 注意
raise ... from e:它保留原始异常作为"原因",
报错时会显示"这个错误是由那个错误引起的"。
线索不断,这是好习惯。
核心函数 read_raw:
def read_raw(path, skip_bad=True):
path = Path(path)
records = []
bad_lines = []
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, start=1):
line = line.strip()
if not line or line.startswith("#"):
continue
if lineno == 1 and line.lower().startswith("subject"):
continue
try:
records.append(parse_line(line, lineno))
except ValueError as e:
if not skip_bad:
raise ValueError(f"第 {lineno} 行解析失败:{e}") from e
bad_lines.append((lineno, line, str(e)))
return records, bad_lines
设计要点:
| 做了什么 | 为什么 |
|---|---|
返回 (记录, 坏行) 两个东西 |
坏行被跳过但要报告,不能静默吞掉 |
enumerate(f, start=1) |
行号从 1 开始,和编辑器显示一致 |
跳过空行和 # 注释行 |
真实数据里很常见 |
skip_bad 参数 |
给调用方选择"宽容"还是"严格" |
⭐ "不静默吞掉错误"是这个项目最重要的设计。
阶段 10 的认知科学连接讲过:科研代码里,静默失败比崩溃危险得多。
11.2 stats_utils.py:算
def median_of(nums):
"""中位数。空列表返回 0。"""
if not nums:
return 0
s = sorted(nums)
mid = len(s) // 2
if len(s) % 2 == 1:
return s[mid]
return (s[mid - 1] + s[mid]) / 2
注意 len(s) // 2(整除)。索引必须是整数,用 / 会得到浮点数而报错。
group_by 用到了阶段 8 的技能卡:
def group_by(records, key):
groups = {}
for r in records:
groups.setdefault(r[key], []).append(r)
return groups
setdefault(key, []):键不存在时设为 [],存在时返回已有的那个列表。
一行替代了 if key not in groups: 分支,和阶段 8 的 counts.get(w, 0) + 1 是同一个技巧。
11.3 main.py:编排
HERE = Path(__file__).resolve().parent # 脚本所在目录
RAW = HERE / "data" / "raw.csv" # 拼出数据路径
RT_MIN, RT_MAX = 0.2, 3.0 # 清洗阈值,放模块顶部方便改
def main():
print(f"读取:{RAW}")
records, bad_lines = read_raw(RAW)
print(f" 读到 {len(records)} 条记录")
if bad_lines:
print(f" ⚠️ {len(bad_lines)} 行格式有问题,已跳过:")
for lineno, line, err in bad_lines:
print(f" 第 {lineno} 行:{line!r} ({err})")
kept, dropped = clean(records)
print(f"清洗:保留 {len(kept)} 条,丢弃 {len(dropped)} 条")
write_csv(kept, CLEAN)
text = build_report(kept, conditions, subjects)
REPORT.write_text(text, encoding="utf-8")
print(text)
if __name__ == "__main__":
main()
两个关键设计:
| 设计 | 为什么 |
|---|---|
Path(__file__).resolve().parent |
路径自动定位,换电脑换目录都不会崩(技能卡 10) |
if __name__ == "__main__": |
能被 import 而不自动跑(阶段 11),所以 自检.py 能用它的函数 |
⚠️
RT_MIN/RT_MAX放在模块顶部,而不是写在函数里。
参数集中在显眼处,别人(和三个月后的你)一眼就能找到并修改。
11.4 清洗规则
def clean(records):
"""按规则过滤,返回 (保留的, 丢掉的)。"""
kept, dropped = [], []
for r in records:
if r["rt"] < RT_MIN or r["rt"] > RT_MAX:
dropped.append(r)
else:
kept.append(r)
return kept, dropped
为什么剔除极端反应时:
| 情况 | 通常的解释 |
|---|---|
rt < 0.2s |
提前反应 / 误按,人不可能那么快完成认知加工 |
rt > 3.0s |
走神 / 分心,不是在执行任务 |
⚠️ 这是常见的做法,但不是唯一做法,也不是无争议的。
不同的剔除规则会改变结果,所以论文里必须写明用了什么规则。
这正是"可复现性"的一部分。
12 跑起来
完整流程:
$ cd ~/Desktop/大一/编程/项目-反应时分析
$ ~/miniconda3/bin/python main.py
读取:.../data/raw.csv
读到 11 条记录
⚠️ 1 行格式有问题,已跳过:
第 11 行:'坏行没有逗号' (字段数应为 5,实际 1)
清洗:保留 9 条,丢弃 2 条(RT 不在 [0.2, 3.0] 秒)
写出清洗后的数据:.../data/clean.csv(9 行)
写出报告:.../data/report.txt
============================================
反应时数据分析报告
============================================
总试次数:9
被试数 :2
【按条件】
条件 n 均值 中位数 SD
-------------------------------------------------
congruent 5 0.556 0.556 0.063
incongruent 4 0.959 0.950 0.114
【按被试】
被试 n 正确率 均值RT
-----------------------------------
A01 4 75.0% 0.684
A02 5 80.0% 0.776
跑测试:
$ ~/miniconda3/bin/python 自检.py
【io_utils.parse_line】
✓ 字段正确
✓ 容忍空格
✓ 字段数不对应报错
...
✓ 全部通过
结果读出来的意思:congruent(一致条件)比 incongruent(不一致条件)快约 0.4 秒,这正是经典的 Stroop 效应。
13 这个项目示范了什么
把技能卡串起来的六条:
| # | 做法 | 来自 |
|---|---|---|
| 1 | 按职责拆文件 | 技能卡 11 |
| 2 | 路径自动定位,不写死 | 技能卡 10 |
| 3 | 原始数据只读,结果写新文件 | 技能卡 10 |
| 4 | 坏数据要报告,不静默吞 | 阶段 10 |
| 5 | 每个模块可单独测 | 技能卡 5(检查用例) |
| 6 | if __name__ == "__main__" 让模块能被导入 |
阶段 11 |
⭐ 这六条合起来就是一个"可复现的分析流程",任何人拿到你的项目,跑一条命令就能得到同样的结果。
这就是认知科学近年最强调的"可复现研究"在技术上最低限度的样子。
14 🔧 技能卡 14:从零把一个项目跑起来
技能卡 13 教你保住自己的代码;这张卡教你"拿到一个陌生项目,怎么让它跑起来",这是你进实验室第一天要做的事。
14.1 为什么这是独立的一项技能
你 90% 的时间在跑别人的代码,写新代码只占很小一部分:
师兄留下的分析脚本、公开数据集的处理代码、GitHub 上复现论文的仓库。
跑不起来是常态。下面是一套排查顺序。
14.2 五步排查法(按顺序做,别跳)
第 1 步:看 README
cat README.md
90% 的项目会告诉你怎么跑。别急着试命令。
第 2 步:看项目结构,找入口
| 找什么 | 文件名常是 |
|---|---|
| 入口 | main.py、run.py、app.py、__main__.py |
| 依赖清单 | requirements.txt、environment.yml、pyproject.toml |
| 配置 | config.py、.env、config.yaml |
| 数据放哪 | data/、inputs/ |
第 3 步:装依赖
pip install -r requirements.txt
⚠️ 尽量用虚拟环境(阶段 16 详讲)。
直接往系统环境装包,久了会变成一团乱麻。
第 4 步:跑入口,看第一个报错
python main.py
关键心态:不要想"一次跑通",要想"一次消灭一个错误"。
第 5 步:按报错类型对号入座
| 报错 | 通常是 | 怎么办 |
|---|---|---|
ModuleNotFoundError: No module named 'xxx' |
依赖没装 | pip install xxx |
FileNotFoundError |
路径不对 / 数据没放 | 用 describe() 小工具查(技能卡 10) |
ImportError: attempted relative import |
运行位置不对 | 从项目根目录运行 |
SyntaxError |
代码是为旧/新 Python 写的 | 看 Python 版本要求 |
KeyError / IndexError |
数据格式和代码预期不符 | 打印数据的实际样子(repr) |
⭐ 99% 的"跑不起来"是这五类之一。
看到报错先分类,再动手,不要瞎试命令。
14.3 三条铁律
① 报错要读完整,特别是最后一行和倒数第二行
最后一行告诉你错误类型和消息;
倒数第二行(File "xxx.py", line N)告诉你出错的位置。
往往那个位置不是真正的原因,但它是线索的起点。
② 一次只改一个东西
同时改三个地方,跑通了也不知道是哪个起的效。
③ 改不动就给原作者发消息,附上完整报错
把完整报错贴出来(不是"跑不了"三个字)。
别人一看报错就能定位,省双方时间。
💡 把你的排查过程记下来。
同一个项目的坑,第二次遇到时你会庆幸自己记过。
15 🧠 这跟认知科学有什么关系(编程侧)
概念性内容,无法用代码验证,请对照文献。
这个项目 ↔ 认知科学实验的"数据流水线",而流水线的每一步都可能是发表后被质疑的地方。
15.1 原始数据长什么样(真实的脏)
你在项目里看到的是故意造的"温和版"脏数据(1 个坏行、2 个极端值)。真实的更糟:
| 真实数据的常见问题 | 本项目里对应哪一行代码在处理 |
|---|---|
| 空行、注释行 | if not line or line.startswith("#") |
| 字段数不对 | len(parts) != len(COLUMNS) 检查 |
数字里混了 "NA"、"" |
try/except ValueError |
| 多余空格 | p.strip() |
| 编码不是 UTF-8 | encoding="utf-8" |
| 时间戳格式不统一 | (本项目没涉及,但一定会遇到) |
| 被试中途退出,数据不全 | (同上) |
15.2 为什么"清洗规则"是研究可复现性的核心
这是认知科学近年最受批评的地方之一。
| 问题 | 后果 |
|---|---|
| 剔除规则不写明 | 别人无法复现你的结果 |
| 剔除规则改来改去(试到结果显著为止) | 这是p-hacking 的一种,严重的方法学问题 |
| 不同规则给出不同结论 | 说明结果不稳健,这本身是重要信息 |
| 剔除比例很高但不报告 | 读者无法判断数据的真实质量 |
"可复现"在技术上就是:
$$\text{同一个 raw 数据} + \text{同一份代码} \longrightarrow \text{同一份结果}$$
本项目就是一个最小示范:raw.csv 不动,规则写在代码里(RT_MIN/RT_MAX),
任何人跑 python main.py 都得到同样的报告。
15.3 实验数据处理的"流水线"视角
认知科学的分析其实是一条流水线,你的项目实现了它的前几段:
| 阶段 | 做什么 | 本项目 |
|---|---|---|
| ① 采集 | 被试做任务,软件记录 | (E-Prime / PsychoPy 负责) |
| ② 读取 | 解析软件导出的文件 | ✓ io_utils.read_raw |
| ③ 清洗 | 剔除异常试次、排除被试 | ✓ main.clean |
| ④ 汇总 | 算均值/正确率 | ✓ stats_utils |
| ⑤ 统计检验 | t 检验 / ANOVA / 混合模型 | (大一下 / 大二) |
| ⑥ 报告 | 表格、图、论文 | ✓ report.build_report |
| ⑦ 共享 | 数据 + 代码一起发布 | (GitHub) |
⭐ 你现在能把 ②③④⑥ 全部做出来了,这已经超过很多只会点鼠标的人。
⑤ 是统计课的内容;⑦ 是 Git 和开放科学的要求。⚠️ 诚实说明:
- 上面提到的 p-hacking、"剔除规则不透明导致不可复现"是学界公认的方法学问题,
近年有大量讨论(如 Psychological Science 上的多篇方法学论文、
以及整个 reproducibility/replication 讨论)。
- 但"该怎么剔除反应时"没有唯一正确标准,不同领域、不同任务有不同惯例
(有人用 ±3SD,有人用绝对阈值,有人用分布拟合)。
- 本项目的[0.2, 3.0]是为了教学方便选的,不是推荐值。
请以你所在领域的方法论文献和实验室惯例为准。
16 编程作业 S14
先自己把项目跑一遍(第 4 节),再做下面的题。
- [ ] 1. 跑
main.py,把输出贴出来,解释每一行在做什么 - [ ] 2. 跑
自检.py,故意改坏一个函数,看自检能不能发现(比如把median_of的//改成/) - [ ] 3. 在
raw.csv里加 3 行你自己的数据(自己编),重跑,看报告变化 - [ ] 4. 把
RT_MIN改成0.3,重跑,看保留的试次数变化,解释为什么 - [ ] 5. 给项目加一个新统计量:
range_of(极差 = 最大值 − 最小值),加进stats_utils.py和报告 - [ ] 6.(重点)给项目加一个"按被试×条件"的分组报告(比如 A01 在 congruent 下的均值)
- [ ] 7.(重点)试把
stats_utils.py里某个函数的返回值改错,看自检.py是否报错,说明为什么测试能发现 - [ ] 8. 把项目的文件依赖关系画成箭头图(谁能 import 谁),说明为什么不能反过来
- [ ] 9.(技能)假设你拿到一个师兄的项目,跑
python main.py报ModuleNotFoundError: No module named 'pandas',你的排查步骤是什么? - [ ] 10.(技能)给这个项目写一份"三个月后的自己"能看懂的
README补充说明:清洗规则为什么这么定、有什么已知问题
第 6 题参考写法:
def build_cross_report(records, conditions, subject_ids):
"""按"被试 × 条件"给出均值矩阵。"""
out = []
header = f"{'被试':<10}" + "".join(f"{c:>14}" for c in conditions)
out.append(header)
out.append("-" * len(header))
for sid in subject_ids:
row = f"{sid:<10}"
for c in conditions:
rts = [r["rt"] for r in records
if r["subject"] == sid and r["condition"] == c]
cell = f"{mean_of(rts):.3f} (n={len(rts)})" if rts else "-"
row += f"{cell:>14}"
out.append(row)
return "\n".join(out)
# 在 main() 里调用
# print(build_cross_report(kept, conditions, subjects))
用本项目 raw.csv 实跑出来的输出(清洗后 9 条记录):
被试 congruent incongruent
--------------------------------------
A01 0.495 (n=2) 0.873 (n=2)
A02 0.596 (n=3) 1.045 (n=2)
两个被试的效应方向一致(都是 incongruent 更慢),这比只看总平均更有说服力。
💡 这种"被试 × 条件"的表格在认知科学论文里到处都是,它让你一眼看出效应在被试间是否一致。
如果某个被试的效应方向反了,那可能是重要的信息,不该被平均掉。
第 2 题答案:
# 把 stats_utils.py 里的
mid = len(s) // 2 # 整除,得到整数索引
# 改成
mid = len(s) / 2 # 浮点数!
# 然后跑 自检.py,会看到:
# ✗ 中位数奇数
# ...
# ✗ 失败 N 项:['中位数奇数', '中位数偶数', ...]
为什么测试能发现:因为索引必须是整数,
len(s) / 2 得到 1.5,用 s[1.5] 会直接抛
TypeError: list indices must be integers。
这正是"检查用例"的价值(技能卡 5):
改动之后跑一次测试,30 秒就知道有没有改坏东西,比手工在浏览器/终端里试半天可靠得多。
第 9 题答案:
1. 报错说缺 pandas → 先确认项目依赖清单
看 requirements.txt 里有没有 pandas
2. 有 → 装它:pip install -r requirements.txt
没有 → 说明作者漏写了,自己装:pip install pandas
3. 装完再跑,看是否出现下一个错误
(通常还有:FileNotFoundError 找不到数据)
4. 如果 pip install 也失败
→ 看 Python 版本是否匹配(README 里通常写了要求)
心态:不要想"一次跑通",一次消灭一个错误。
本阶段自评
- 数学:你的知识地图有哪四条主线?它们之间的关系是什么?
- 数学:默写等价无穷小表的前 4 条(不许翻书)
- 数学:$(AB)^{-1}$ 和 $(AB)^T$ 各等于什么?它们的共同规律是什么?
- 数学:不定积分表里最容易漏负号的是哪一条?
- 数学:求特征向量的完整步骤是什么?
- 数学:$\int_1^\infty\frac{1}{x}dx$ 收敛吗?为什么和 $\frac{1}{x^2}$ 不一样?
- 数学:微积分基本定理说的是什么?为什么 $e^{-t^2}$ 这种"积不出来"的函数也适用?
- 数学:模拟卷你错了几道?错在哪几类(对照 12 条清单)?
- 编程:这个项目为什么拆成 4 个模块?各自的职责是什么?
- 编程:
read_raw为什么返回两个东西(记录和坏行)?只返回记录有什么问题? - 编程:
setdefault(key, [])和groups.get(key, 0)这类写法,省掉了什么? - 编程:
main.py里的路径为什么用Path(__file__).resolve().parent而不是写死? - 技能:拿到一个陌生项目,你的五步排查顺序是什么?
- 技能:
ModuleNotFoundError和FileNotFoundError分别意味着什么?怎么查? - 连接:为什么"看懂答案"不等于"会做"?"流畅性错觉"是什么?
- 连接:间隔效应和提取练习,对你的复习方式有什么具体改变?
全部答得上 → 进阶段 15,你离"大一上主线完成"只剩两阶段。
有任何一个答不上 → 回去补,别跳。