feat(solve): monte_carlo 系列两个函数落地(L0 11/12、L1)+ Wilson 区间;修 z_score^2 异或 bug

用户实现:
- solve.monte_carlo(L0 第 11 项,S 档):向量化采样→仿真→汇总,8 个指标;
  批量大小由实现传 sampler(rng, size=n);结果含 NaN 判失败;std 用 ddof=0。
- solve.monte_carlo_probability(L1,S 档):event(batch)->bool,Wilson score 区间夹 [0,1],
  confidence 记进 params,小概率改成 message 警告。

Codex 只碰工程侧(算法未动,唯一例外见下):
- 真 bug:z_score^2 是异或不是乘方(np.float64 ^ 2 抛 TypeError,被 except 吞成 success=False,
  函数实际不可用)-> 改 z_score**2。新增经验第 21 条。
- docstring:补齐 Parameters / Raises,修掉两处跑不通的 Examples,Wald -> Wilson 同步。
- 入口校验按 §4.7 收严 n(bool / np.integer / TypeError 与 ValueError 分工)—— 决策 49 后此类归用户,已记档。
- ruff 九条(I001 / F541 x3 / SIM300 / W291 x2 / W293 x2 等)、格式与文案。
- 测试:摘掉 2 条已实现却仍挂着的 skip(易错点 9),新增 14 条(含 vectorization、
  Wilson 对 statsmodels 的对照、NaN / 布尔 / 长度等边界),solve 段共 25 条。

校验:193 收集 / 161 passed / 32 skipped / 0 xfail;ruff 0;pyright 0。
文档:STATUS.md 决策 47-50;docs/dev/personal-pitfalls.md 第 18-21 条;CHANGELOG.md。
This commit is contained in:
guzhujushi committed 2026-10-05 11:26:54 +08:00
1 parent 58f2236703
commit b0c4b4a413
5 files changed
+619 -61

No files matched your search

