Files
MCM-ICM_Toolkit/CHANGELOG.md
T
guzhujushi 9cb295a362 fix(solve): 同步第三轮入口口径(docstring + 测试 + lint)
用户第三轮改了 5 处口径:非负校验带容差、initial 收 np.integer、paths 赋值挪进 try、
metrics 换成 terminal_distribution、mm1 放开 lambda=0 并收 numpy 标量。三处算法本体仍正确
(探针复测:终点分布 {0: 0.6644, 1: 0.3356}、lambda=0 给 L=Lq=Wq=0 且 W=1/mu、data is metrics 已为 False)。

Changed(Codex 只做同步,未改口径)
- docstring:markov_simulate 的 Returns 与"遗留"段、markov_steady_state 的 Raises
  (TypeError / ValueError 分工)、mm1_metrics 的 Parameters / Returns / Raises。
- tests/test_solve.py:同步 5 条受影响的测试,新增 2 条(terminal_distribution 只列出现过的状态、
  lambda=0 的空系统)。test_solve.py 64 条。
- ruff:B905(zip 缺 strict)+ 2 条空白 -> 0;rng 之后的双空行 -> 1。
- STATUS.md:决策 56 / 57 / 58 更新到"只剩 5 条小口径";「下一步」第八轮;
  新增「第八轮备忘:models 与 decision 各是干什么的」(函数清单 / 使用顺序 / 建议开工顺序)。
- CHANGELOG 与 docs/dev/personal-pitfalls.md 易错点 30(口径一改,docstring / 测试 / STATUS 同时过期)。

校验:pytest 204 passed / 28 skipped(232 收集);ruff 0;pyright 0
2026-10-10 09:31:30 +08:00

