Files
MCM-ICM_Toolkit/STATUS.md
T
guzhujushi cfc1be31ca fix(stats): CV 改用 |mean|,并记录决策 37/38/39 与经验 12
coefficient_of_variation:
- 分母由 mean 改为 |mean|(用户本人改),均值为负时 CV 不再是负值
- docstring 摘要 / Returns / Notes 同步,加一条测试钉住"均值为负时 CV 仍为正"

口径拍板(2026-10-03):
- 决策 37: 非数值列的处置按输出契约分,不按包分 —— describe/zscore 丢掉(吐数值结果表),
  detect_outliers 保留列、掩码恒 False(吐与原表同形的掩码),刻意不统一
- 决策 38: CV 用 sigma/|mean|
- 决策 39: 工作约定 —— 语义/非法输入口径由用户拍板,Codex 只列选项不代选;
  ruff / STATUS.md / CHANGELOG.md / tests / docstring 这类与算法无关的工程事务 Codex 顺手补

经验(docs/dev/personal-pitfalls.md,只追加):
- 第 12 条: 三个并列的 if 让后面的静默覆盖前面的(transform.normalize 三种 method 实际只走 max)
  第 11 条(上一轮)是 descriptive 的 .loc 对齐失败静默填 NaN —— 两条的共同点是"不抛异常"

验证: pytest 103 passed / 39 skipped;ruff All checks passed;pyright 0 errors。

docs: STATUS.md 决策 37/38/39 + 下一步;CHANGELOG 记录
2026-10-03 13:26:13 +08:00