+71 -1
View File
@@ -39,6 +39,10 @@
| 15 | 区间被无条件夹到 `[0, 1]`,且 `min` / `max` 参数写反 | 2026-10-03 |
| 16 | 同一段入口转换抄了 N 遍,还当它是"本项目的写法" | 2026-10-03 |
| 17 | `hasattr(f, "__name__")` 挡不住 lambda,`else` 分支是死代码 | 2026-10-03 |
| 18 | 入口校验"想起什么写什么",没照 §4.7 清单逐条过 | 2026-10-04 |
| 19 | 只想着主路径那种值,忘了布尔 / NaN / 长度不对 | 2026-10-04 |
| 20 | "不简单"的信号:同一个信息要在调用方写两遍 | 2026-10-04 |
| 21 | 公式里的 `^` 不是乘方,而且被 `except` 吞成 success=False | 2026-10-05 |
---
@@ -50,7 +54,10 @@
- 以后怎么防:写类型分支时默认假设"这个值可能来自 numpy"。判断用 `isinstance(x, (int, np.integer))`,
取值统一 `int(x)` 归一化;并且给所有非预期类型加 `raise TypeError` 兜底——
**宁可报错,也不要静默走默认分支**。
- 首次:2026-10-01|复发:0
- 复发(2026-10-04):`monte_carlo` 的 `n` 只写了 `isinstance(n, int)` —— `np.int64(1000)` 被 `ValueError` **误拒**,
而 `n=True` 反而放行(`bool` 是 `int` 子类,判断顺序又反了)。
同一张 §4.7 表的第一、三行,症状从"静默兜底"变成"误拒 + 误收":**规则没换,只是没去查**(见第 18 条)。
- 首次:2026-10-01|复发:0 → **1(2026-10-04)**
### 2. 以为 `# TODO(L0)` 只是普通注释
@@ -309,3 +316,66 @@
把"没有名字"和"名字是占位符"两种情况都覆盖;③ 每个 `else`/兜底分支都问一句
"**这条到底测得到吗**"——走不到的分支就是 bug 伪装成的防御性代码。
- 首次:2026-10-03|复发:0
### 18. 入口校验"想起什么写什么",没有照着清单逐条过
- 现象:`monte_carlo` 的入口只写了"n 是整数、n 为正",没写"那 `n=True` / `n=np.int64` / `n=10.0` 各走哪条"。
结果 `np.int64(1000)` 被误拒、`n=True` 被放行、浮点 `n` 报的是 `ValueError` 而不是 `TypeError`。
§4.7 那张判断顺序表早就写死了,写代码时一次都没翻。
- 为什么会错:校验是"防御性代码",写的时候满脑子是主路径("次数当然是整数"),
**不会自然想到 `True`、`np.int64`、`None`、0 这些"合法类型里的例外值"**。
清单存在的意义正是替代这种直觉——但清单要主动去查才有用;不查就等于没有。
同类:第 1 条(只写 `int`)、第 7 条(守卫子句的位置)、`random_state=None` 绕开 `get_rng`(决策 46)。
- 以后怎么防:每个公开函数写完,把 §4.7 的表当 checklist **逐行过一遍**
(int + np.integer / bool 排在 int 前 / float + np.floating / 数组先 `np.asarray`),
再用三个探针各打一枪:`f(x=np.int64(1))`、`f(x=True)`、`f(x=1.0)`。
**这一步是本人的活**(决策 49):要不要放宽、报什么错、算不算合法,都是取舍,不是机械活。
- 首次:2026-10-04|复发:0
### 19. 只想着主路径上的那种值,忘了同一位置还会出现别的类型 / 值
- 现象:`monte_carlo` 的 `simulator` 返回**布尔**数组("事件是否发生",MC 最常见的用法)时,
`np.asarray(...)` 原样保留 bool,`np.std` 在 numpy 2 上直接抛
`numpy boolean subtract ... is not supported`,整个函数判失败;字符串结果则给出
"ufunc isnan not supported"这种看不懂的话。同一轮还漏了:结果里含一个 NaN 时 8 个指标全 NaN 却报 `success=True`。
- 为什么会错:写的时候脑子里只有"年收益 = 浮点数"这一种结果,
"0/1 事件"、"NaN"、"长度对不上"都不在主路径上。而**这个函数恰恰是这几类输入的高发区**
(事件概率、模型发散、批量大小写错),所以它们不是边角料,是常规用法。
- 以后怎么防:写完一个"吃调用方数组"的函数,除了主路径,按五类各跑一次:
**布尔 / 整数 / 含 NaN / 全相同(常量)/ 长度不对**。
类型归一化放在**入口一处**做(`np.asarray(x, dtype=float)`),后面所有指标就都安全了——
这正是 §4.7 表最后一行与 §4.8 存在的理由。
- 首次:2026-10-04|复发:0
### 20. "不简单"的最早信号:同一个信息要在调用方写两遍
- 现象:第一版 `monte_carlo` 的契约是 `sampler(rng, **kwargs)`,但采样器**拿不到 n**,
于是调用方必须写 `n=1000, size=1000` —— 同一个数写两遍,还要靠 `len(data) != n` 事后兜底。
改成实现自己传 `sampler(rng, size=n, **kwargs)` 之后,调用方只写 `n=1000`。
同一处的后遗症:`**kwargs` 里再传 `size` 会撞成 `got multiple values for keyword argument`。
- 为什么会错:设计签名时只想着"参数全靠 `**kwargs` 透传"这一条好处(灵活),
没数"调用方为了让它跑起来还得额外写什么"。
**重复的那一个数是症状,不是问题**:真正的问题是"批量大小"这个信息在接口里**没有归属**。
- 以后怎么防:写完签名,立刻写出**最小调用示例**,然后数两件事:
① 有没有哪个信息写了两遍;② 有没有哪一步"只有我知道"。有 → 把它吸收进实现,而不是写进文档。
简单 ≠ 参数少,而是**调用方不用重复表达**。
- 首次:2026-10-04|复发:0
### 21. 把数学公式抄进代码时,`^` 当成乘方 —— 还被 `except` 吞成 `success=False`
- 现象:`monte_carlo_probability` 的 Wilson 区间里写 `z_score^2 / n`。Python 的 `^` 是**按位异或**,
对 `np.float64` 直接抛 `TypeError: ufunc 'bitwise_xor' not supported`;因为整段计算在
`try/except Exception` 里,对外只表现为 `success=False` + 一句 message ——
最后是 6 条测试全红(`KeyError: 'ci_high'`)才把它挖出来。
- 为什么会错:纸上的 `z²`、`x²` 抄进代码时手会自动敲 `^`(数学记号、Excel、LaTeX 都是这个符号),
而 Python 的乘方是 `**`。更险的是 `^` 在**整数**上是合法异或:`3^2` 得到 1、不报错,只算错。
第二层原因是 `try/except Exception` 把"代码写错"和"数据不合法"混成一个出口,
错误现场(哪一行、哪个异常)被压成一句字符串,静态检查也看不见(pyright 当时 0 error)。
- 以后怎么防:① 公式里的幂一律写 `**`(或 `np.power`),见到 `^` 就当 bug 处理;
② 新函数写完先**手动跑一次正常输入**,`success` 必须是 `True` —— 这条一跑就现形;
③ 给算法配一条**独立实现的对照测试**(Wilson 对 `statsmodels.proportion_confint(method="wilson")`),
这类"公式抄错"会被直接钉死;④ 记住 `except Exception` 越宽,错误越难看见。
- 首次:2026-10-05|复发:0