620 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 更新日志
格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
版本号参考 [语义化版本](https://semver.org/lang/zh-CN/)。
## [Unreleased]
### Added
- 仓库骨架:`src/mcm_toolkit/` 7 个包(core / data / stats / models / solve / decision / viz)
- 顶层 `__init__.py` 只导出 7 个子包 + 8 个快捷函数,`__all__` 即公开 API 清单
- `core.Result`:唯一的结果对象(替代原建议里的 5 个 Result 类)
- `docs/01-suggestion-review.md`:对 ChatGPT 建库建议的审查与精简(14 包 → 7 包,~250 函数 → ~40)
- `docs/02-architecture.md`:包边界、`Result` / RNG / `fit-predict-score` 协议、依赖分层
- `docs/03-roadmap.md`:L0/L1/L2 分级 + **永久不做清单**
- `docs/04-environment.md`:conda / venv 配置与三个已踩的坑
- `docs/dev/function-card-template.md`:S 档算法的函数卡片模板
- `.venv`(基于 Anaconda base,`--system-site-packages`)+ 可编辑安装 —— 10.5 MB,离线可用
- conda 环境 `mcm-toolkit`(`D:\anaconda\envs\mcm-toolkit`)—— Python 3.13.15,含 SALib / pymoo / pyright
- `scripts/setup-conda.ps1` / `scripts/setup-venv.ps1` / `scripts/doctor.ps1`
- `environment.yml`:需联网时的精确复现方案
- `pyproject.toml`:核心依赖只 4 个,其余走 extras
- `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、
非法类型必须抛 `TypeError`。配套在 `docs/03-roadmap.md` 的开发纪律里加了指针。
- `docs/dev/personal-pitfalls.md`:个人易错点清单,**只追加、不通读**
- `data.clean`:四个函数落地(**由用户本人实现**)——`clean_missing`(L0 第 4 项)、
`impute_missing` / `detect_outliers` / `winsorize`(原属 L1,顺手写了)。
- `tests/test_data.py`:新增 13 条 clean 行为测试,覆盖 `clean_missing` 的阈值语义与轴向、
`impute_missing` 的 mean/ffill/mode 与全 NaN 列、`winsorize` 的分位点与异常类型。
- `tests/test_data.py`:另加 7 条 `xfail`,把**已知但未修的契约**钉在测试报告里(见 Notes)。
- `docs/algorithms/detect_outliers.md`:**第一张 S 档函数卡片**(roadmap §3.1 要求
约 15 张,这是第 1 张)。除模板九节外,额外记了三条容易踩的判断:为什么不返回 `Result`、
非数值列恒 `False` 的两条备选路各错在哪、以及 `ddof` 口径与 `stats.zscore` 不一致。
- `examples/01_clean_and_describe.py`:`load_table` / `save_table` / `clean_missing` /
`impute_missing` / `detect_outliers` / `winsorize` 六个函数的最小用法,实测跑通(exit 0);
末尾留 `TODO(L0)` 等 `stats.describe`。
- `tests/test_data.py`:`data` 的 transform 段 **18 条用例** —— `normalize` 的三种 method、
`feature_range` 区间与两条校验、常量列不出 nan、docstring 那个 Examples 的精确值、按列语义;
`standardize` 的按列与 `ddof`(用解析关系钉住:`ddof=1` 时结果的 std = `sqrt((n-1)/n)`);
`log_transform` 的定义域平移;`one_hot_encode` 的自动识别、`drop_first`、列不存在报错。
其中两条是把原先挂着的 `skip` 转为实跑(见坑 9 / 个人易错点 9)。
- `tests/test_stats.py`:新写 **20 条**用例 —— `describe` 的方向约定(行 = 指标、列 = 变量)、
`missing` / `missing_ratio` / `skewness` / `kurtosis` 四行**不能是 NaN**、非数值列自动跳过、
接受 ndarray / list / Series、加权均值与加权标准差对照手算公式、5 种非法 `weights`;
`zscore` 的手算对照 / 按列 / 常量列 / 非法类型;`coefficient_of_variation` 的手算对照、
单列 DataFrame、均值为 0 抛 `ValueError`、空数组与二维输入。
- `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出(实测 exit 0)。
### Added
- `stats.correlation_pvalues` / `stats.correlation_table`:`correlation_matrix` 的伴随函数
(**方案 A**,用户 2026-10-03 拍板,见决策 41)——
`correlation_matrix` 只返回**数值**矩阵;`correlation_pvalues` 给数值 p 值矩阵、
`correlation_table` 给"系数 + 显著性星标"的论文表格。
三者共用 `_check_corr_args` 入参守卫与**成对删缺失**口径,`correlation_table` 在最外层调另外两个。
### Changed
- `core.get_rng` / `core.set_seed`:实现(L0 第 2 项)。`get_rng` 收 `int` / `np.integer` /
`Generator` / `None`,其余类型抛 `TypeError`;无参调用复用同一个会推进的共享生成器。
`set_seed` 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。
- `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。
`repr` 紧凑单行且不打印 `data` 内容;`summary()` 输出可直接贴进论文的 Markdown 表格。
- `data.load_table`:实现(L0 第 3 项)。按扩展名分派 `.csv` / `.xlsx` / `.xls`;
`.csv` 默认做编码探测(utf-8 → utf-8-sig → gbk → gb18030),显式传 `encoding=` 时不再探测;
`na_values` 默认吃下空串 / `NA` / `N/A` / `#N/A` / `null` / `-`。
- `data.save_table`:实现(原属 L1,顺手落地)。按扩展名分派,父目录不存在时自动创建,
返回实际写入的 `Path`。
- `data.load_table` / `data.save_table`:docstring 补 `Parameters` / `Raises`
(`docs/02-architecture.md` §4.7 对每个公开函数的硬要求)。
- `data.load_table` 的返回注解 `Any` → `pd.DataFrame`,与 docstring 的承诺对齐。
- `data.clean`:docstring 按 `docs/02-architecture.md` §4.7 补全 Parameters / Returns / Raises
(Codex 代写,与 io.py 同一套 numpydoc 格式)。
- `data.clean`:ruff 从 6 条清到 1 条——导入分组(I001)、elif 链改 if 加末尾无条件 return
(RET505 / RET503)、去掉多余 f 前缀(F541)、删掉只会被覆盖的 mask 初始化(死代码)、
去掉行尾空白(W293)。
- `pyright` 报"无法解析导入 numpy / pandas"(17 errors)的原因:分析器会去 PATH 上找 python,
本机 PATH 上只有 Codex 运行时的 3.14 shim(无科学栈),且 pyright 不认
`pyvenv.cfg` 的 `include-system-site-packages`。
改为 `[tool.pyright]` 指定 `venvPath = "."` / `venv = ".venv"` /
`pythonVersion = "3.13"`,并让 `scripts/setup-venv.ps1` 往 `.venv` 里写一个
指向 base site-packages 的 `.pth`。pyright 17 errors → **0**。
详见 `docs/04-environment.md` 坑 7。
- `tests/test_data.py`:`df["b"].isna().all()` 的类型是 `Series | bool`,
直接当断言条件 pyright 会报"条件操作数无效",外面套 `bool()`;
两条故意传错类型的用例加 `# type: ignore[arg-type]`。
- `data.save_table` 的后缀校验排在 `path.parent.mkdir(...)` **之后**,传 `.txt` 会先留下一个
空目录再抛 `ValueError`。改为**先校验、后副作用**,并加回归测试。
- `data/io.py` 里 `__all__` 与下一个 `def` 之间只有 1 个空行(PEP 8 要 2 个)。
- `tests/test_data.py` 顶上的**模块级** `pytestmark = pytest.mark.skip(...)` 会连带跳过同文件里
已经写好的函数,导致 `load_table` 零覆盖。改为逐条 skip 未实现的算法,并补 8 条 io 测试
(编码探测、NA 归一、后缀校验、缺文件报错、往返读写、类型校验)。测试数 31 → **39 passed**。
- conda base 里 zstandard 的 `*.conda_trash` 残骸目录被 Python 当成 namespace package 导入,
使 `.venv`(= base)的 `pd.read_csv` 抛 `ImportError: Can't determine version for zstandard`。
已把残骸目录改名禁用(**可逆**,未删除),详见 `docs/04-environment.md` 坑 6。
- `data.clean` 第二轮审查(2026-10-02),Codex 直接修掉 4 处**轻度**问题:
`winsorize(Series)` 因 `select_dtypes` 在 `Series` 上不存在而直接崩(Series 分支提到最前,
并加数值 dtype 守卫);`winsorize` 的 `data.quantile()` 改为只在数值列上算
(文本列不再崩,与已有的 `numeric_cols` 判断对齐);删掉 `detect_outliers` 里已成空操作的
`df = pd.DataFrame(data)`(数据在更早处就转过了,顺手去掉 `df` 这个别名);
清掉行尾空白(W293)、函数体内的连续空行,并把 `clean_missing` 的嵌套 `if` 合并
(SIM102,ruff 最后 1 条)。ruff `1 error` → **`All checks passed`**,pyright 仍 0。
- `tests/test_data.py`:5 条 `xfail` 经 XPASS 信号确认已修好,**摘掉 mark 转正式用例**
(`detect_outliers` 的 4 条 + `winsorize` 混合类型 1 条)。测试 **53 passed / 7 xfailed →
58 passed / 2 xfailed**。
- `CHANGELOG.md`:[Unreleased] 里有一句被重复追加撑坏
("…删掉它即全绿。:分析器会去 PATH 上找 python,"),已修。
- `data.detect_outliers` 收尾(2026-10-02,**由用户本人实现**):统计量(`quantile` /
`mean` / `std`)与掩码写入**都**收窄到数值列,含文本列的赛题表不再崩;
非数值列在掩码里恒 `False`(用户本人定的语义,见决策 32)。docstring `Notes` 补上这条语义。
- `data.detect_outliers` 里 `data=data.copy()` 是死代码:下面的 `to_frame()` / `pd.DataFrame()`
本来就返回新对象,函数全程也没原地改过 `data`,白白复制一份数据;而且它排在类型分派
**之前**,会把错误入参的报错从 `ValueError` 变成 `AttributeError`。已删。
- `data.detect_outliers`:删掉那行死代码后 pyright 反而报了 1 个真错 ——
`df[Index]` 被类型存根当成 `Series`,`.std()` 于是被判成标量、`std.replace(...)` 非法。
在取列子表处用 `cast(pd.DataFrame, ...)` 按运行时真值收窄。详见 `docs/04-environment.md` 坑 8。
- `tests/test_data.py`:`test_detect_outliers_survives_text_columns` 摘掉 xfail 并加强断言
(文本列必须恒 `False`);新增 `test_detect_outliers_all_text_frame_returns_all_false`
钉住"整表没有数值列"这条边界。测试 **58 passed / 2 xfailed → 60 passed / 1 xfailed**。
- `docs/dev/personal-pitfalls.md`:追加第 8 条(限制掩码 ≠ 限制统计量)。
- `data.impute_missing` 的 mode 分支改为**返回新表**(原先 `data[col] = ...` 会就地改调用方的
DataFrame,另外四支都返回新对象)。最后一条 `xfail` 随之转正,**测试里 `xfail` 清零**。
- `data.detect_outliers` 补**入参类型校验**(§4.7:非法类型抛 `TypeError`,不静默兜底):
接受 `DataFrame` / `Series` / `list` / `tuple` / `ndarray`。`tuple` 同时补进 `is_ld_array`,
与 `list` 同等对待;`dict` 不再被接受(以前靠 `pd.DataFrame()` 碰巧能过,见决策 33)。
- `tests/test_data.py`:摘掉最后一条 `xfail`(mode 就地改)并加强断言;新增 3 条
(非法类型抛 `TypeError`、元组按一维数组型处理、全 NaN 列与单元素列不抛异常)。
测试 **60 passed / 1 xfailed → 64 passed / 0 xfailed**。
- `data.transform` 四个函数实现(**由用户本人写**):`normalize`(minmax / vector / max,均按列)、
`standardize`(按列,`ddof` 真的传下去)、`log_transform`(`offset=None` 时自动取 `1 - min(x)`)、
`one_hot_encode`。`data/` 下的 L0 至此结清。
- `data.transform`:Codex 清掉 5 条 ruff —— RET503(`normalize` 末尾缺无条件 `return`)、
F541(无占位符的 f-string)、RET504(多余的 `result` 中转)、RET505(`return` 后的 `elif`)、
SIM108(`log_transform` 的 if/else 改三元)。
- `data.transform.normalize`:`supported_method` 与 `feature_range` 的校验提到 `np.asarray` **之前**
(§4.7 的"守卫集中在开头");并修掉说反的报错文案 —— 条件是要求下界 < 上界,
原文却写"上界必须小于下界",且 `TypeError` 那条用了无占位符 f-string。
- `data.transform.one_hot_encode`:自动识别类别列时显式加上 `'str'` / `'string'`。
原来只写 `['object', 'category']`,在 pandas 3 上是**已弃用路径**(每次调用发 `Pandas4Warning`),
未来会变成一列都选不中的**静默错误**。详见坑 9。
- 测试计数:**64 passed → 82 passed**(新增 18 条,另两条 skip 转为实跑),skip 42 → 40。
- `data.transform`:修掉上一版引入的 **3 处真 bug**(**Codex 代修,用户授权"报错顺手修掉"**)。
症状是 7 条测试变红 + pyright 在 `transform.py:105` / `:133` 报 `reportArgumentType`。
**这不是编辑器误报** —— pyright CLI 与 `pytest` 都能复现,所以**一行 `# type: ignore` 都没加**:
- 输入白名单漏了 `pd.DataFrame`:`standardize(df)` / `log_transform(df)` 抛 `TypeError`,
函数体里的 `is_df` 分支全是**死代码**。白名单统一成模块级 `_NUMERIC_TYPES` 并移到 `np.asarray` 之前。
- `normalize` 的三个 method 分支**互相覆盖**:`minmax` / `vector` 算完的结果被末尾无条件的
`x / max` 冲掉,三种 method 实际都只走 max(`test_normalize_*` 5 条因此变红)。改成 `if / elif / else`。
- `one_hot_encode` 把 `get_dummies` 的结果按 `data.columns` 重包一层 `DataFrame`:
哑变量列名(`city_A`)与原列名(`city`)对不上 → 编码列被**整列丢掉**。改为直接返回 `get_dummies` 结果。
- `log_transform` 的 `offset` 守卫条件**写反**(`offset > 0` 就抛错,恰好把唯一合法用例拒掉),
改为校验定义域前提 `min(x) + offset > 0`;`offset=0` 对全正数据合法(见决策 35)。
- `data.transform` 四个函数的 docstring 按 `docs/02-architecture.md` §4.7 补齐
`Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` / `clean.py` 同一套 numpydoc 格式),
并把常量列行为(决策 34)写进 `Notes`。
- `data.transform`:口径定稿后 ruff 剩的 4 条 RET505(`return` 后的 `elif`)一并清掉 ——
`All checks passed`;pyright **2 errors → 0 errors**;测试 **7 failed / 75 passed → 82 passed / 40 skipped**。
- `stats.describe`:实现落地(**由用户本人写**,Codex 审查 + 代修),并修掉一个**静默 bug** ——
`df_num.describe().T` 把表转反了(行 = 变量、列 = 指标),而 docstring 与
`test_describe_reports_expected_keys` 定的是"行 = 指标、列 = 变量"。
转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名**零交集**,pandas 不报错,
直接把这四行(`missing` / `missing_ratio` / `skewness` / `kurtosis`)**整行填成 NaN**;
`weights` 分支则报 `cannot set a row with mismatched columns`。去掉 `.T` 一处修好三件事。
另补 `median` 行(pandas 只有 `50%`,测试契约定的是 `median`)、入参放宽到
`list` / `tuple` / `ndarray`(`00_quickstart.py` 传的就是 ndarray)、
`weights` 校验补齐(维度 / 长度 / NaN / 负值 / 和为 0,原先和为 0 时裸抛 `ZeroDivisionError`)、
docstring 按 §4.7 补 `Parameters` / `Returns` / `Raises`。
- `stats.zscore`:修 RET505;docstring 按 §4.7 补齐;DataFrame 取列处按坑 8 用 `cast` 收窄
(pyright 的 `DataFrame | Series` 重载)。
- `stats.coefficient_of_variation`:均值为 0 由 `TypeError` 改为 `ValueError`
(那是**取值**问题不是类型问题,§4.7);空数组提前抛 `ValueError`(原先静默返回 nan + RuntimeWarning);
报错文案里"异变系数"的错别字与 "N 维数组" 改为 "N 列"。
- `stats.coefficient_of_variation`:分母由 `mean` 改为 `|mean|`(**用户本人改的**,决策 38)——
均值为负时 `σ/μ` 会给出负值,容易被读成"波动更小";现在 CV 恒非负。docstring 的 `Notes` 同步,
并加一条测试钉住"均值为负时 CV 仍为正"。
- `tests/test_stats.py`:去掉**模块级** `pytestmark = pytest.mark.skip` —— 它把该文件 6 条测试
**全部**跳过(包括已经实现好的 `describe`),正是决策 26 要禁的写法:实现零覆盖而测试全绿。
改为逐条 skip 未实现的 5 条。
- 测试计数:**82 passed / 40 skipped → 103 passed / 39 skipped**(新增 21 条);
ruff 7 error → **All checks passed**;pyright **1 error → 0**。
- `stats.correlation_matrix`:实现落地(**由用户本人写**,Codex 审查 + 代修),修掉一个**硬 bug** ——
`if numeric_cols == 0:` 拿 `Index` 和标量比,得到的是布尔**数组**:0 列时静默不报错、
1 列时碰巧能跑、**≥2 列直接 `ValueError: The truth value of an array ... is ambiguous`** ——
而"≥2 个数值列"正是最常见的情况,等于整个函数不可用。改成 `len(numeric_cols) == 0`,
并加回归用例(见易错点 13)。
- `stats.correlation_matrix`:pyright 的两处报错**都不是运行时问题,也都没挂 `# type: ignore`** ——
`data[numeric_cols]` 是坑 8 的 `DataFrame | Series` 重载(`cast` 收窄);
`_, p = stats.pearsonr(...)` 解包后会退化成 `object`(scipy 的返回值是 `_make_tuple_bunch`
在运行时拼的类,存根里看不到 `pvalue`)。新增 `_pvalue(result: Any) -> float` 小助手统一取 p 值,
后面 `t_test` / `anova` / `chi_square_test` 可复用(见 `docs/04-environment.md` 坑 10、决策 40)。
- `stats.correlation_matrix`:`method` 校验提到最前(守卫集中开头);`add_stars` 的 `elif` 链改 `if`
(RET505);清掉 3 条 F541 与 1 条 I001(import 未排序)—— ruff 归零。
- `tests/test_stats.py`:`test_correlation_matrix_diagonal_is_one` 由 `skip` 改成 `xfail(strict=False)`
(决策 30)—— 契约要**数值**矩阵,实现返回带星标的字符串表,口径待拍板;
另新增 2 条(多列数值表能跑通、非法输入报错)。
- 测试计数:**103 passed / 39 skipped → 105 passed / 38 skipped / 1 xfail**。
- `stats.correlation_matrix`:按**方案 A** 拆开 —— 只返回**数值**矩阵,不再返回带星标的字符串表。
字符串表做不了后续计算(挑高相关对、喂聚类 / PCA、画热力图),而反过来随时能格式化;
一旦返回字符串,数值信息不可逆地丢失(见易错点 14)。
- `stats.correlation_pvalues`:**对角线由 `0.0` 改成 `NaN`** —— 写成 0 会被下游读成"极显著",
而"自己和自己相关"根本没有 p 值可言。
- `stats.correlation_pvalues`:p 值与 r 现在用**同一批观测** —— 先成对 `dropna()` 再算,
并同样尊重 `min_periods`(不足给 NaN)。此前 `corr` 是成对删缺失、`pearsonr` 不删,
两者基于不同的样本量,r 和 p 对不上。
- `stats.correlation_pvalues`:只算上三角再镜像到对称位置(成对变量的 p 天然对称),
省掉一半重复计算。
- `tests/test_stats.py`:`test_correlation_matrix_diagonal_is_one` 摘掉 `xfail` 转正;
新增 8 条(数值性 + 对称性、非数值列跳过、p 值对照**解析式** `t = r·sqrt((n-2)/(1-r²))`、
对角线 NaN、成对删缺失口径、`min_periods`、对角线无星标、表格与数值输出一致)。
- 测试计数:**105 passed / 38 skipped / 1 xfail → 114 passed / 38 skipped / 0 xfail**。
### Notes
- `detect_outliers` 原有的三个 bug 加一处设计空白**已全部收口**:`threshold` 范围校验挪进
`clean_missing`、`is_series` / `is_ld_array` 提到类型转换**之前**、统计量与掩码都限制在数值列、
非数值列恒 `False`(决策 32)、入参类型校验补上(决策 33)。
S 档卡片与 `examples/` 用法也已落地 —— `detect_outliers` 这条线**结清**。
- `transform.py` 的 **4 个口径已全部收口**:① 三个函数共用 `_NUMERIC_TYPES` 白名单(含 `DataFrame`);
② 全部接受 `DataFrame` 且保持"DataFrame 进 DataFrame 出";③ 显式 `offset` 按定义域校验
(`min(x) + offset > 0`,见决策 35,**待用户确认**);④ 常量列行为写进 docstring 的 `Notes`;
另外 `Parameters` / `Returns` / `Raises` 已按 §4.7 补齐。
- 新增易错点第 10 条:**把真报错当成"编辑器误报"** —— 红线先跑 CLI 与 `pytest` 再判断(`docs/dev/personal-pitfalls.md`)。
- `stats` 这条线开工了:`describe` 落地,填掉 `examples/01_clean_and_describe.py` 的 `TODO(L0)`;
`correlation_matrix` 功能上也能跑了(硬 bug 已修)。
- `correlation_matrix` 的返回契约已按**方案 A** 定案(决策 41):数值归 `correlation_matrix`、
p 值归 `correlation_pvalues`、星标表归 `correlation_table`。对角线不带星、p 与 r 同批观测两条也一并修掉。
- `correlation_matrix` 仍只收 `DataFrame`(`describe` / `zscore` / `transform` 都收 ndarray)——
要不要放开由用户决定,记在 `STATUS.md`「下一步」第 5 条。
- 三处口径已拍板(2026-10-03):`log_transform` 的 `offset` 维持现状(决策 35);
`describe` / `zscore` **默认丢掉**非数值列、与 `detect_outliers` 的"保留列"刻意不统一(决策 37);
变异系数改用 `σ/|μ|`(决策 38)。另立决策 39 明确分工:语义口径用户拍板、工程事务 Codex 顺手补。
- 已实现函数的 `examples/` 最小用法仍不全:`00_quickstart.py` 要等 `bootstrap_ci` / `viz`,
`Result` 的用法届时并进去。
- 已知缺口:
- 除 `core.Result`、`core.get_rng`、`core.set_seed`、`data.load_table`、`data.save_table` 外,其余函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。
这是刻意的:先定接口,再按 `docs/03-roadmap.md` 的 L0 → L1 顺序填实现。
- 已知缺口:`SALib`(Sobol/Morris)、`pymoo`、`pyright` 已装进 conda 环境 `mcm-toolkit`,`.venv` 里没有。
### Fixed
- `stats.confidence_interval`:修掉一个**会给出错误数字**的硬 bug —— 原码返回
`(float(min(0.0, lower)), float(max(upper, 1.0)))`,`min` / `max` 的**参数写反**(下界被钉死成 `0.0`),
而且**无条件**把区间夹到 `[0, 1]`。实测 `confidence_interval(rng.normal(170, 8, 50), method="t")`
的下界是 `0.0`(正确是 `171.0 ± 2.1`);`confidence=1.5` 被夹成 `(0.0, nan)` 而不报错。
现在**只有 `proportion` 夹到 `[0, 1]`**(决策 42),`normal` / `t` 直接返回 `mean ± z|t·se`;
`method` 非法改抛 `ValueError`、`confidence` 按 `(0, 1)` 开区间校验、样本量 < 2 抛 `ValueError`。
- `stats.confidence_interval` 的入参曾只收 `DataFrame`,而 docstring 的 Examples 传 `list`、
L0 用例传 `ndarray` —— **两条都会 `TypeError`**。现放宽到 `list` / `tuple` / `ndarray` / `Series` / `DataFrame`。
### Added
- `stats.inference._statistic(result: Any) -> float` 助手,与 `_pvalue` 成对(决策 43)。
scipy 的 `TtestResult` 与 `PearsonRResult` 同源,都是 `_make_tuple_bunch` 运行时拼的类,
存根里 `.statistic` / `.pvalue` 都看不到。用户报告的 `t_test` 「307 / 308 两行报错」即此,
**不是真 bug**(探针实测 `TtestResult(statistic=3.873, pvalue=0.0305)`,运行时完全正常);
换成助手后 pyright 归零,**一行 `# type: ignore` 都没挂**。见 `docs/04-environment.md` 坑 10。
- `stats.t_test`(L1 提前落地):docstring 补齐 `Parameters` / `Returns` / `Raises` / `Notes`;
`_clean_data` 改用 `isinstance(data, pd.DataFrame)` 收窄(坑 8 的 `cast`);`alpha` 开区间校验;
`df` 按 Welch–Satterthwaite / pooled / paired 分别给。
- `tests/test_stats.py`:**+10 条(36 → 46)**,含把原先挂着 `skip` 的
`test_confidence_interval_matches_scipy` 转正。对照一律用解析式(z 公式、Welch `df` 公式、
`t = (mean − value)/(s/√n)`),不拿实现对自己。
- `docs/dev/personal-pitfalls.md`:新增第 15 条「区间被无条件夹到 `[0, 1]`,且 `min` / `max` 参数写反」;
并把**停在 6 号就没人管**的索引表补齐到 15(只补索引,正文一个字没动)。
- `docs/04-environment.md`:坑 10 扩写成 `pvalue` / `statistic` 两条;顺手修掉残留的
`[FENCE]` 代码围栏占位符与一处重复的 `---`。
### Changed
- `STATUS.md`:L0 记为 **9/12**(另 L1 的 `confidence_interval` / `t_test` 提前落地);
校正测试数为 **162 条 / 125 passed / 37 skipped**;新增决策 42 / 43;「下一步」列出
**`t_test` 待用户拍板的 4 个口径**(单样本结论文案、`paired` 的 NaN 对齐、`value` 被静默忽略、
`DataFrame` 摊平多列)—— 按决策 39,Codex 只列选项、不代选。
### Added
- `core.validate.as_array`:**所有收数值函数的统一入口**(决策 44)。
一次做完"类型白名单 + 转 `ndarray` + 报告原来是什么容器",返回 `(arr, kind)`,
`kind` 是 `"frame"` / `"series"` / `"array"`。**还原不代做** —— 调用方按 `kind` 手写,
因为 `describe` / `correlation_*` / `confidence_interval` / `t_test` 的输出天生与输入不同形。
入口三条硬规矩:`numeric_only` 按"输出是否与输入同形"选、**默认绝不摊平**(要摊平显式 `ndim=1`)、
**不删缺失值**(删缺失是各函数的语义)。见 `docs/02-architecture.md` §4.8。
### Changed
- **入口去重**:`normalize` / `standardize` / `log_transform` / `zscore` / `confidence_interval` /
`t_test` 六处各自手抄的 `isinstance(x, pd.DataFrame)` / `select_dtypes` / `np.asarray` 全部删掉,
各只剩 1 行 `as_array(...)`;`data/transform.py` 的 `_NUMERIC_TYPES` 白名单连同 3 份副本一起删除。
原来这类判断散在 **5 个文件 31 处**。
- `data.transform` 的 `normalize` / `standardize` / `log_transform` 走 `numeric_only=False`:
它们的输出必须与输入**同形**,跳过非数值列会让返回的表少列、对不上原表。
- `zscore` / `normalize` / `standardize` / `log_transform` 的 Series 分支**补回 `name`** ——
此前 `pd.Series(result, index=...)` 会把 `name` 丢掉,管道里 `concat` 会变成无名列。
- 测试:`tests/test_core.py` 新增 `TestAsArray` **8 条**(五种容器的 `kind`、只取数值列、
`numeric_only=False` 遇文本报错、默认不摊平、`ndim=2` 只校验不重塑、白名单外一律 `TypeError`、
**保留 NaN**、`dtype=None` 保持原 dtype);`tests/test_stats.py` 新增 1 条(`zscore` 保住 Series 的
`index` 与 `name`)。全量 **171 条 / 134 passed / 37 skipped**。
- `docs/02-architecture.md`:新增 §4.8「入口只有一个:`core.validate.as_array`」;
目录树里 `validate.py` 补上 `as_array`。
- `docs/dev/personal-pitfalls.md`:新增第 16 条「同一段入口转换抄了 N 遍,还当它是约定」,索引补齐到 16。
### Fixed
- `normalize(含文本列的表)` 等"容器对、内容转不成 float"的情况,错误类型由 numpy 的 `ValueError`
统一收成 `TypeError`(决策 45,**Codex 代选、待用户确认**)—— 与 `t_test` / `confidence_interval`
已经写进 docstring 的口径一致。
### Added
- `stats.bootstrap_ci`(**L0 第 10 项,S 档**,实现由用户本人写):百分位法 bootstrap 置信区间,
`statistic` 可以是任意 `f(sample) -> float`(默认 `np.mean`)。返回 `Result`,
`params = {estimate, low, high, confidence, n_resamples}`、`metrics.statistic_name`。
重采样**按块处理**(`batch_size=1000`),不落地 `B × n` 整矩阵。
- `docs/algorithms/bootstrap_ci.md`:**第 2 张 S 档函数卡片**。除模板九节外,记了一条非平凡性质 ——
**统计量有上界时百分位区间会塌到上界**(极差的重采样值永远 ≤ 原样本极差,实测 `high == estimate`),
以及 5 个待拍板口径。`docs/algorithms/README.md` 的进度表同步打勾。
### Fixed
- `bootstrap_ci` 的 `random_state=None` 分支原来另开 `np.random.default_rng()`,**绕过了 `core.get_rng`** ——
与决策 24 冲突:`set_seed` 对它无效、区间不可复现。改成无条件 `get_rng(random_state)`(决策 46)。
- `bootstrap_ci` 的 `metrics["statistic_name"]`:`hasattr(statistic, "__name__")` 判不出 lambda ——
lambda 的 `__name__` 恒为 `"<lambda>"`,于是 `else "custom"` 是**死代码**,自定义统计量拿不到 `"custom"`。
改成 `getattr(f, "__name__", None)` 再判 `name in (None, "<lambda>")`(易错点 17)。
- `bootstrap_ci` 的 docstring 补齐 `Parameters` / `Returns` / `Raises`(§4.7),并如实写明
`method="bca"` **尚未实现**(此前 docstring 承诺支持、实现却抛 `NotImplementedError`)。
### Changed
- `tests/test_stats.py`:**摘掉 3 条 `skip`**(等 `bootstrap_ci` 的那 3 条),并新增 6 条断言 ——
`estimate` 等于原样本统计量、`confidence` 单调性、五种容器结果一致、自定义 lambda 走回退分支、
**不给种子时跟随 `set_seed`**、非法/未实现 `method`。bootstrap 段共 9 条。
全量 **177 条:143 passed / 34 skipped**;ruff 0,pyright 0。
- `docs/dev/personal-pitfalls.md`:**第 9 条「实现写完了测试还挂着 skip」复发 +1**(这次是 `bootstrap_ci` ——
实现写完、3 条测试仍挂 skip、全量照样全绿);新增第 17 条「`hasattr(f, "__name__")` 挡不住 lambda,
`else` 分支是死代码」;索引补到 17。
- `STATUS.md`:E 节记 `bootstrap_ci`(L0 10/12),决策 46,「下一步」改写并新增第 8 条列 5 个待拍板口径。
### Changed
- `tests/test_solve.py`:模块级 `pytestmark = pytest.mark.skip(...)` 拆成 **9 条逐条**
`@pytest.mark.skip(reason=_NOT_IMPLEMENTED)` —— 模块级 skip 会把整个文件吞掉(决策 26、易错点 9)。
用例数与跳过数不变(177 收集 / 143 passed / 34 skipped),仅粒度变细。
- `STATUS.md`:新增**分工硬规矩(决策 47)** —— 算法一律由用户本人写(即便用户改口也不代写)、
工程事务由 Codex 写;L0 计数 9/12 → **10/12**;「下一步」新增「⭐ 明天从这里开始」区块
(`monte_carlo` 的契约 / 向量化要求 / 三条测试建议 / 卡片路径 / 一个待拍板口径);
验证结果里的 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 区间"。
### Added
- `solve.monte_carlo_integral` 落地(**L1,S 档**,实现由用户本人写):`bounds` → 自造 `(n, d)` 均匀样本 → 逐点求值 →
`mean(func(X) × 体积)`;内部自造 `sampler` / `simulator` 再复用 `monte_carlo` —— "造批 → 一次算 → 汇总"的第二次复用。
`params` 额外记 `bounds` / `dimension`;配套 `tests/test_solve.py` 新增 4 条 + 摘 1 条 skip。
### Fixed
- **`monte_carlo_integral` 的体积符号写反**(真 bug,按决策 31 修):写成 `wights = lows - highs`,奇数维体积为负 ——
1D `x²` 于 `[0,1]` 实测 **-0.3324**(应 +1/3)、3D `x` 得 **-0.5005**(应 +1/2);**2D `x·y` 却得 +0.2499 "看着对"**
(两个负号相乘抵消,把 bug 藏住)。改成 `widths = highs - lows` 后复测 **0.3324 / 0.5005 / 1.9971(`[0,2]`)** 全部正确。
- `monte_carlo_integral` 的 docstring `Examples` 跑不通:断言 `abs(r.data - 1 / 3) < 0.01`,而 `data` 是 n 个样本的贡献数组,
`bool(数组)` 会抛 `ValueError: truth value ... is ambiguous`;改成断言 `metrics["mean"]`。
- 格式:`simulate.py` 里 `def random_walk` 前只留了 1 个空行;`tests/test_solve.py` 有 3 处留了 3 个空行。
### Changed
- `monte_carlo_integral` 的 docstring 补齐 `func` / `n` / `random_state` 三节 `Parameters` 与 `Raises`(原文只有 `bounds` +
Notes + Examples);`Returns` 写清"**`metrics["mean"]` 才是积分估计值**,`data` 是 n 个样本各自的贡献"(**决策 51**)。
- `tests/test_solve.py`:摘掉 `test_monte_carlo_integral_converges_to_analytic_value` 的 skip,新增 3 条"抓符号"用例
(3D 奇数维、`[0,2]` 宽度、2D + `name`/`dimension`/`bounds` 三个 params)与 1 条非法 `bounds` 抛 `ValueError`;
`solve` 段 25 → **30** 条,全量 **197 收集 / 166 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:L1 清单加入 `monte_carlo_integral`;新增**决策 51** 与「`monte_carlo_integral` 的 3 条待办」;
「下一步」改成从 `random_walk` 接着写;验证计数更新为 166 / 31;`monte_carlo_probability` 的 6 条待办收窄到 1 条。
- `docs/dev/personal-pitfalls.md`:新增第 **22 / 23** 条(失败结果的 `message` 被包装层无条件覆盖;体积/宽度也会减反、
维度奇偶会把符号错误藏住),第 3 条(注解与 docstring 不一致)**复发 +1**,索引补到 23。
- `tmp/type_demo.py`(gitignored):用 pyright 证明 `Callable[..., bool]` 会**拒绝**一个注解良好的向量化 `event`
(`ndarray` 与 `bool` 不兼容),`Callable[..., Any]` 才收 —— 这是决策 50 那条遗留的实际代价。
### Fixed
- `monte_carlo_integral` 的 `bounds` 形状校验缺失(用户第二轮补齐):`bounds=[0, 1]` 从裸 `IndexError: too many indices`
变成可读 `ValueError`(带当前形状与"请检查是否漏了括号");`[]` / 非 `list`-`tuple` / 参差不齐的输入也一并收口。
- `monte_carlo_integral` 的失败 `message` 被包装层覆盖(用户第二轮改成 `if result.success:` / `else:` 两支):
`func` 返回标量时不再是 `success=False` + "成功"文案,而是"蒙特卡洛多重积分失败:维度 1,体积 1.0000,模拟 100 次。
蒙特卡洛模拟失败:TypeError - object of type 'float' has no len()"。
- 格式:`simulate.py` 两行"只有空格的行"(ruff W293 ×2,Codex 顺手补)。
### Changed
- `monte_carlo_probability` 的 `event` 注解 `Callable[..., bool]` → `Callable[..., Any]`(用户改,**决策 50 的遗留清零**):
pyright 实测原注解会**拒绝**一个注解良好的向量化事件函数(`ndarray` 与 `bool` 不兼容),运行期却一直收得下。
- `monte_carlo_integral` 的 `n` 注解放宽为 `int | np.integer`(与全模块对齐,易错点 1 的老口径)。
- `monte_carlo_integral` 的 docstring:`bounds` 入参说明与 `Raises` 同步到新入口校验(补"非空 `list`/`tuple`、形状 `(d, 2)`")。
- `tests/test_solve.py`:非法 `bounds` 从 1 类扩到 4 类(`[(1, 0)]` / `[0, 1]` / `[]` / 参差不齐),新增"失败 message 不许出现
'成功'"与 `np.int64` 两条;`solve` 段 30 → **32** 条,全量 **199 收集 / 168 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:integral 的 3 条待办标记办结、概率函数的 6 条待办全部办结;新增**决策 52** 与 2 条新口径
(`bounds` 收不收 ndarray、`if`/`else` 里那三行重复);验证计数更新为 168 / 31。
### Added
- `solve.random_walk` 落地(**L1,S 档**,实现由用户本人写):`step_sampler(rng, size=(n_paths, n_steps))` 一次抽完整批增量 →
`np.cumsum(..., axis=1)` → 首列填 `start`、其余列 `start + cumsum`;`data` 形状 `(n_paths, n_steps + 1)`。
配套 `tests/test_solve.py` **新增 6 条**(此前这个函数一条测试都没有)。
### Fixed
- **`random_walk` 的默认采样器整个不可用**(真 bug,按决策 31 修):`rng.integers(0, 2, size=size, dtype=float)`
被 numpy 拒掉(`TypeError: Unsupported dtype dtype('float64') for integers`)—— 默认路径 100% `success=False`、
`data=None`,连 docstring 的 Examples 都跑不通。改成 `rng.integers(0, 2, size=size) * 2 - 1`
(由后面的 `np.asarray(..., dtype=float)` 统一转 float);实测形状 `(5, 101)`、步长恒 ±1、同种子可复现。
- 格式:`simulate.py` 又两行"只有空格的行"(ruff W293 ×2)。
### Changed
- `monte_carlo_integral` 按用户第二轮口径收口(**决策 52**):`bounds` 现在也收 `np.ndarray`;
`name` / `params["bounds"]` / `params["dimension"]` 从 `if` / `else` 两支提到 `if` 之前,只让 `message` 分两支。
- `random_walk` 的 docstring 补全(原文只有 `step_sampler` 一节):补齐 `n_steps` / `n_paths` / `start` / `random_state`
与 `Returns` / `Raises` / `Notes`,并写清 `step_sampler` 收的是**二维** `size=(n_paths, n_steps)`。
- `tests/test_solve.py`:新增 6 条 `random_walk` 测试(形状与 `start` 列、默认步长恒 ±1、同种子可复现、
自定义高斯采样器、3 类非法入参、docstring Example 真能跑);`solve` 段 32 → **38** 条,
全量 **205 收集 / 174 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:新增**决策 53** 与 `random_walk` 的 4 条待办;`monte_carlo_integral` 的 2 条新口径办结 1 条
(剩"第一道 `isinstance` 分支已冗余");「下一步」推进到 `markov_simulate` / `markov_steady_state`。
### Fixed
- **`random_walk` 的失败路径全部炸成 `UnboundLocalError`**(真 bug,按决策 31 修):`call_params = {...}` 写在 `try:` 里,
而 `except` 分支要引用它 —— 实测 5 种失败输入(形状不对 / 增量含 NaN / 采样器抛异常 / `start` 非数值 / 返回 1-D 数组)
**5 个全中**,本该返回 `success=False` 的 `Result`,变成把异常抛给调用方。按原意把 `call_params` 前移到 `try` 之前,
未改任何口径;失败时照样带上 `n_steps` / `n_paths` / `start` / `random_state`。
### Changed
- `random_walk` 按用户第二轮口径改定(**决策 54**):`n_steps` / `n_paths` 拆成 `TypeError`(类型)/ `ValueError`(取值);
新增 `steps.shape != (n_paths, n_steps)` 与 `np.isnan(steps).any()` 两条校验;默认步长改 `rng.choice([-1.0, 1.0], size=size)`;
`params` 补 `random_state`;`metrics` 由"抄 params"改成 `mean_terminal` / `std_terminal`(终点位置,`ddof=0`)。
- `random_walk` 的 docstring 同步:`Returns` 写实际 metrics、`Raises` 改成"类型错 `TypeError` / 取值错 `ValueError`"、
去掉已不存在的 `start` 校验、`Notes` 记明形状与 NaN 都判失败。
- `tests/test_solve.py`:+3 条("失败也必须返回带 params 的 Result"、NaN 判失败、终点 `std ≈ √t`),
`start="a"` 改为断言 `success is False`(口径待定);`solve` 段 38 → **41** 条,
全量 **208 收集 / 177 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:新增**决策 54** 与 `random_walk` 的 3 条新口径;4 条待办标记办结;验证计数更新为 177 / 31。
- `docs/dev/personal-pitfalls.md`:新增第 **26** 条(`except` 用到的变量在 `try` 里才赋值 —— 每条失败路径都炸)。
### Changed
- `random_walk` 收口 2 条口径(用户第二轮改动):恢复 `start` 的入口类型校验(`isinstance(start, (int, float, np.number))`);
签名注解放宽为 `n_steps: int | np.integer` / `n_paths: int | np.integer`(与 §4.7 及 `monte_carlo_integral` 的 `n` 对齐)。
- `random_walk` 的 `start` 报错文案(Codex 顺手):原文"start必须是整数"与校验本身矛盾 —— 这条校验**收 float**,
改成"start 必须是数值";类型名统一 `{type(start).__name__}`(全模块口径);`n_paths: int | np.integer= 1` 补空格。
- `random_walk` 的 docstring 同步:`start` 的说明去掉"入口不校验类型",`Raises` 补回 `start` 的 `TypeError`。
- `tests/test_solve.py`:`start="a"` 恢复断言 `TypeError`;补回 `np.int64` 的 `n_steps` / `n_paths`;
`solve` 段 41 → **42** 条,全量 **209 收集 / 178 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:新增**决策 55**(`n_paths == n_steps` 的"方阵歧义"留作已知边界,A 接受 / B 换契约两条路);
3 条口径办结 2 条,验证计数更新为 178 / 31。
### Changed
- `random_walk` 的 `step_sampler` 契约按**方案 B** 重定(用户拍板,**决策 55**):`size` 由 `(n_paths, n_steps)` 改为**整数**
`n_paths * n_steps`,必须返回**一维**数组,内部 `reshape(n_paths, n_steps)`(行主序)。
**动因**:转置 `(n_steps, n_paths)` 与目标 `(n_paths, n_steps)` 在 `n_paths == n_steps` 时形状相同,形状校验天生分不出来(易错点 25)。
探针实测:默认采样器 `(5, 101)` 正常;转置二维 / 方阵二维 / 长度 +1 / `(N, 1)` 四种返回一律判失败。
**关键点:不要用 `.ravel()` 救场**,那会把转置的二维摊平、歧义原样回来。红利:与 `monte_carlo` 的 `sampler`(决策 48 的 `size=n`)
契约统一,给 `monte_carlo` 写的采样器可直接复用(实测通过)。
- `random_walk` 的 docstring 同步:`step_sampler` 说明改为"整数 `size` + 一维返回 + 行主序",`Notes` 记明二维返回一律判失败。
- `tests/test_solve.py`:把"转置采样器"那条改成 `test_random_walk_rejects_2d_step_sampler`(含方阵情形),
新增 `test_random_walk_step_sampler_gets_flat_size_and_is_row_major`(验 `size == 6` 与 `cumsum` 的行主序结果);
`solve` 段 42 → **43** 条,全量 **210 收集 / 179 passed / 31 skipped / 0 xfail**(ruff 0 / pyright 0)。
- `STATUS.md`:决策 55 改为"已选 B 并落地";`random_walk` 的 7 条待办全部办结;验证计数更新为 179 / 31。
### Added
- `solve.markov_simulate` 落地(**L1,S 档**,实现由用户本人写):`arr[current_states]` 取当前状态那一行 →
`cumsum` 得累积分布 → 用 `[0, 1)` 均匀数"落格"(`(cum_probs < r[:, None]).sum(axis=1)`)→ 写进下一列;
`data` 形状 `(n_paths, steps + 1)`、值是整数状态编号。Codex 审查结论:**算法本体正确**
(探针实测 200 步后终点频率 `[0.657, 0.343]` 对照平稳分布 `[2/3, 1/3]`、同种子可复现)。
- `tests/test_solve.py`:新增 9 条 `markov_simulate` 测试(此前 0 条)—— 形状 / 首列 / 状态取值 / 整数 dtype、
单位矩阵吸收态自锁、可复现(同种子相同、异种子不同)、终点频率收敛到 `[2/3, 1/3]`、metrics = 终点状态、
非法转移矩阵(非方阵 / 行和不为 1)、非法 `steps` / `n_paths`(0 / float / bool)、`np.int64` 可收、docstring Example 真能跑。
### Changed
- `markov_simulate` 的 docstring 从一行补到完整的 `Parameters` / `Returns` / `Raises` / `Notes` / `Examples`。
- `markov_simulate` 的工程侧清理:`kind` 全程未用 → `arr, _`;`马尔科夫` → `马尔可夫`(3 处)、
`initail` → `initial`、半角逗号 → 全角;`paths[:,0]` → `paths[:, 0]`;ruff 8 条全清(`I001` + `W291` / `W293`)。
- `STATUS.md`:新增**决策 56**(`initial` 的语义待拍板,含 A / B / C 三个选项)与 5 条待办;
验证计数更新为 **188 passed / 31 skipped(219 收集)**。
### Notes
- **`markov_simulate` 的 5 条口径留给用户**(决策 49):`initial` 语义(标量按路径数 / 数组按状态数,两条口径互相矛盾)、
转移矩阵未查非负(行和恰为 1.0 的 `[[1.5, -0.5], [0, 1]]` 会静默给出全 0)、`paths[:, 0]` 赋值在 `try` 之外
(长度不匹配时裸抛 `ValueError`)、状态越界没在入口拦、`std_terminal` 对分类状态的意义。
### Added
- `solve.markov_steady_state` 落地(**L1,S 档**,实现由用户本人写):`v` 从均匀分布出发,`v_next = v @ arr`
反复推进(行随机矩阵按行向量右乘),直到 `max|v_next - v| < tol` 或耗尽 `max_iter`;
`data` 是平稳分布向量、`metrics={"converged", "iterations"}`。Codex 审查结论:**算法本体正确** ——
3 状态与 `np.linalg.eig(P.T)` 特征值 1 的特征向量逐位一致、200×200 一致到 `1.9e-12`(6 次迭代 / 0.10s)、
与 `markov_simulate` 的终点频率互验 `[0.6702, 0.3298]` vs `[0.6667, 0.3333]`。
- `tests/test_solve.py`:摘掉 `test_markov_steady_state_two_state_chain` 的 skip,另新增 7 条 ——
与特征分解对拍、`data` 是分布(和为 1 / 非负 / `float64`)、慢混合链迭代次数 > 100、`np.int64` 的 `max_iter`、
非法转移矩阵、`max_iter=0` / `tol=0`、docstring Example 真能跑。
### Changed
- `markov_steady_state` 的 docstring 补全:`max_iter` / `tol` 两个参数、`Raises` 一节,
并写清 `tol` 是**相邻差口径**(实测慢混合链 `tol=1e-10` 的实际误差 `4.6e-9`,被 `1/(1-|λ2|)` 放大约 50 倍)。
- `markov_steady_state` 的 Notes 修正"周期链会不收敛"的说法:可约链的实情是**平稳分布不唯一**,
迭代法给出的是"离初值最近的那个"(均匀初值对双随机矩阵本身就是不动点,所以 `iterations=1` 不代表链唯一平稳)。
- `markov_steady_state` 的工程侧清理:ruff 5 条 `W293`;`mm1_metrics` 前 4 个空行 → 2;失败文案 `失败: ` → `失败:`。
- `STATUS.md`:新增**决策 57**(6 条待拍板口径)与「下一步」第六轮;验证计数更新为 **196 passed / 30 skipped(226 收集)**。
- `docs/dev/personal-pitfalls.md`:新增易错点 28(数值阈值参数的 `bool` / NaN 漏网)。
### Notes
- **`markov_steady_state` 的 6 条口径留给用户**(决策 49):转移矩阵未查非负(行和恰为 1.0 的 `[[1.5, -0.5], [0, 1]]`
会溢出成 `nan`,而 `data=[nan, nan]` 却 `success=True`,违反决策 48 的 NaN 口径;附带的 `RuntimeWarning`
在 pyproject 里被设成 error,**测试会直接报错**)、`tol=True` 被当 `tol=1` 静默放行(`isinstance(True, int)`)、
`tol=np.nan` 穿过校验白跑迭代、`converged=False` 仍 `success=True`、
类型错也抛 `ValueError`(与决策 54 分工不一致)、`params` 没记转移矩阵。
### Added
- `solve.mm1_metrics` 落地(**L1,S 档**,实现由用户本人写):闭式解 `ρ = λ/μ`、`L = ρ/(1-ρ)`、
`Lq = ρ²/(1-ρ)`、`W = 1/(μ-λ)`、`Wq = W - 1/μ`;`λ >= μ` 走"系统不稳定"分支返回 `success=False`。
Codex 审查结论:**公式全对**(探针实测 3 组参数 `(2,3)` / `(1,4)` / `(0.5,1.0)` 与闭式公式逐位一致,
`W = Wq + 1/μ` 恒等式成立)。至此 `solve` 层的 L1 清单**全部落地**。
- `markov_simulate` / `markov_steady_state` 增加**非负校验**(用户 2026-10-09 改定):含负元素的行随机矩阵
现在入口即报错,不再静默给出错结果(前者会"全落 0",后者会溢出成 `nan` 却报成功)。
- `markov_steady_state` 的 `params` 补 `transition`(`arr.tolist()`);`success=converged`
(未收敛判失败,口径 B);`tol` 拆成 `TypeError` / `ValueError` 且先判 `bool`。
- `tests/test_solve.py`:摘掉 2 条 `mm1_metrics` 的 skip,新增 4 条(含 `λ == μ` 的回归测试);本轮累计 +6 条。
### Fixed
- **`mm1_metrics` 的稳定性分界写错**(真 bug,按决策 31 修):`service_rate < arrival_rate` 与 docstring / 既有测试的
`λ >= μ` 口径不符 —— `λ == μ` 会掉进 `try`、`rho == 1.0` → `ZeroDivisionError`,被吞成 `success=False`
(结果碰巧对,但 message 成了"计算失败:ZeroDivisionError - float division by zero")。改成 `<=` 并加回归测试;
**两个分支的可观测输出完全相同(`success=False` / `metrics={}` / `data=None`),故未动任何口径**。
- **三处报错文案的参数名抄错**:`markov_steady_state` 的 `max_iter <= 0` 写成"tol 必须是正数";
`mm1_metrics` 的 `service_rate` 两条报错都写成 "tol"。顺带把半角逗号统一成全角。
### Changed
- `mm1_metrics` 的 docstring 补 `Raises` 与 `data` 说明;`call_params = {{...}}` 补空格;
ruff 6 条(5 处 `W293` + `F541` f-string 无占位符 + `W292` 文件末尾无换行)。
- `markov_steady_state` 的 `try` 前 3 个空行 → 1;`异常: ` → `异常:`。
- `STATUS.md`:决策 56 / 57 更新办结项,新增**决策 58**(`mm1_metrics` 的 4 条待拍板);「下一步」推进到第七轮;
验证计数更新为 **202 passed / 28 skipped(230 收集)**。
- `docs/dev/personal-pitfalls.md`:新增易错点 29(同一个校验里的两把尺子)。
### Notes
- **待用户拍板的口径**(Codex 只报不改):`markov_simulate` 4 条(`initial` 语义 A/B/C、`paths[:, 0]` 赋值位置、
状态越界、`std_terminal` 对分类量)、`markov_steady_state` 3+2 条(`tol` 的 NaN、`max_iter` 类型错、
非负校验的**容差**、未收敛却带 `data`)、`mm1_metrics` 4 条(`λ = 0`、numpy 标量、`data=metrics`、
失败分支丢 `rho`)。详见 `STATUS.md` 决策 56 / 57 / 58。
### Added
- `tests/test_solve.py`:本轮再 +2(`markov_simulate` 的 `terminal_distribution` 口径、
`mm1_metrics` 的 `λ = 0` 空系统),并把受口径变更影响的 5 条测试同步到新行为。
### Changed
- `markov_simulate`:非负校验带容差 `-1e-12`(浮点归一化的 `-1e-16` 残渣不再被误判);
`initial` 收 `np.integer`;`paths` 构造与首列赋值挪进 `try`(长度不匹配 / 状态越界不再裸抛);
`params` 键 `n_steps` → `steps`、`initial` 存成可 JSON 化的标量 / 列表;
`metrics` 的 `std_terminal` → `terminal_distribution`(`{状态: 频率}`,只列出现过的状态)。
- `markov_steady_state`:非负校验带容差;`max_iter` 类型错改抛 `TypeError`;`tol` 增加 `np.isnan` 拦截;
非方阵改抛 `TypeError`。
- `mm1_metrics`:`arrival_rate` 放开为**非负**(`λ = 0` = 空系统,`L = Lq = Wq = 0`、`W = 1/μ` 取极限值);
两个速率收 `np.number`;不稳定分支保留 `rho` 并写进 message;`data=metrics.copy()`(去掉同一个 dict 对象的别名)。
- docstring 同步:`markov_simulate` 的 `Returns` 与"遗留"段、`markov_steady_state` 的 `Raises`(TypeError / ValueError 分工)、
`mm1_metrics` 的 `Parameters` / `Returns` / `Raises`。
- ruff:`B905`(`zip` 缺 `strict`)+ 2 条空白 → 0;`markov_simulate` 里 `rng` 之后的双空行 → 1。
- `STATUS.md`:决策 56 / 57 / 58 更新到"只剩 5 条小口径",「下一步」推进到第八轮,
并写入**第八轮备忘:`models` 与 `decision` 各是干什么的**(含两包的 19 / 8 个函数清单、使用顺序、建议开工顺序)。
- `docs/dev/personal-pitfalls.md`:新增易错点 30(口径一改,docstring / 测试 / STATUS 三处会同时过期)。
### Notes
- **只剩 5 条口径待拍板**(Codex 只报不改):`markov_simulate` 的 `bool` 漏网与 `initial` 的 A/B/C、
`markov_steady_state` 未收敛时是否带 `data`、非方阵的 `TypeError` / `ValueError` 不一致、
`mm1_metrics` 的 `data` 该不该是 dict。
## [0.0.1] - 2026-10-01
### Added
- 项目初始化,包结构落盘(实现为空)。