266 lines
27 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.
# STATUS — 当前进度
> 更新日期:2026-10-02
## 本次任务
**第一阶段(已完成)**:审查并精简 ChatGPT 的建库建议 → 生成骨架文件 →
配置 conda 与 venv 环境 → 初始化 git 仓库 → 接入 Gitea 远端。
两套环境均已建成并通过验证,仓库已推送到 `code.guzhujushi.cn`。
**第二阶段(进行中)**:按 `docs/03-roadmap.md` 的 L0 → L1 顺序**逐个实现函数**,
用户本人写 `src/`、Codex 负责审查 + `tests/`(决策 25)。
当前 8/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、`detect_outliers`、`normalize`/`standardize`、`describe`。
## 任务清单
### ✅ 已完成
**A. 建议审查与精简**
- [x] 通读 `chatGPT_Suggestion.md`(33 节,约 2.4 万字),输出 `docs/01-suggestion-review.md`
- 总裁决:**方向对,量级错**
- 14 个包 → **7 个**;约 250 个函数 → **约 40 个**;5 个 Result 类 → **1 个**
- 单函数文档要求 10 项 → **2 档**(只有约 15 个招牌算法写完整卡片)
- 逐项给出"砍什么 / 为什么 / 替代什么",并补上原文缺失的六件事
(论文产出链路、指标方向与量纲、时间预算、可复现性、测试测什么、离线约束)
**B. 骨架文件生成**(76 个文件,约 6800 行)
- [x] `docs/02-architecture.md` 架构、`docs/03-roadmap.md` 路线图与**永久不做清单**
- [x] `docs/04-environment.md` 环境文档、`docs/dev/function-card-template.md` 卡片模板
- [x] `src/mcm_toolkit/` 7 个包 23 个模块(**只有签名 + docstring + `NotImplementedError`**)
- [x] `tests/` 9 个文件、`examples/` 4 个、`problems/` `notebooks/` `figures/` `tables/`
- [x] `pyproject.toml`(核心依赖 4 个 + 6 组 extras)、`environment.yml`
- [x] `.gitignore` `.gitattributes` `.editorconfig` `LICENSE` `CHANGELOG.md` `README.md`
- [x] 脚本 `scripts/setup-venv.ps1` `setup-conda.ps1` `doctor.ps1`
**C. 环境配置**
- [x] **venv 已建好**:`.venv`(10.5 MB,Anaconda base + `--system-site-packages`)
+ `pip install -e . --no-deps --no-build-isolation`;
可直接导入 numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6 / matplotlib 3.11.2 /
sklearn 1.9.1 / statsmodels 0.15.0 / networkx 3.6.1 / sympy 1.14.0
(版本随 base 走,base 升级后自动同步)
- [x] **conda 环境已建好**:`D:\anaconda\envs\mcm-toolkit`(1.55 GB,Python 3.13.15)
- 科学栈:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6 / matplotlib 3.11.2 /
sklearn 1.9.1 / statsmodels 0.15.0 / networkx 3.6.1 / sympy 1.14.0(MKL 后端)
- 已补装 pip-only 依赖:SALib、pymoo 0.6.2、pyright 1.1.414
- 已在本项目中可编辑安装
- 实测耗时 868 秒,下载约 3 GB
- [x] **Anaconda base 已升级** 2025.12 → **2026.07**(365 个包,下载 638 MB,耗时 1033 秒)
- conda 自身 25.11.1 → **26.7.3**(实测复核)
- 并把项目环境 numpy 对齐到 base 的 2.4.6,**两套环境数值栈现已完全一致**(MKL 后端)
- 升级前已用 `conda list -n base --revisions` 留了回滚点
- [x] 环境脚本按实测结果重写:`setup-conda.ps1` 默认改为**精准包列表**
(`--clone base` 实测会走完整下载安装,不再是"离线快捷方式")
**D. Git 仓库**
- [x] `git init -b main`,`core.autocrlf=false`,首次提交
`5319589`(76 文件 / 6812 行),工作区干净
- [x] `git ls-files --eol` 复核:**全部 LF**,`.gitattributes` 生效
- [x] 仓库由**本人账户 guzhu** 创建(见决策 17)
- [x] **远端 Gitea 已接通并推送**:`origin` = `git@code.guzhujushi.cn:guzhujushi/MCM-ICM_Toolkit.git`,
`main` 跟踪 `origin/main`,远端 HEAD = `96080b9`,76 个文件已在服务器上(见决策 19/20)
- [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`),
`~/.ssh/config` 加了一条域名别名,remote 里仍写域名
**E. L0 实现进度(8/12)**
- [x] `core/result.py` 的 `Result`
- `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印
(十万级数组的 repr 实测仍 < 200 字符);`success` 恒在
- `summary()`:Markdown 表格,可直接贴进论文;
行序 `success` → `params.*` → `metrics.*` → `extras.*`;
float 走 `.6g`,`|` 转义,`message` 以引用行附在表后;幂等无副作用
- 测试:`tests/test_core.py` 去掉 skip + 新增 3 条
(默认值/失败路径/大数组、Markdown 结构、可重复调用)
- [x] `core/random.py` 的 `get_rng` / `set_seed`(**设计由用户本人写**,Codex 补齐类型校验)
- 无参 `get_rng()` 复用**同一个模块级生成器**且会推进:既保证重跑一致,
又避免两个都省略 `random_state` 的函数抽到同一串数(原决策 24 的取舍已消掉)
- `get_rng` 收 `int` / `np.integer` / `Generator` / `None`,其余类型**抛 `TypeError`**:
`np.int64` 不是 `int` 的子类,曾因此静默退回全局流
- `set_seed` 同时调 `np.random.seed`,覆盖 sklearn / statsmodels 走的 numpy 旧全局
(全项目**唯一**碰旧接口的地方,带 `# noqa: NPY002` 与理由)
- 测试:`TestRandom` 共 7 条(可复现、透传、无参跟随、旧全局、np.integer、非法类型、共享推进)
- [x] `data/io.py` 的 `load_table`(+ `save_table`,原属 L1,顺手落地)
- 实现由**用户本人**写(决策 25),Codex 只做审查 + 补测试
- `.csv` / `.xlsx` / `.xls` 按扩展名分派;`.csv` 默认编码探测
utf-8 → utf-8-sig → gbk → gb18030,显式传 `encoding=` 时不再探测
- `na_values` 默认吃下空串 / `NA` / `N/A` / `#N/A` / `null` / `-`
- 不支持的扩展名抛 `ValueError`;文件不存在抛带路径的 `FileNotFoundError`
- `save_table` 的校验顺序已修正:后缀判断提到 `mkdir` 之前
(此前传 `.txt` 会先留下一个空目录再抛错)
- 测试:`tests/test_data.py` 由 1 条(且被模块级 skip 掉)扩到 **9 条**
- docstring 的 `Parameters` / `Raises` 已按 §4.7 补齐(Codex 代写,用户授权)——
numpydoc 格式,对齐参考实现 `core/random.py` 的 `get_rng`
- [x] `data/clean.py` 的 `clean_missing`(L0 第 4 项)
- 实现由**用户本人**写,Codex 只做审查、补测试与文档
- `threshold` 用 `math.ceil` 算 `min_non_na`:3 列、阈值 0.5 时必须是 2,
写 `int()` 截断会漏删
- 顺手落地兄弟函数 `impute_missing` / `winsorize`(原属 L1)
- [x] `data/clean.py` 的 `detect_outliers`(L0 第 5 项,S 档,**实现由用户本人写**)
- 语义由用户本人定:**非数值列不参与统计,掩码里恒 `False`**(见决策 32)
- 三轮修掉的 4 个 bug:`threshold` 范围校验放错函数、IQR 上界符号、
`is_series` / `is_ld_array` 判在类型转换之后(永远为 False)、文本列上算分位数直接崩
- 关键教训:**限制掩码 ≠ 限制统计量** —— 只把 `mask[numeric_cols] = ...`
收窄是不够的,`quantile()` / `mean()` / `std()` 也要一起收窄(见坑 8 / 个人易错点 8)
- 返回形状与输入同形;Series 进 Series 出(列名保留)、一维数组进一维布尔数组出
- **S 档函数卡片已写**:`docs/algorithms/detect_outliers.md`
(含"为什么不返回 `Result`"、非数值列恒 `False` 的两条备选路、`ddof` 口径不一致等)
- 测试:`tests/test_data.py` 该函数共 **9 条**
- [x] `data/clean.py` 的两处收尾(**Codex 代写,用户授权**)
- `impute_missing` 的 mode 分支改为**返回新表**(原先就地改调用方的表,与另外四支不一致)
—— 最后一条 `xfail` 随之转正,**`xfail` 清零**
- `detect_outliers` 补**入参类型校验**(§4.7 要求非法类型抛 `TypeError`):
接受 `DataFrame` / `Series` / `list` / `tuple` / `ndarray`,`dict` 不再接受(决策 33)
- [x] `examples/01_clean_and_describe.py`:`load_table` / `save_table` / `clean_missing` /
`impute_missing` / `detect_outliers` / `winsorize` 六个函数的最小用法,实测可直接跑通
- [x] `data/transform.py` 的四个函数(**由用户本人实现**):`normalize` / `standardize`(L0 第 6、7 项)、
`log_transform` / `one_hot_encode`(原属 L1,顺手写了)
- `normalize`:三种 method(`minmax` / `vector` / `max`)都**按列**;`feature_range` 校验下界 < 上界;
常量列压到下界而不出 nan(决策 34)
- `standardize`:按列,`ddof` 真的传下去 —— 测试用解析关系钉住
(`ddof=0` → std 恰为 1;`ddof=1` → `sqrt((n-1)/n)` = 0.866)
- `log_transform`:`offset=None` 时自动取 `1 - min(x)`,保证 `x + offset >= 1`
- `one_hot_encode`:自动识别的 dtype 集合已显式补上 `'str'` / `'string'`(见坑 9)
- Codex 清掉 5 条 ruff(RET503 / F541 / RET504 / RET505 / SIM108),并把 `feature_range`
的两条校验提到 `np.asarray` **之前**、修掉说反的报错文案(条件是"下界必须小于上界")
- 测试:**新写 18 条**,其中两条是把原先挂着的 `skip` 转为实跑 —— 见 `tests/test_data.py` 的 transform 段
- [x] **`transform.py` 的 4 个口径已收口**(2026-10-02,Codex 代修,用户授权"报错顺手修掉"):
- 输入白名单统一成模块级 `_NUMERIC_TYPES`,**`DataFrame` 补进去**并移到 `np.asarray` 之前
(此前 `DataFrame` 不在白名单 → `standardize(df)` / `log_transform(df)` 直接 `TypeError`,
函数体里所有 `is_df` 分支都是**死代码**)
- 三个函数都保持"DataFrame 进 DataFrame 出"(列名 + 索引原样带回),Series 进 Series 出
- `normalize` 的三个 method 分支原来**互相覆盖**(`minmax` / `vector` 算完的结果被末尾
无条件的 `x / max` 冲掉,三种 method 实际都只走 max),改成 `if / elif / else`
- `one_hot_encode` 不再拿 `data.columns` 重包一层(哑变量列名对不上 → 编码列整列丢失)
- `log_transform` 的 `offset` 守卫**写反了**(`offset > 0` 就抛错,恰好拒掉唯一合法用例),
改成校验真正的定义域前提 `min(x) + offset > 0`(见决策 35)
- docstring 按 §4.7 补齐 `Parameters` / `Returns` / `Raises`(Codex 代写,与 `io.py` 同一套 numpydoc 格式)
- 口径定稿后 ruff 4 条 RET505 一并清掉 → `All checks passed`;pyright **2 errors → 0**
- [x] `stats/descriptive.py` 的 `describe`(L0 第 8 项,**由用户本人实现**,Codex 审查 + 代修)
- **一个静默 bug(最危险的那种)**:`df_num.describe().T` 把表转反了(行 = 变量、列 = 指标),
而 docstring 与 `test_describe_reports_expected_keys` 都要求"行 = 指标、列 = 变量"。
转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名**零交集**,pandas 不报错,
直接把这四行(`missing` / `missing_ratio` / `skewness` / `kurtosis`)**整行填成 NaN**。
去掉 `.T` 一处即修好三件事:方向对了、NaN 没了、`weights` 分支不再报
`cannot set a row with mismatched columns`(见易错点 11、决策 36)
- `median` 行补上(pandas 的 `describe()` 只有 `50%`,而测试契约定的是 `median`)
- 入参放宽到 `list` / `tuple` / `ndarray`(兄弟函数都收,`00_quickstart.py` 传的就是 ndarray)
- `weights` 校验补齐:长度、非一维、NaN/inf、负值、和为 0 都抛带原因的错误
(原先和为 0 时裸抛 `ZeroDivisionError`)
- `zscore`:修 RET505,docstring 补 `Parameters` / `Returns` / `Raises`;
`coefficient_of_variation`:均值为 0 由 `TypeError` 改为 `ValueError`(那是取值问题不是类型问题)、
空数组提前报错(原先静默返回 nan + RuntimeWarning)、改掉"异变系数"错别字;分母改用 `|mean|`(**用户本人定**,见决策 38)
- pyright 的 `DataFrame | Series` 重载问题按坑 8 用 `cast` 收窄;ruff 7 条清到 0
- 测试:`test_stats.py` 去掉**模块级** `pytestmark`(它把 6 条测试**全部**跳过了 ——
决策 26 说的就是这个,实现写完了却零覆盖),改成逐条 skip 未实现的 5 条,
并新写 20 条 describe / zscore / 变异系数用例
- [x] `examples/01_clean_and_describe.py`:填掉末尾的 `TODO(L0)`,加一段 `describe` 输出
- [ ] 剩余 4 项:`correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
### 验证结果
| 检查 | venv | conda `mcm-toolkit` |
|---|---|---|
| `pytest` | 见下(单列) | **103 passed, 39 skipped(0 xfail)** |
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
| `ruff check` | All checks passed | **All checks passed**(0 error) |
| `scripts/doctor.ps1` | 跑通 | — |
| SALib / pymoo / pyright | 缺失 | 已装 |
- `pyright`(分析目标 = 工作区 `.venv`):**0 errors, 0 warnings**
(修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与
`docs/04-environment.md` 坑 7)
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
- **142 条用例:103 passed / 39 skipped**。收集数按文件:`test_data.py` 52、`test_api_surface.py` 20、
`test_core.py` 15、`test_solve.py` 9、`test_viz.py` 8、`test_decision.py` 7、`test_stats.py` 26、
`test_models.py` 5
- **`xfail` 清零**:累计 7 条"契约已写进 docstring、实现没跟上"的用例全部转正,
决策 30 那套信号(XPASS → 摘 mark)跑完了一个完整闭环
- `examples/01_clean_and_describe.py` 实测跑通(exit 0);`00_quickstart.py` 仍等 `stats` / `viz`
- 39 条 skip 是等实现的算法(`train_test_split`、`describe`、`bootstrap_ci`、`monte_carlo` 等)
- **本轮回归(已修)**:`transform.py` 上一版有 3 处真 bug —— 修前 **7 failed / 75 passed**,
且 pyright 在同一处报 `reportArgumentType`。**不是静态误报**:pyright CLI 复现 + 测试真红。
结论:见到红线先跑 CLI 与 `pytest`,**不许直接挂 `# type: ignore`**(见个人易错点 10)
- 三个 `.ps1` 通过 PowerShell AST 语法解析
### ⚠️ 需要注意 / 遗留
- [x] ~~两套环境版本不一致~~ → **已解决**:base 升级到 2026.07 后,
numpy/scipy/pandas/matplotlib/sklearn/statsmodels/networkx/sympy 版本完全一致,
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
- [ ] L0 完成 **8/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
`detect_outliers`、`normalize`、`standardize`、`describe`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进;
详情见上面 E 节
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
(`helper_unknown_error: setup refresh had errors`),只有提权
(`require_escalated`)模式可用。沙箱模式只允许写 `D:\MCM&ICM_Toolkit`。
- [ ] **副作用**:沙箱通道(Node 子进程)以 `CodexSandboxOffline` 账户运行,
它写出的文件 Owner 是沙箱账户而非 `guzhu`。
实测 `guzhu` 对这些文件仍有 Modify 权限(继承自 `Authenticated Users`),
读写不受影响;但 git 的 **dubious ownership** 检查会因此误报,
所以 `.git` 必须由本人账户创建(见决策 17)。
若以后再用沙箱通道写文件,注意同一问题。
## 决策记录
| # | 决策 | 理由 |
|---|---|---|
| 1 | 包数从 14 砍到 7 | `regression`/`ml` 重复;`optimization`/`simulation`/`sensitivity` 同族;`dynamics`/`network` 低频 → 降级为模块 |
| 2 | 只保留 1 个 `Result` | 第一版没有任何函数需要区分 5 种结果类型,属过早分类 |
| 3 | 函数清单从 ~250 砍到 ~40 | 4 天赛程的工具,不是 6 个月的框架;没在真题里用过三次的不写 |
| 4 | 删掉 `maximize` / 重复的 `normalize` / 13 个 `plot_*` 等 | 同义或一行转发;`plot(kind=...)` 替代 13 个函数 |
| 5 | 文档从"每函数 10 项"降到 2 档 | 10 项文档的写作成本是实现的 20 倍,是最可能导致项目停滞的一条 |
| 6 | 补上原文缺失的**论文产出链路** `save_table` / `mcm_style` | 美赛是论文比赛;原文只给了一个 `save_figure` |
| 7 | 补上**永久不做清单** | 原文用"建议不要",防不住膨胀;写成硬规则 |
| 8 | venv 用 `--system-site-packages` | 10.5 MB、离线可用;作为轻量开发与断网兜底 |
| 9 | conda 环境用**精准包列表**而非 `--clone base` | 实测 `--clone` 会重放 base 全套包并逐个下载(连 pandoc 都下),远慢于精准列表 |
| 10 | 生成的是**签名 + docstring + NotImplementedError** | 用户要自己读代码学算法;骨架定接口,实现留给学习过程 |
| 11 | `.ps1` 写成**纯 ASCII**(英文注释/输出),而非中文 | Windows PowerShell 5.1 无 BOM 时按 GBK 解析 `.ps1`,中文直接导致语法错误;保留中文就必须加 BOM,违反 AGENTS.md |
| 12 | `.ps1` 用 LF 而非 CRLF | 遵守 AGENTS.md「shell 脚本 UTF-8 无 BOM + LF」;PS 5.1 对 LF 无问题 |
| 13 | `chatGPT_Suggestion.md` 保持原样 | 用户手写的输入文件,不属于本次产物,不做无谓改动 |
| 14 | base 升级用 `conda update -n base --all`,而不是装新 conda | 计划干净(365 更新 / 0 删除 / 0 冲突),且能保留既有的 `lingbot-map` 环境 |
| 15 | 项目环境 numpy **降**到 base 的 2.4.6 | 升 base 的 numpy 会连带把 BLAS 从 MKL 换成 OpenBLAS;宁可降一个 minor,也不动整套线性代数后端 |
| 16 | 新增 `.gitattributes` 强制 LF | 仓库要给队友在 Windows/WSL/Linux 上 clone,不固定行尾会产生"整文件都变了"的假 diff |
| 17 | `.git` 由**本人账户 guzhu** 重建,不做 `safe.directory` 豁免 | 沙箱通道建的 `.git` Owner 是 `CodexSandboxOffline`,git 会报 dubious ownership。加全局豁免是把问题藏起来;直接以本人账户重建,仓库和远程(Gitea)身份一致 |
| 18 | `core.autocrlf=false`(仓库级) | 配合 `.gitattributes` 的 LF 策略,避免 Windows 上 checkout 又转成 CRLF |
| 19 | Gitea 的 SSH **走 Tailscale 内网**,不在公网开 22 | 实测公网 `8.145.36.108:22` 超时(服务器侧过滤,本机出站 22 没被封:github.com:22 可通),而 Tailscale `100.115.86.48:22` 可达且 `git@` 认证直接过 Gitea。remote 里仍写域名,域名→内网 IP 的映射放在 `~/.ssh/config`,换机只改一处 |
| 20 | 首次推送用 `--force-with-lease` **覆盖** Gitea 的 `Initial commit` | 远端那个提交是建仓时自动初始化产生的,与本地无共同祖先,普通 push 必被拒。推送前已把其中的 LICENSE 与手写简介并入本地 `README.md`,原提交仍留在本机对象库(`1c683cf`) |
| 21 | `Result.__repr__` **不打印 `data` 内容**,只给类型与规模 | 模拟轨迹动辄十万个数,repr 一旦刷屏,traceback / 调试器 / notebook 输出就全废了。要看数据请直接取 `result.data` |
| 22 | `Result.summary()` 的行序 = 字典插入顺序,不排序 | 比赛时"想先看哪个指标"自己决定键的插入顺序即可,工具不该替人排序 |
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
| 24 | `get_rng(None)` **复用同一个模块级生成器**(会推进),不每次新建 | **用户本人提出**,取代 Codex 原来"每次新建 `default_rng(全局种子)`"的写法——那样两个都省略 `random_state` 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进 |
| 25 | `src/` 下的实现默认由**用户本人**写,Codex 只改 `tests/` 与 `docs/` | 目标是"一边学一边自己写"。曾出现 Codex 直接落盘 `random.py`、与用户编辑器缓冲区冲突导致保存失败。需要代写时用户会明说 |
| 26 | 未实现的算法用**逐条** `@pytest.mark.skip`,禁用模块级 `pytestmark` | 模块级 skip 会把同文件里**已经写好**的函数一起跳过。本次 `test_data.py` 就这样让 `load_table` 零覆盖,直到有人手动数才被发现 |
| 30 | 已知待修的契约用 `@pytest.mark.xfail(strict=False)`,不用 skip | skip 会让"契约还没写"和"写了但实现有问题"混在一起,5 条 skip 曾把 `clean.py` 的 bug 全藏住。xfail 照样进报告,修好后自动变 XPASS,是"修好了"的信号,而且 XPASS 不失败,不会卡住提交 |
| 29 | 静态分析器钉死到工作区 `.venv`,并把 base 的 site-packages 用 `.pth` 链进去 | pyright / Pylance **不认** `pyvenv.cfg` 的 `include-system-site-packages`,`.venv` 自己只有 `pip`;不钉的话它们会去 PATH 上找 python,本机 PATH 上那个是 Codex 运行时的 3.14 shim,没有科学栈,于是报"无法解析导入"。`.pth` 是唯一被 pyright 认的转发手段 |
| 28 | 不启用 ruff 的 preview 规则集 | `E302`(函数间要 2 个空行)在 ruff 0.16 里是 preview 规则,`select = ["E"]` 默认**不包含**它——所以少一个空行 ruff 照样报 "All checks passed"。行数空行得自己盯,别指望 lint |
| 34 | 常量列(`max == min`)不定区间也不崩:`normalize` 压到 `feature_range` 下界,`standardize` 给 0 | 除零必须有个确定的答案,不能出 nan / inf。给 0 与 `sklearn` 的 `MinMaxScaler` / `StandardScaler` 一致(sklearn 把常量列的 scale 设为 1,结果就是 0),换库不换结论。行为已由 `test_normalize_constant_column_stays_finite` 与 `test_standardize_constant_column_stays_finite` 钉住 |
| 33 | `detect_outliers` 只接受 `DataFrame` / `Series` / `list` / `tuple` / `ndarray`,`dict` 一律 `TypeError` | §4.7 要求非法类型抛 `TypeError` 而非静默兜底。`dict` 以前能"碰巧"被 `pd.DataFrame()` 转成表,但那不是文档承诺的形状,静默接受会让调用方以为传什么都行;`tuple` 则补进文档与 `is_ld_array`,与 `list` 同等对待 |
| 32 | `detect_outliers` 对非数值列恒返回 `False`(既不跳过也不报错) | **用户本人在两条路里选的**。跳过会让 docstring 里"与原数据同形"变成谎话(形状对不上,没法按位置对回原表);报错则让一张带 `city` 列的真题表直接不可用。`False` 的读法是"这一列没有离群点",对文本列是最不坏的解释,也让整表能一次跑完 |
| 31 | Codex 可**直接改** `src/` 的范围:纯 bug(不改接口与语义)、格式、死代码 | 决策 25 说实现归用户,但"能跑"是底线:`winsorize(pd.Series)` 因 `select_dtypes` 在 `Series` 上不存在而直接崩、测试变红,这种必须当场修。凡是要**选一个语义**的(文本列返回什么、mode 要不要就地改)一律留给用户本人,Codex 只把选项和影响列清楚 |
| 27 | base 里的 zstandard 残骸目录**改名禁用**,不删除、不重装 | 删掉不可逆;`pip install zstandard` 会给 base 塞一个 conda 记录里根本没有的包,下次 `conda update --all` 又要重新判断。改名后 `import` 变回正常 `ModuleNotFoundError`,pandas 自动跳过 |
| 35 | `log_transform` 的显式 `offset` 按**定义域前提**校验:`min(x) + offset > 0` 才放行 |
| 36 | `describe` 的返回表**不转置**:行 = 指标、列 = 变量 |
| 37 | 非数值列的处置**按输出契约分**,不按包分:`describe` / `zscore` 直接丢掉;`detect_outliers` 保留列、掩码恒 `False` |
两族函数的产出物不一样:`describe` / `zscore` 吐的是**数值结果表**,文本列塞不进去,丢掉才能得到一张干净、可直接贴论文的表;`detect_outliers` 吐的是**与原表同形的掩码**,必须能按位置对回原表,丢列就废了。"不统一"不是前后矛盾,是契约不同。用户 2026-10-03 拍板:丢掉更好 |
| 38 | `coefficient_of_variation` 用 `σ / |mean|`(**用户本人改的**) |
均值为负时 `σ/μ` 会给出负值,容易被读成"波动更小",其实只是符号跟着均值走。取绝对值后 CV 恒非负。可比性仍只在均值远离 0 的比率尺度数据上成立(已写进 docstring 的 Notes) |
| 39 | 工作约定:**语义 / 非法输入口径由用户拍板,Codex 只列选项与影响、不代选**;**ruff、`STATUS.md`、`CHANGELOG.md`、`tests/`、docstring 这类与算法无关的工程事务,Codex 顺手补掉** |
用户 2026-10-03 明确:没考虑到的非法情况"告诉我我自己改"——因为那是设计决策,得由写实现的人形成判断;而 lint / 进度板 / 测试这类机械活再由用户动手只是浪费。这条与决策 25 / 31 配套:25 定"谁写 src",31 定"Codex 可改的 bug 范围",39 定"非算法事务的默认归口" |
docstring 与 `test_describe_reports_expected_keys` 早就写死了这个方向,实现里多做的 `.T` 是净负担 —— 且不是"风格不同"那么轻:转置后 `.loc[指标名] = <按变量索引的 Series>` 与列名零交集,pandas **不报错**、把四行整行填成 NaN。方向约定以"能按列名对齐"为准,见易错点 11 |
原码写的是 `if offset > 0: raise`,把唯一合法的用例(正 offset 正好抬定义域)全拒了,报错文案却是"offset 必须大于 0",自相矛盾。改成校验真正的前提:`log` 要求自变量取正。`offset=0` 对全正数据合法(`offset=None` 的自动分支本来就取 0)。**Codex 代选、待用户确认** —— 若原意是"只收正 offset",改回即可 |
## 下一步
1. **`correlation_matrix` 是 L0 里下一个该写的**(A 档,`stats/inference.py`)——
写法和 `describe` 一样:先想"替代了哪段手写代码" → 先写一条会红的测试 → 再实现
→ 补 `examples/` 一行(`docs/03-roadmap.md` §3.2)。
2. 之后按 L0 剩余顺序推进:`bootstrap_ci`(**S 档,要写函数卡片**)→ `monte_carlo`(**S 档**)
→ `save_figure`(A)。
3. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
4. **三处口径已拍板**(2026-10-03):
- `log_transform` 的显式 `offset`:**按现状**(定义域前提校验,决策 35)✓
- 非数值列:`describe` / `zscore` **默认丢掉**,与 `detect_outliers` 的"保留列、掩码恒 False"
**不统一** —— 两者输出契约不同,理由见决策 37 ✓
- 变异系数:已改用 `σ / |μ|`(决策 38)✓