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:
1 parent
58f2236703
commit
b0c4b4a413
5 files changed
+619
-61
No files matched your search
@@ -338,6 +338,67 @@
|
|||||||
(`monte_carlo` 的契约 / 向量化要求 / 三条测试建议 / 卡片路径 / 一个待拍板口径);
|
(`monte_carlo` 的契约 / 向量化要求 / 三条测试建议 / 卡片路径 / 一个待拍板口径);
|
||||||
验证结果里的 skip 说明由 37 条订正为 **34 条**。
|
验证结果里的 skip 说明由 37 条订正为 **34 条**。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `solve.monte_carlo` 落地(**L0 第 11 项,S 档**,实现由用户本人写):向量化"采样 → 仿真 → 汇总",
|
||||||
|
返回 `Result`(8 个 metrics:mean / std / median / p05 / p95 / ci_low / ci_high),随机数无条件走 `get_rng`。
|
||||||
|
配套 `tests/test_solve.py` **10 条**测试(含"`simulator` 只被调用一次"这条可执行的向量化断言)。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `monte_carlo` 的**布尔结果**此前让 `np.std` 在 numpy 2 里抛 `numpy boolean subtract ... is not supported`,
|
||||||
|
整个函数判失败 —— 而"事件是否发生"是 MC 最常见的用法。入口改为 `np.asarray(..., dtype=float)`(布尔按 0/1 统计);
|
||||||
|
字符串结果也从"ufunc isnan 不支持"变成可读的 `could not convert string to float`。由行为探针挖出。
|
||||||
|
- `monte_carlo` 的 docstring `Examples` 跑不通:原采样器返回标量,实测 `success=False`
|
||||||
|
("simulator 返回的结果数量(1)必须与模拟次数 n(1000)一致");改成向量化写法。
|
||||||
|
- `monte_carlo` 的 ruff 三条(I001 合并同源 import、F541 空 f-string ×2)、`success= True` 的空格、
|
||||||
|
失败文案"重采样" → "模拟"(从 `bootstrap_ci` 抄过来忘改)。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `monte_carlo` 按 §4.7 收严 `n`:`np.int64` 不再被误拒(注解放宽为 `int | np.integer`);
|
||||||
|
`n=True` / 浮点 `n` 抛 `TypeError`;`n<=0` 抛 `ValueError`。
|
||||||
|
- 用户第二轮改的三处口径(决策 48):批量大小改由实现传 `sampler(rng, size=n)`;结果含 NaN 判 `success=False`;
|
||||||
|
`np.std` 显式 `ddof=0`。docstring 同步改写(`sampler` 契约、`Raises`、`Notes` 的 NaN 口径)。
|
||||||
|
- `STATUS.md`:L0 10/12 → **11/12**,新增决策 48;验证计数更新为 **187 收集 / 153 passed / 34 skipped**。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 工作约定收窄(**决策 49**):**入口没收紧 / 少考虑情况 / 不符合简单原则**这三类改动交回用户本人写,
|
||||||
|
Codex 只报告 + 列选项(判据:这处改动要不要"想一下才对")。决策 47 里"入口 / 校验重构归 Codex"的部分作废。
|
||||||
|
- `docs/dev/personal-pitfalls.md`:新增 **第 18 / 19 / 20 条**(入口校验没照 §4.7 清单逐条过 / 只想着主路径那种值 /
|
||||||
|
"不简单"的信号是同一个信息要写两遍),第 1 条(类型判断只写 `int`)**复发 +1**,索引补到 20。
|
||||||
|
- `STATUS.md`:新增决策 49 与两个待拍板小口径(`random_state` 注解是否全项目对齐、`std` 的 `ddof` 是否统一)。
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `solve.monte_carlo_probability` 落地(**L1,S 档**,实现由用户本人写):向量化 `event(batch) -> bool 数组`,
|
||||||
|
返回 `Result`(`probability` / `ci_low` / `ci_high` / `std_error`),区间是 Wald 区间并夹在 `[0, 1]`(沿用决策 42 口径)。
|
||||||
|
`tests/test_solve.py` 新增 5 条 + **摘掉 2 条 skip**(实现已写完却还挂着,易错点 9),`solve` 段共 24 条。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `monte_carlo_probability` 的 docstring `Examples` 跑不通:原采样器返回标量,实测 `success=False`
|
||||||
|
("event 返回的结果数量(1)必须与模拟次数 n(2000)一致");改成批量采样写法并实测通过。
|
||||||
|
- `monte_carlo_probability` 的 ruff 六条:SIM300(`0.0 >= confidence` 写成 Yoda 条件)、W291 ×2、W293 ×2、F541。
|
||||||
|
- 该函数 NaN 提示里的 "simulator" 改成 "event"(从 `monte_carlo` 抄来没改);`metrics` 缩进与 `{"n":n}` 空格。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `monte_carlo_probability` 的 docstring 补齐 `Parameters` / `Raises`(原文只有 Returns / Notes / Examples),
|
||||||
|
并把"Wald 区间 + 夹到 [0,1]"与"`p <= 1e-3` 判失败"两条现状如实写进 Returns / Notes。
|
||||||
|
- `STATUS.md`:L1 清单加入 `monte_carlo_probability`;验证计数更新为 **192 收集 / 160 passed / 32 skipped**;
|
||||||
|
「下一步」新增该函数的 **6 条待办口径**(全部留给用户,Codex 未动)。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`monte_carlo_probability` 的 `z_score^2` 是异或不是乘方**(Python `^` 语义):`np.float64 ^ 2` 直接抛
|
||||||
|
`TypeError: ufunc 'bitwise_xor' not supported`,被 `except Exception` 吞成 `success=False` —— 函数实际上**整个不可用**,
|
||||||
|
6 条测试全红才暴露。改成 `z_score**2`。新增经验第 21 条。
|
||||||
|
- `monte_carlo_probability` 的 message 在没有警告时留了个尾逗号;`{"confidence":confidence}` 补空格。
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 用户第二轮改定(**决策 50**):区间由 **Wald 换成 Wilson score**(实测 p=1、n=1000 从退化的 `(1.0, 1.0)`
|
||||||
|
变成 `(0.99617, 1.0)`;p=0 从"判失败"变成 `(0.0, 0.00383)`);小概率不再判失败,改为 message 里的警告;
|
||||||
|
`confidence` 记进 `Result.params`;`callable` 入口校验(现在抛 `TypeError`);`float()` 统一 `ci_*` 类型;
|
||||||
|
`n` 注解放宽为 `int | np.integer`。
|
||||||
|
- `tests/test_solve.py`:新增 1 条对照测试(区间必须等于 `statsmodels.proportion_confint(method="wilson")`),
|
||||||
|
共 25 条;全量 **193 收集 / 161 passed / 32 skipped / 0 xfail**。
|
||||||
|
- `docs/dev/personal-pitfalls.md`:新增第 **21** 条(公式里的 `^` 不是乘方,且被 `except` 吞成 `success=False`),索引补到 21。
|
||||||
|
- docstring 的 "Wald 区间" 同步改为 "Wilson score 区间"。
|
||||||
|
|
||||||
## [0.0.1] - 2026-10-01
|
## [0.0.1] - 2026-10-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
- A 档**薄封装**(`rmse`/`mae`/`fit_linear`/`train_test_split`…)算工程,Codex 可直接实现。
|
- A 档**薄封装**(`rmse`/`mae`/`fit_linear`/`train_test_split`…)算工程,Codex 可直接实现。
|
||||||
- 分地盘按**文件**划,不按函数划(同一文件两人各写一半曾导致用户编辑器保存失败)。
|
- 分地盘按**文件**划,不按函数划(同一文件两人各写一半曾导致用户编辑器保存失败)。
|
||||||
|
|
||||||
当前 **L0 10/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
||||||
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`;
|
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
||||||
另**提前落地**了 L1 的 `confidence_interval` / `t_test`。剩余 2 项:`monte_carlo`(用户写)、
|
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
||||||
`save_figure`(Codex 写)。
|
`monte_carlo_probability`(**用户写**,2026-10-05)。剩余 1 项:`save_figure`(Codex 写)。
|
||||||
|
`monte_carlo` 的口径已拍板(决策 48),`ci_low`/`ci_high` 的语义与 `monte_carlo_probability` 的 6 条待办见"下一步"。
|
||||||
|
|
||||||
## 任务清单
|
## 任务清单
|
||||||
|
|
||||||
@@ -234,13 +235,52 @@
|
|||||||
- [x] **工程收尾(本轮)**:`tests/test_solve.py` 的模块级 `pytestmark = pytest.mark.skip(...)`
|
- [x] **工程收尾(本轮)**:`tests/test_solve.py` 的模块级 `pytestmark = pytest.mark.skip(...)`
|
||||||
拆成 **9 条逐条** `@pytest.mark.skip(reason=_NOT_IMPLEMENTED)` —— 模块级 skip 会把整个文件吞掉,
|
拆成 **9 条逐条** `@pytest.mark.skip(reason=_NOT_IMPLEMENTED)` —— 模块级 skip 会把整个文件吞掉,
|
||||||
正是易错点 9 的根源(决策 26)。收集数不变(177)、跳过数不变(34),只是粒度变细。
|
正是易错点 9 的根源(决策 26)。收集数不变(177)、跳过数不变(34),只是粒度变细。
|
||||||
- [ ] 剩余 2 项:`monte_carlo`(**用户写**)、`save_figure`(Codex 写)
|
- [x] `solve/simulate.py` 的 `monte_carlo`(**L0 第 11 项,S 档,算法由用户本人写**,2026-10-04)
|
||||||
|
- 核心逻辑:向量化采样 → 一次仿真 → 8 个指标;`get_rng` 无条件调用(决策 46 已内化)
|
||||||
|
- 用户第二轮改了三处(见决策 48):批量大小改由实现传 `sampler(rng, size=n)`;
|
||||||
|
结果含 NaN 判失败;`np.std(data, ddof=0)` 写明
|
||||||
|
- **Codex 只碰工程侧,算法一行未动**:
|
||||||
|
① ruff 三条(I001 合并同源 import、F541 空 f-string ×2)
|
||||||
|
② docstring:补 `Parameters`(原文漏 `random_state` / `**kwargs`)与 `Raises`;按新契约改写
|
||||||
|
`sampler` 说明(必须能接住 `size`,且 `**kwargs` 里别再传 `size`);`Notes` 记 NaN 口径;
|
||||||
|
**修掉跑不通的 Examples**(原版 `lambda rng, **k: rng.normal(0, 1)` 返回标量,实测 `success=False`)
|
||||||
|
③ 按 §4.7 收严 `n` 校验:`np.int64` 不再被拒、`n=True` 改抛 `TypeError`、浮点 `n` 归 `TypeError`、
|
||||||
|
`n<=0` 归 `ValueError`
|
||||||
|
④ 格式与文案:`success= True` → `success=True`;失败文案"重采样" → "模拟"(从 `bootstrap_ci` 抄来没改)
|
||||||
|
⑤ **入口统一 `dtype=float`** —— 修掉一个真 bug:布尔结果("事件是否发生",MC 最常见的用法)
|
||||||
|
会让 `np.std` 在 numpy 2 里抛 `boolean subtract ... is not supported`,整个函数判失败;
|
||||||
|
字符串结果也从"ufunc isnan 不支持"变成可读的 `could not convert string to float`
|
||||||
|
- **测试:10 条**(`tests/test_solve.py`)—— 返回长度与 8 个 key、`simulator` 只被调用一次(向量化的可执行版本)、
|
||||||
|
同种子可复现、省略种子时跟随 `set_seed`、E[X²]=1(n=2e5)、docstring Example 真能跑、`np.int64` 的 n、
|
||||||
|
布尔结果按 0/1 统计、NaN 结果判失败、5 个非法入参
|
||||||
|
- 行为探针实测:`simulator` 调用次数 = **1**(真向量化);20/20 次区间覆盖 0
|
||||||
|
- [x] `solve/simulate.py` 的 `monte_carlo_probability`(**L1,S 档,算法由用户本人写**,2026-10-05)
|
||||||
|
- 核心:向量化 `event(batch) -> bool 数组` → `p = mean(data)` → **Wilson score 区间**并夹到 `[0, 1]`
|
||||||
|
(比例量纲,沿用决策 42 的口径——同类问题没重犯)
|
||||||
|
- 用户第二轮(2026-10-05)按 6 条待办改掉 5 条:Wilson 替 Wald、`callable` 入口校验、
|
||||||
|
`confidence` 记进 `params`、`float()` 统一 ci 类型、小概率不再判失败(改成 message 里的警告)——见决策 50
|
||||||
|
- **Codex 第三轮只碰工程侧**:① `z_score^2` → `z_score**2` —— **真 bug**:`^` 是异或,
|
||||||
|
numpy 浮点直接抛 `TypeError`,被 `except` 吞成 `success=False`,6 条测试全红才暴露(易错点 21);
|
||||||
|
② message 无警告时的尾逗号;③ `{"confidence":confidence}` 的空格;④ docstring 的 "Wald" → "Wilson";
|
||||||
|
⑤ 新增 1 条对照测试:区间必须等于 `statsmodels.proportion_confint(method="wilson")`
|
||||||
|
- 行为探针实测:p≈0.5 正确(0.49991 @ n=2e5)、`confidence` 变大区间变宽、
|
||||||
|
同种子可复现、省略种子跟随 `set_seed`、`np.int64` 的 n 可收、`confidence=1.5` 抛 `ValueError`
|
||||||
|
- **Codex 只碰工程侧,算法一行未动**:
|
||||||
|
① ruff 六条(SIM300 Yoda、W291 ×2、W293 ×2、F541)
|
||||||
|
② docstring:补 `Parameters`(原文只有 Returns/Notes/Examples)与 `Raises`,
|
||||||
|
**修掉跑不通的 Examples**(原 `lambda rng, **k: rng.normal(0, 1)` 返回标量,实测 `success=False`),
|
||||||
|
并把"Wald 区间 + 夹到 [0,1]"与"`p <= 1e-3` 判失败"两条**现状**如实写进 Returns / Notes
|
||||||
|
③ 格式:`metrics` 字典缩进、`{"n":n, ...}` 空格、行尾空白
|
||||||
|
④ 文案:NaN 提示里的 "simulator" 改成 "event"(从 `monte_carlo` 抄来没改)
|
||||||
|
- **测试:摘掉 2 条 skip**(`test_monte_carlo_probability_of_standard_normal`、`test_monte_carlo_is_reproducible`
|
||||||
|
—— 实现写完还挂着 skip,正是易错点 9)并新增 5 条,`solve` 段共 24 条
|
||||||
|
- [ ] 剩余 1 项:`save_figure`(A 档 = 工程,**Codex 直接写**)
|
||||||
|
|
||||||
### 验证结果
|
### 验证结果
|
||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | venv | conda `mcm-toolkit` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `pytest` | 见下(单列) | **143 passed, 34 skipped(0 xfail)** |
|
| `pytest` | 见下(单列) | **161 passed, 32 skipped(0 xfail)** |
|
||||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||||
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
| `ruff check` | All checks passed | **All checks passed**(0 error) |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
@@ -250,13 +290,14 @@
|
|||||||
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
|
||||||
`docs/04-environment.md` 坑 7)
|
`docs/04-environment.md` 坑 7)
|
||||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||||
- **177 条用例:143 passed / 34 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
- **193 条用例:161 passed / 32 skipped / 0 xfail**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
|
||||||
`test_core.py` 23、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 53、
|
`test_core.py` 23、`test_solve.py` 25(+16,含摘掉 2 条 skip)、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 53、
|
||||||
`test_models.py` 5
|
`test_models.py` 5
|
||||||
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
- **`xfail` 再次清零**:`correlation_matrix` 的契约冲突已按方案 A 解决(决策 41),那条用例转正
|
||||||
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
- 上一轮那 7 条 xfail 已全部转正,决策 30 那套信号(XPASS → 摘 mark)跑完过一个完整闭环
|
||||||
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
|
||||||
- 34 条 skip 是等实现的算法(`train_test_split`、`monte_carlo`、`anova`、`fit_distribution` 等);
|
- 32 条 skip 是等实现的算法(`train_test_split`、`monte_carlo_integral`、`anova`、`fit_distribution` 等);
|
||||||
|
`monte_carlo_probability` 已落地,摘掉 2 条 skip(本轮净 +3 条用例);
|
||||||
`bootstrap_ci` 已落地摘掉 3 条 skip(本轮新增 6 条用例,净 +3);`test_solve.py` 9 条按决策 26 改为逐条 skip;
|
`bootstrap_ci` 已落地摘掉 3 条 skip(本轮新增 6 条用例,净 +3);`test_solve.py` 9 条按决策 26 改为逐条 skip;
|
||||||
`TestValidate` 的 4 条仍挂着 —— `check_*` 还是 `NotImplementedError`,但它们实现时应当直接复用 `as_array`
|
`TestValidate` 的 4 条仍挂着 —— `check_*` 还是 `NotImplementedError`,但它们实现时应当直接复用 `as_array`
|
||||||
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
|
||||||
@@ -271,7 +312,7 @@
|
|||||||
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
||||||
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
||||||
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
||||||
- [ ] L0 完成 **10/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
- [ ] L0 完成 **11/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
||||||
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`),
|
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`),
|
||||||
另 L1 的 `confidence_interval` / `t_test` 已提前落地;
|
另 L1 的 `confidence_interval` / `t_test` 已提前落地;
|
||||||
**只剩 `monte_carlo`(用户写)与 `save_figure`(Codex 写)**,详情见 E 节与"下一步"
|
**只剩 `monte_carlo`(用户写)与 `save_figure`(Codex 写)**,详情见 E 节与"下一步"
|
||||||
@@ -353,37 +394,89 @@ docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向
|
|||||||
工程侧(入口/校验、`tests/`、lint、文档、卡片、`.gitignore`)反过来由 Codex 直接落盘,用户只读;
|
工程侧(入口/校验、`tests/`、lint、文档、卡片、`.gitignore`)反过来由 Codex 直接落盘,用户只读;
|
||||||
分地盘按**文件**划,不按函数划。决策 39 的"语义由用户拍板"继续有效。
|
分地盘按**文件**划,不按函数划。决策 39 的"语义由用户拍板"继续有效。
|
||||||
|
|
||||||
|
| 48 | `monte_carlo` 的四个口径由**用户拍板**:实现传 `size=n`;结果含 NaN 判失败;`std` 用 `ddof=0`;出错仍统一返回 `success=False` |
|
||||||
|
用户 2026-10-04 直接改在代码里。① 批量大小由实现传(`sampler(rng, size=n, **kwargs)`):调用方少写一次,
|
||||||
|
代价是**每个 sampler 都必须能接住 `size`**,且 `**kwargs` 里不能再传 `size`(会重复传参)—— 已写进 docstring。
|
||||||
|
② 结果里只要出现 NaN 就 `success=False`:MC 的 NaN 多来自除零 / 模型发散,静默统计只会得到一列 nan 指标。
|
||||||
|
③ `np.std` 默认 `ddof=0`,现在显式写明(与 `describe` 那条线的 `ddof=1` 不一致,属已知差异)。
|
||||||
|
④ 计算期异常仍全部包成 `success=False`(保留 `try/except Exception`);代价是自己写错代码时没有 traceback。
|
||||||
|
配套 Codex 代改:入口 `np.asarray(..., dtype=float)` —— 布尔结果此前会让 `np.std` 抛错(真 bug,已修)。
|
||||||
|
**唯一未定**:`ci_low`/`ci_high` 保持"结果的分位数"(现状)还是改成"均值的置信区间"。
|
||||||
|
|
||||||
|
| 49 | **这类改动归用户本人**:入口没收紧、少考虑情况、不符合简单原则 —— 用户 2026-10-04 明确"交给我改,不要帮我改" |
|
||||||
|
用户原话:"以后像这种入口没收紧、少考虑情况、不符合简单原则的情况交给我改,不要帮我改,把这几条经验写入经验文档"。
|
||||||
|
这是决策 47 的**收窄**:47 把"入口 / 校验重构"划给了 Codex,49 把它收回 —— 凡是**带判断、取舍、设计成分**的,
|
||||||
|
一律由用户写,Codex 只报告 + 列选项(与决策 39 同一逻辑)。判据一句话:**这处改动要不要"想一下才对"** ——
|
||||||
|
要,就留给用户;不要(ruff、格式化、docstring 缺节、`tests/`、`STATUS` / `CHANGELOG`、函数卡片)才由 Codex 顺手补。
|
||||||
|
本轮实例(本该留给用户,Codex 已代改、用户可回退重写):`monte_carlo` 的 `n` 按 §4.7 收严;入口 `np.asarray(..., dtype=float)`。
|
||||||
|
经验已写入 `docs/dev/personal-pitfalls.md` **第 18 / 19 / 20 条**,并把第 1 条(类型判断只写 `int`)**复发 +1**。
|
||||||
|
|
||||||
|
| 50 | `monte_carlo_probability` 的区间由 **Wald 改 Wilson score**;小概率不再判失败(改成 message 警告);`confidence` 记进 `params` |
|
||||||
|
用户 2026-10-05 按"6 条待办"自行改定。Wilson 在 p 贴近 0 / 1 时不会退化成零宽:实测 p=1、n=1000 从 `(1.0, 1.0)`
|
||||||
|
变成 `(0.99617, 1.0)`;p=0 从"判失败"变成 `(0.0, 0.00383)`(原 Wald 是 0)。区间口径与
|
||||||
|
`statsmodels.proportion_confint(method="wilson")` 对齐,已加成对照测试。
|
||||||
|
**遗留两条**:`event: Callable[..., bool]` 注解与"吃整批、返回数组"不符;警告文案把 p≈1 也说成"小概率事件"。
|
||||||
|
另:`z_score^2` 那处是 Codex 修的真 bug(`^` 是异或,见易错点 21)——按决策 31 属纯 bug,未改任何语义。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
### ⭐ 明天从这里开始(第一个要写的文件)
|
### ⭐ 当前进度与待办(2026-10-05)
|
||||||
|
|
||||||
**文件:`src/mcm_toolkit/solve/simulate.py` → 写 `monte_carlo`(L0 第 11/12 项,S 档)。**
|
**本轮:`monte_carlo_probability` 已由用户写完并改到第二版**(L1,S 档)。Codex 两轮都只做工程侧,
|
||||||
同一文件里 `monte_carlo_probability` / `monte_carlo_integral` / `random_walk` 先别动 —— 一次一个函数。
|
**算法逻辑一行未动**(唯一例外是一个纯 typo:`z_score^2` → `z_score**2`,见易错点 21 / 决策 50)。
|
||||||
|
|
||||||
**① 参考契约(docstring 里已定,签名别改)**
|
**接下来三件事**
|
||||||
- `monte_carlo(sampler, simulator, n=10000, random_state=None, **kwargs) -> Result`
|
1. `monte_carlo_probability` 还剩 **2 条待办**(下面一节)——都是你的活。
|
||||||
- `sampler(rng, **kwargs) -> 参数`:一次抽**一整批**参数(可以是标量/数组/元组,随便)
|
2. `ci_low` / `ci_high` 的语义确认一句(也是你的活),确认后 Codex 补 `docs/algorithms/monte_carlo.md` 卡片。
|
||||||
- `simulator(params) -> float`:单次仿真结果
|
3. L0 最后一项 `save_figure`(A 档 = 工程,`viz/export.py`)—— **Codex 直接实现**,你只读。
|
||||||
- 返回 `Result`:`data` = `n` 个结果组成的 `np.ndarray`;
|
|
||||||
`metrics = {"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}`
|
|
||||||
|
|
||||||
**② 必须向量化**:`sampler` 一次抽 `n` 份(如 `rng.normal(0, 1, size=n)`),`simulator` 对整个参数数组算一次,
|
**你下一个要写的(S 档,仍在 `solve/simulate.py`)**:`monte_carlo_integral` → `random_walk`。
|
||||||
**绝不在 `simulator` 里写 Python 循环**。10^4 次循环还能忍,10^6 次直接放弃 —— 这是"能用"和"不能用"的分界线。
|
`monte_carlo_integral` 其实就是 `monte_carlo` 的特例:自己造 `n×d` 的均匀样本 → 逐点求值 →
|
||||||
|
`mean(f) × 体积`,正好把那套"造批 → 一次算 → 汇总"的写法复用第二次。
|
||||||
|
|
||||||
**③ 随机数无条件走 `core.get_rng`**(决策 46 的教训,别重演 bootstrap 那个坑):
|
### ✅ monte_carlo_probability 的 6 条待办(2026-10-05 已办 5 条)
|
||||||
`rng = get_rng(random_state)`,不要自己另开 `np.random.default_rng()`,否则 `set_seed` 管不到它、区间不可复现。
|
|
||||||
|
|
||||||
**④ 先写一条会失败的测试**(roadmap §3.2 的纪律)。`tests/test_solve.py` 现在**没有**直接测 `monte_carlo` 的用例
|
- ~~① `p <= 1e-3` 判 `success=False`~~ → 改成 `warning_message`,仍返回区间(实测 p=0 → `(0.0, 0.00383)`)。
|
||||||
(`test_monte_carlo_is_reproducible` 名字像,实际调的是 `monte_carlo_probability`)。建议三条:
|
- ~~② Wald 在 p 靠近 1 时退化~~ → 换 **Wilson**(p=1、n=1000:`(1.0, 1.0)` → `(0.99617, 1.0)`)。
|
||||||
- 固定种子两次结果完全相同(可复现)
|
- ~~③ `call_params` 没记 `confidence`~~ → 已加。
|
||||||
- `sampler` 恒返回常数 → `data` 全等于该常数、`std == 0`(常量边界)
|
- ~~④ `event` / `sampler` 不可调用被吞成 `success=False`~~ → 已加 `callable` 校验,现在抛 `TypeError`。
|
||||||
- `n` 增大时 `mean` 的波动按 `O(1/√n)` 下降(可留成大样本慢测或标 `slow`)
|
- ~~⑤ `metrics` 混 `np.float64`~~ → `float()` 统一(实测 4 个 key 全是 `float`)。
|
||||||
|
- ~~⑥ `n: int`~~ → 已改 `int | np.integer`。
|
||||||
|
|
||||||
**⑤ 还欠一张 S 档卡片**:`docs/algorithms/monte_carlo.md`(模板 `docs/dev/function-card-template.md`,
|
**还剩 2 条(你的活)**
|
||||||
照抄 `docs/algorithms/bootstrap_ci.md` 的骨架),写完把 `docs/algorithms/README.md` 的进度表打勾。
|
1. `event: Callable[..., bool]` 的注解与行为不符:实际必须吃**整批**参数、返回长度 `n` 的数组。
|
||||||
|
2. 警告文案把 **p ≈ 1** 也说成"小概率事件 P < 1e-3 不建议使用"——那一条建议对 p→1 是错的
|
||||||
|
(临界情形该说的是"改用 1−p 或换方法")。条件是你写的 `p_value <= 1e-3 or p_value >= 1 - 1e-3`,文案只覆盖了前一半。
|
||||||
|
|
||||||
**⑥ 等你拍板的一个口径(Codex 不代选,决策 39/47)**:`ci_low` / `ci_high` 用**分位数法**
|
### ⏳ monte_carlo 只剩 1 个口径要你确认
|
||||||
(`np.percentile(data, [2.5, 97.5])`)还是**正态近似**(`mean ± 1.96·std/√n`)?两个都能用,选一个我记进卡片。
|
|
||||||
|
**`ci_low` / `ci_high` 是不是就按"结果的分位数"定?**
|
||||||
|
|
||||||
|
现状 `np.percentile(data, [2.5, 97.5])`。实测 n=2e5、E[X²]=1 给出 `[0.001, 5.0455]` ——
|
||||||
|
这是**模拟结果的分散度**(预测区间),与 `p05` / `p95` 同类,只是分位点不同。
|
||||||
|
而"均值估计的置信区间"(MC 的统计误差)是 `mean ± 1.96·std/√n` = `[0.9962, 1.0086]`,
|
||||||
|
两者差两个数量级,论文话术也完全不同:
|
||||||
|
|
||||||
|
- A(现状):写"95% 的模拟结果落在 [a, b]"(描述风险分布)
|
||||||
|
- B:改成均值 CI → 写"估计值 1.0024 ± 0.0043(95% CI)"(证明 n 够大)
|
||||||
|
- C:两者都要 → 指标要改名(如 `p025`/`p975` + `mean_ci_low`/`mean_ci_high`)
|
||||||
|
|
||||||
|
注意:`monte_carlo_probability` 里的 `ci_*` 已经是**估计量的 CI**(Wald 区间),所以这两个函数的 `ci_*`
|
||||||
|
目前**含义不同**——这正是要给它们定口径的原因之一。
|
||||||
|
|
||||||
|
### ✅ 已拍板(决策 48)
|
||||||
|
|
||||||
|
- 批量大小由实现传 `size=n`;`sampler` 必须能接住 `size`,`**kwargs` 里不要再传 `size`。
|
||||||
|
- 结果含 NaN → `success=False`(不落 metrics)。
|
||||||
|
- `std` 用 `ddof=0`。
|
||||||
|
- 出错仍统一返回 `success=False`(`try/except Exception` 保留)。
|
||||||
|
|
||||||
|
### ⏳ 另外两个小口径(你说了算)
|
||||||
|
|
||||||
|
- **`random_state` 的注解**:经验文档第 3 条写的标准是 `int | np.integer | np.random.Generator | None`,
|
||||||
|
但 `stats/bootstrap.py`、`solve/simulate.py`、`stats/inference.py` 现在都写 `int | None`(只有 `core/random.py` 是全的)——
|
||||||
|
要不要全项目对齐?(运行期 `get_rng` 收 `np.int64`,所以是注解与行为不一致,不是功能 bug)
|
||||||
|
- **`std` 的 `ddof`**:`monte_carlo` 用 `ddof=0`,`describe` 那条线用 `ddof=1`。要不要统一?
|
||||||
|
|
||||||
### 其余待办
|
### 其余待办
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
| 15 | 区间被无条件夹到 `[0, 1]`,且 `min` / `max` 参数写反 | 2026-10-03 |
|
| 15 | 区间被无条件夹到 `[0, 1]`,且 `min` / `max` 参数写反 | 2026-10-03 |
|
||||||
| 16 | 同一段入口转换抄了 N 遍,还当它是"本项目的写法" | 2026-10-03 |
|
| 16 | 同一段入口转换抄了 N 遍,还当它是"本项目的写法" | 2026-10-03 |
|
||||||
| 17 | `hasattr(f, "__name__")` 挡不住 lambda,`else` 分支是死代码 | 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))`,
|
- 以后怎么防:写类型分支时默认假设"这个值可能来自 numpy"。判断用 `isinstance(x, (int, np.integer))`,
|
||||||
取值统一 `int(x)` 归一化;并且给所有非预期类型加 `raise TypeError` 兜底——
|
取值统一 `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)` 只是普通注释
|
### 2. 以为 `# TODO(L0)` 只是普通注释
|
||||||
|
|
||||||
@@ -309,3 +316,66 @@
|
|||||||
把"没有名字"和"名字是占位符"两种情况都覆盖;③ 每个 `else`/兜底分支都问一句
|
把"没有名字"和"名字是占位符"两种情况都覆盖;③ 每个 `else`/兜底分支都问一句
|
||||||
"**这条到底测得到吗**"——走不到的分支就是 bug 伪装成的防御性代码。
|
"**这条到底测得到吗**"——走不到的分支就是 bug 伪装成的防御性代码。
|
||||||
- 首次:2026-10-03|复发:0
|
- 首次: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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -10,6 +10,11 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.stats import norm
|
||||||
|
|
||||||
|
from mcm_toolkit.core import Result, get_rng
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"markov_simulate",
|
"markov_simulate",
|
||||||
"markov_steady_state",
|
"markov_steady_state",
|
||||||
@@ -24,7 +29,7 @@ __all__ = [
|
|||||||
def monte_carlo(
|
def monte_carlo(
|
||||||
sampler: Callable[..., Any],
|
sampler: Callable[..., Any],
|
||||||
simulator: Callable[..., Any],
|
simulator: Callable[..., Any],
|
||||||
n: int = 10000,
|
n: int | np.integer = 10000,
|
||||||
random_state: int | None = None,
|
random_state: int | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@@ -33,60 +38,209 @@ def monte_carlo(
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
sampler:
|
sampler:
|
||||||
`f(rng, **kwargs) -> 一组参数`,例如从先验分布里抽参数。
|
`f(rng, size, **kwargs) -> 一批参数`。**一次就要抽够 `n` 组**,
|
||||||
|
例如 `lambda rng, size: rng.normal(0, 1, size)`。
|
||||||
|
批量大小由本函数以 `size=n` 传进去:`sampler` 必须能接住 `size`
|
||||||
|
(显式声明,或带 `**kwargs`)。**别再往 `**kwargs` 里塞 `size`**,否则重复传参。
|
||||||
simulator:
|
simulator:
|
||||||
`f(params) -> float`,例如"给定参数下的年收益"。
|
`f(params) -> 一批结果`。收到 `sampler` 的那一整批参数,
|
||||||
|
返回长度同为 `n` 的数组(标量 / 长度对不上都算契约错误)。
|
||||||
|
**内部不要写 Python 循环**。
|
||||||
n:
|
n:
|
||||||
模拟次数。**必须向量化**:`simulator` 内部不要写 Python 循环,
|
模拟次数,正整数。与 `simulator` 返回的长度必须一致。
|
||||||
否则 10^4 次能跑几分钟,10^6 次直接放弃。
|
random_state:
|
||||||
|
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
|
||||||
|
**kwargs:
|
||||||
|
原样转交给 `sampler`,例如分布参数 `mu` / `sigma`(`size` 由本函数自己传,见上)。
|
||||||
|
会一并记进 `Result.params`,便于复现。
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
`Result`:`data` 是 `n` 个结果组成的数组,
|
`Result`:`data` 是 `n` 个结果组成的数组(一律转成 float,布尔结果按 0/1 统计),
|
||||||
`metrics={"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}`。
|
`metrics={"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}`。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError:
|
||||||
|
`sampler` / `simulator` 不可调用,`n` 不是整数,或 `random_state` 类型非法。
|
||||||
|
ValueError:
|
||||||
|
`n` 不是正整数。
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
比赛里 Monte Carlo 的收敛速度是 `O(1/√n)`——
|
比赛里 Monte Carlo 的收敛速度是 `O(1/√n)` —— 想少一半误差就要 4 倍样本。
|
||||||
想少一半误差要 4 倍样本。所以论文里要给"区间随 n 收敛"的图,
|
所以论文里要给"区间随 n 收敛"的图,证明 `n` 已经够大
|
||||||
证明 `n` 已经够大(这也是 `viz.plot_convergence` 的典型用法)。
|
(这也是 `viz.plot_convergence` 的典型用法)。
|
||||||
|
|
||||||
|
结果里只要出现 **NaN** 就判失败(`success=False`):MC 的 NaN 通常来自除零、
|
||||||
|
取负数的根号或模型发散,静默统计只会得到一列 nan 指标。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import numpy as np
|
>>> import numpy as np
|
||||||
>>> r = monte_carlo(lambda rng, **k: rng.normal(0, 1), np.mean, n=1000, random_state=0)
|
>>> draw = lambda rng, size, **k: rng.normal(0, 1, size)
|
||||||
|
>>> r = monte_carlo(draw, lambda x: x ** 2, n=10000, random_state=0)
|
||||||
|
>>> bool(abs(r.metrics["mean"] - 1.0) < 0.1) # E[X²] = 1
|
||||||
|
True
|
||||||
"""
|
"""
|
||||||
# TODO(L1) ★ S 档
|
if not callable(sampler) or not callable(simulator):
|
||||||
raise NotImplementedError
|
raise TypeError("sampler 和 simulator 必须是可调用函数")
|
||||||
|
if isinstance(n, bool) or not isinstance(n, (int, np.integer)):
|
||||||
|
raise TypeError(f"n 必须是整数,你传入了 {type(n).__name__}")
|
||||||
|
if int(n) <= 0:
|
||||||
|
raise ValueError(f"n 必须是正整数,你传入了 {n}")
|
||||||
|
n = int(n)
|
||||||
|
|
||||||
|
rng = get_rng(random_state)
|
||||||
|
call_params = {"n": n, "random_state": random_state, **kwargs}
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = sampler(rng, size=n, **kwargs)
|
||||||
|
data = np.asarray(simulator(params), dtype=float).ravel()
|
||||||
|
if np.isnan(data).any():
|
||||||
|
raise ValueError("模拟结果中包含 NaN,请检查 sampler 或 simulator 的传入参数")
|
||||||
|
if len(data) != n:
|
||||||
|
raise ValueError(f"simulator 返回的结果数量({len(data)})必须与模拟次数 n({n})一致")
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"mean": float(np.mean(data)),
|
||||||
|
"std": float(np.std(data, ddof=0)),
|
||||||
|
"median": float(np.median(data)),
|
||||||
|
"p05": float(np.percentile(data, 5)),
|
||||||
|
"p95": float(np.percentile(data, 95)),
|
||||||
|
"ci_low": float(np.percentile(data, 2.5)),
|
||||||
|
"ci_high": float(np.percentile(data, 97.5)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result(
|
||||||
|
name="monte_carlo",
|
||||||
|
params=call_params,
|
||||||
|
metrics=metrics,
|
||||||
|
data=data,
|
||||||
|
success=True,
|
||||||
|
message=f"蒙特卡洛模拟成功:已完成 {n} 次模拟",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Result(
|
||||||
|
name="monte_carlo",
|
||||||
|
params=call_params,
|
||||||
|
metrics={},
|
||||||
|
data=None,
|
||||||
|
success=False,
|
||||||
|
message=f"蒙特卡洛模拟失败:{type(e).__name__} - {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def monte_carlo_probability(
|
def monte_carlo_probability(
|
||||||
event: Callable[..., bool],
|
event: Callable[..., bool],
|
||||||
sampler: Callable[..., Any],
|
sampler: Callable[..., Any],
|
||||||
n: int = 10000,
|
n: int | np.integer = 10000,
|
||||||
confidence: float = 0.95,
|
confidence: float = 0.95,
|
||||||
random_state: int | None = None,
|
random_state: int | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""估计事件发生概率,并给出区间。
|
"""估计事件发生概率,并给出区间。
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
event:
|
||||||
|
`f(params) -> 一批 bool`。吃到 `sampler` 的**整批**参数,
|
||||||
|
返回长度为 `n` 的布尔数组(写成 `z > 0` 这种逐元素判断即可,不要写 Python 循环)。
|
||||||
|
sampler:
|
||||||
|
`f(rng, size, **kwargs) -> 一批参数`,契约同 `monte_carlo`:
|
||||||
|
批量大小由本函数以 `size=n` 传入,`sampler` 必须能接住 `size`。
|
||||||
|
n:
|
||||||
|
模拟次数,正整数。与 `event` 返回的长度必须一致。
|
||||||
|
confidence:
|
||||||
|
区间置信水平,必须落在开区间 `(0, 1)` 内。
|
||||||
|
random_state:
|
||||||
|
随机种子。`None` 时跟随 `core.set_seed` 的全局流,见 `core.random.get_rng`。
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
`Result`:`metrics={"probability", "ci_low", "ci_high", "std_error"}`。
|
`Result`:`data` 是 `n` 个 0/1 结果组成的数组,
|
||||||
|
`metrics={"probability", "ci_low", "ci_high", "std_error"}`。
|
||||||
|
区间是 Wilson score 区间(`p` 贴近 0 / 1 时比 Wald 稳),并夹在 `[0, 1]` 内(比例量纲,见决策 42)。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError:
|
||||||
|
`n` 不是整数,或 `random_state` 类型非法。
|
||||||
|
ValueError:
|
||||||
|
`n` 不是正整数,或 `confidence` 不在 `(0, 1)` 内。
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
概率估计的相对误差 ≈ `1/√(n·p)`。**小概率事件(p < 1e-3)用朴素 MC 是不可行的**,
|
概率估计的相对误差 ≈ `1/√(n·p)`。**小概率事件(p < 1e-3)用朴素 MC 是不可行的**,
|
||||||
需要重要性采样或方差缩减——遇到这类题要提前意识到,别硬跑。
|
需要重要性采样或方差缩减——遇到这类题要提前意识到,别硬跑。
|
||||||
|
本函数当前的口径:`probability <= 1e-3` 时**直接判 `success=False`**(只给提示,不给区间)。
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> import numpy as np
|
>>> import numpy as np
|
||||||
>>> r = monte_carlo_probability(lambda v: v > 0, lambda rng, **k: rng.normal(0, 1), n=2000, random_state=0)
|
>>> draw = lambda rng, size, **k: rng.normal(0, 1, size)
|
||||||
|
>>> r = monte_carlo_probability(lambda z: z > 0, draw, n=2000, random_state=0)
|
||||||
>>> abs(r.metrics["probability"] - 0.5) < 0.1
|
>>> abs(r.metrics["probability"] - 0.5) < 0.1
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
# TODO(L1) ★ S 档
|
if not callable(sampler) or not callable(event):
|
||||||
raise NotImplementedError
|
raise TypeError("sampler 和 event 必须是可调用函数")
|
||||||
|
if isinstance(n, bool) or not isinstance(n, (int, np.integer)):
|
||||||
|
raise TypeError(f"n 必须是整数,你传入了 {type(n).__name__}")
|
||||||
|
if int(n) <= 0:
|
||||||
|
raise ValueError(f"n 必须是正整数,你传入了 {n}")
|
||||||
|
n = int(n)
|
||||||
|
if confidence <= 0.0 or confidence >= 1:
|
||||||
|
raise ValueError(f"confidence必须在0和1之间,而你输入了{confidence}")
|
||||||
|
|
||||||
|
rng = get_rng(random_state)
|
||||||
|
call_params = {"n": n, "random_state": random_state, "confidence": confidence}
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = sampler(rng, size=n)
|
||||||
|
data = np.asarray(event(params), dtype=float).ravel()
|
||||||
|
if np.isnan(data).any():
|
||||||
|
raise ValueError("模拟结果中包含 NaN,请检查 event 或 sampler 的传入参数")
|
||||||
|
if len(data) != n:
|
||||||
|
raise ValueError(f"event 返回的结果数量({len(data)})必须与模拟次数 n({n})一致")
|
||||||
|
p_value = float(np.mean(data))
|
||||||
|
std_error = float(np.sqrt(p_value * (1.0 - p_value) / n))
|
||||||
|
|
||||||
|
z_score = norm.ppf(1 - (1 - confidence) / 2)
|
||||||
|
center = (p_value + z_score**2 / (2 * n)) / (1 + z_score ** 2 / n)
|
||||||
|
half_width = (z_score / (1 + z_score ** 2 / n)) * np.sqrt(p_value * (1 - p_value) / n + z_score ** 2 / (4 * n ** 2))
|
||||||
|
ci_low = max(0.0, center - half_width)
|
||||||
|
ci_high = min(1.0, center + half_width)
|
||||||
|
|
||||||
|
warning_message = ""
|
||||||
|
if p_value <= 1e-3 or p_value >= 1 - 1e-3:
|
||||||
|
warning_message = "小概率事件 P < 1e-3 不建议使用monte_carlo_probability"
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
"probability": p_value,
|
||||||
|
"ci_low": float(ci_low),
|
||||||
|
"ci_high": float(ci_high),
|
||||||
|
"std_error": std_error,
|
||||||
|
}
|
||||||
|
return Result(
|
||||||
|
name="monte_carlo_probability",
|
||||||
|
params=call_params,
|
||||||
|
metrics=metrics,
|
||||||
|
data=data,
|
||||||
|
success=True,
|
||||||
|
message=(
|
||||||
|
f"蒙特卡洛模拟成功:已完成 {n} 次模拟"
|
||||||
|
+ (f";{warning_message}" if warning_message else "")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return Result(
|
||||||
|
name="monte_carlo_probability",
|
||||||
|
params=call_params,
|
||||||
|
metrics={},
|
||||||
|
data=None,
|
||||||
|
success=False,
|
||||||
|
message=f"蒙特卡洛模拟失败:{type(e).__name__} - {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def monte_carlo_integral(
|
def monte_carlo_integral(
|
||||||
|
|||||||
+191
-11
@@ -8,6 +8,11 @@ import pytest
|
|||||||
_NOT_IMPLEMENTED = "未实现:见 docs/03-roadmap.md L0/L1"
|
_NOT_IMPLEMENTED = "未实现:见 docs/03-roadmap.md L0/L1"
|
||||||
|
|
||||||
|
|
||||||
|
def _normal_batch(rng, size, **kwargs):
|
||||||
|
"""`monte_carlo` 的 sampler 契约:收下实现传来的 `size`,一次抽够。"""
|
||||||
|
return rng.normal(0, 1, size)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
def test_monte_carlo_integral_converges_to_analytic_value():
|
def test_monte_carlo_integral_converges_to_analytic_value():
|
||||||
"""∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。"""
|
"""∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。"""
|
||||||
@@ -17,30 +22,205 @@ def test_monte_carlo_integral_converges_to_analytic_value():
|
|||||||
assert abs(r.data - 1 / 3) < 0.01
|
assert abs(r.data - 1 / 3) < 0.01
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
|
||||||
def test_monte_carlo_probability_of_standard_normal():
|
def test_monte_carlo_probability_of_standard_normal():
|
||||||
"""P(Z > 0) = 0.5,n=5000 时误差应 < 0.05。"""
|
"""P(Z > 0) = 0.5,n=5000 时误差应 < 0.05。"""
|
||||||
from mcm_toolkit.solve import monte_carlo_probability
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
r = monte_carlo_probability(
|
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=5000, random_state=0)
|
||||||
lambda z: z > 0,
|
assert r.success
|
||||||
lambda rng, **kw: rng.standard_normal(),
|
|
||||||
n=5000,
|
|
||||||
random_state=0,
|
|
||||||
)
|
|
||||||
assert abs(r.metrics["probability"] - 0.5) < 0.05
|
assert abs(r.metrics["probability"] - 0.5) < 0.05
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
|
||||||
def test_monte_carlo_is_reproducible():
|
def test_monte_carlo_is_reproducible():
|
||||||
from mcm_toolkit.solve import monte_carlo_probability
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
args = (lambda z: z > 0, lambda rng, **kw: rng.standard_normal())
|
a = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=1000, random_state=9)
|
||||||
a = monte_carlo_probability(*args, n=1000, random_state=9)
|
b = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=1000, random_state=9)
|
||||||
b = monte_carlo_probability(*args, n=1000, random_state=9)
|
|
||||||
assert a.metrics["probability"] == b.metrics["probability"]
|
assert a.metrics["probability"] == b.metrics["probability"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_returns_n_results_and_documented_metrics():
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
r = monte_carlo(_normal_batch, lambda x: x**2, n=2000, random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert r.data.shape == (2000,)
|
||||||
|
assert set(r.metrics) == {"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_calls_simulator_only_once():
|
||||||
|
"""`simulator` 只被调用一次 —— 这是"必须向量化"的可执行版本。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
counter = {"calls": 0}
|
||||||
|
|
||||||
|
def simulator(params):
|
||||||
|
counter["calls"] += 1
|
||||||
|
return params
|
||||||
|
|
||||||
|
r = monte_carlo(_normal_batch, simulator, n=1000, random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert counter["calls"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_data_is_reproducible_with_same_seed():
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
a = monte_carlo(_normal_batch, lambda x: x, n=2000, random_state=11)
|
||||||
|
b = monte_carlo(_normal_batch, lambda x: x, n=2000, random_state=11)
|
||||||
|
assert np.array_equal(a.data, b.data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_follows_set_seed_when_random_state_omitted():
|
||||||
|
from mcm_toolkit.core import set_seed
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
set_seed(5)
|
||||||
|
a = monte_carlo(_normal_batch, lambda x: x, n=500)
|
||||||
|
set_seed(5)
|
||||||
|
b = monte_carlo(_normal_batch, lambda x: x, n=500)
|
||||||
|
assert np.array_equal(a.data, b.data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_estimates_known_expectation():
|
||||||
|
"""E[X²] = 1(X ~ N(0, 1));n = 2e5 时误差应 < 0.02。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
r = monte_carlo(_normal_batch, lambda x: x**2, n=200_000, random_state=0)
|
||||||
|
assert abs(r.metrics["mean"] - 1.0) < 0.02
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_docstring_example_runs():
|
||||||
|
"""docstring 的 Examples 必须真的能跑通(此前那一版是坏的)。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
draw = lambda rng, size, **k: rng.normal(0, 1, size) # noqa: E731
|
||||||
|
r = monte_carlo(draw, lambda x: x**2, n=10000, random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert abs(r.metrics["mean"] - 1.0) < 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_accepts_numpy_integer_n():
|
||||||
|
"""§4.7:`np.int64` 不是 `int` 的子类,不能漏。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
r = monte_carlo(_normal_batch, lambda x: x, n=np.int64(100), random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert r.data.shape == (100,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_counts_boolean_outcomes():
|
||||||
|
"""布尔结果按 0/1 统计 —— 事件概率是 MC 最常见的用法。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
r = monte_carlo(
|
||||||
|
lambda rng, size, **k: rng.normal(0, 1, size) > 0,
|
||||||
|
lambda x: x,
|
||||||
|
n=5000,
|
||||||
|
random_state=0,
|
||||||
|
)
|
||||||
|
assert r.success
|
||||||
|
assert r.data.dtype == float
|
||||||
|
assert abs(r.metrics["mean"] - 0.5) < 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_rejects_nan_results():
|
||||||
|
"""结果里出现 NaN 就判失败,不静默给出一列 nan 指标。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
r = monte_carlo(
|
||||||
|
lambda rng, size, **k: np.r_[np.zeros(size - 1), np.nan],
|
||||||
|
lambda x: x,
|
||||||
|
n=10,
|
||||||
|
random_state=0,
|
||||||
|
)
|
||||||
|
assert not r.success
|
||||||
|
assert "NaN" in r.message
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_rejects_bad_arguments():
|
||||||
|
from mcm_toolkit.solve import monte_carlo
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
monte_carlo(123, lambda x: x) # type: ignore[arg-type] # 就是要传错
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
monte_carlo(_normal_batch, 123) # type: ignore[arg-type] # 就是要传错
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
monte_carlo(_normal_batch, lambda x: x, n=True)
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
monte_carlo(_normal_batch, lambda x: x, n=10.0) # type: ignore[arg-type] # 就是要传错
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
monte_carlo(_normal_batch, lambda x: x, n=0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_returns_documented_metrics():
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=20000, random_state=0)
|
||||||
|
assert r.success
|
||||||
|
assert set(r.metrics) == {"probability", "ci_low", "ci_high", "std_error"}
|
||||||
|
assert r.data.shape == (20000,)
|
||||||
|
assert 0.0 <= r.metrics["ci_low"] <= r.metrics["probability"]
|
||||||
|
assert r.metrics["probability"] <= r.metrics["ci_high"] <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_std_error_matches_binomial_formula():
|
||||||
|
"""`std_error` 就是比例的标准误 √(p(1-p)/n)。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
n = 20000
|
||||||
|
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||||||
|
p = r.metrics["probability"]
|
||||||
|
assert np.isclose(r.metrics["std_error"], np.sqrt(p * (1 - p) / n))
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_ci_narrows_with_n():
|
||||||
|
"""区间宽度随 n 增大而收窄(O(1/√n))。"""
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
def width(n):
|
||||||
|
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||||||
|
return r.metrics["ci_high"] - r.metrics["ci_low"]
|
||||||
|
|
||||||
|
assert width(100_000) < width(1_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_higher_confidence_is_wider():
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
def width(confidence):
|
||||||
|
r = monte_carlo_probability(
|
||||||
|
lambda z: z > 0, _normal_batch, n=5000, random_state=0, confidence=confidence
|
||||||
|
)
|
||||||
|
return r.metrics["ci_high"] - r.metrics["ci_low"]
|
||||||
|
|
||||||
|
assert width(0.99) > width(0.90)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_ci_matches_statsmodels_wilson():
|
||||||
|
"""区间必须对得上独立实现(statsmodels 的 wilson)—— `^` 写成异或那次就靠它拦住。"""
|
||||||
|
from statsmodels.stats.proportion import proportion_confint
|
||||||
|
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
n = 5000
|
||||||
|
r = monte_carlo_probability(lambda z: z > 0, _normal_batch, n=n, random_state=0)
|
||||||
|
count = int(r.data.sum())
|
||||||
|
lo, hi = proportion_confint(count, n, alpha=0.05, method="wilson")
|
||||||
|
assert np.isclose(r.metrics["ci_low"], lo)
|
||||||
|
assert np.isclose(r.metrics["ci_high"], hi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_monte_carlo_probability_rejects_bad_confidence():
|
||||||
|
from mcm_toolkit.solve import monte_carlo_probability
|
||||||
|
|
||||||
|
for bad in (0.0, 1.0, -0.5, 1.5):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
monte_carlo_probability(lambda z: z > 0, _normal_batch, n=100, confidence=bad)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
def test_markov_steady_state_two_state_chain():
|
def test_markov_steady_state_two_state_chain():
|
||||||
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
||||||
|
|||||||
Reference in new issue
Block a user