# 更新日志 格式参考 [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__` 恒为 `""`,于是 `else "custom"` 是**死代码**,自定义统计量拿不到 `"custom"`。 改成 `getattr(f, "__name__", None)` 再判 `name in (None, "")`(易错点 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 - 项目初始化,包结构落盘(实现为空)。