From 49fd84a2df15479b33eacea8905851843c59ae22 Mon Sep 17 00:00:00 2001 From: guzhujushi Date: Thu, 1 Oct 2026 23:11:48 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20L0=20=E8=BF=9B=E5=BA=A6=203/12=EF=BC=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20zstandard=20=E6=AE=8B=E9=AA=B8=E5=9D=91?= =?UTF-8?q?=E4=B8=8E=E5=86=B3=E7=AD=96=2026/27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STATUS.md:本次任务拆成「第一阶段已完成 / 第二阶段进行中」; L0 3/12;测试计数改为 39 passed / 44 skipped; 下一步里写明 io.py 待修的两处(mkdir 早于校验、缺 Raises) - docs/04-environment.md 坑 6:conda 升级留下的 *.conda_trash 残骸目录会被 Python 当成 namespace package 导入成功(__file__ 为 None), pandas 3.0 因此抛 ImportError: Can't determine version for zstandard 直接打断 pd.read_csv;含排查方法与「改名禁用」的修法 - docs/dev/personal-pitfalls.md:条目 3(注解与 docstring 打架)复发 +1 - 决策 26:未实现的算法逐条 skip,禁用模块级 pytestmark - 决策 27:zstandard 残骸改名禁用,不删不重装 - CHANGELOG:补 load_table/save_table 与 Fixed 段 --- CHANGELOG.md | 17 +++++++++++++-- STATUS.md | 41 ++++++++++++++++++++++++++--------- docs/04-environment.md | 35 ++++++++++++++++++++++++++++++ docs/dev/personal-pitfalls.md | 2 +- 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 555a7bd..6a8bb56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,16 +26,29 @@ `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`。 ### Added - `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、 非法类型必须抛 `TypeError`。配套在 `docs/03-roadmap.md` 的开发纪律里加了指针。 - `docs/dev/personal-pitfalls.md`:个人易错点清单,**只追加、不通读** +### Fixed +- `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。 + ### Notes -- 除 `core.Result`、`core.get_rng`、`core.set_seed` 外,其余函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。 +- 除 `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` 未安装(本机离线)。 +- 已知缺口:`SALib`(Sobol/Morris)、`pymoo`、`pyright` 已装进 conda 环境 `mcm-toolkit`,`.venv` 里没有。 ## [0.0.1] - 2026-10-01 diff --git a/STATUS.md b/STATUS.md index 739c368..ae0aee6 100644 --- a/STATUS.md +++ b/STATUS.md @@ -4,9 +4,13 @@ ## 本次任务 -审查并精简 ChatGPT 的建库建议 → 生成骨架文件 → 配置 conda 与 venv 环境 → 初始化 git 仓库 → 接入 Gitea 远端。 +**第一阶段(已完成)**:审查并精简 ChatGPT 的建库建议 → 生成骨架文件 → +配置 conda 与 venv 环境 → 初始化 git 仓库 → 接入 Gitea 远端。 +两套环境均已建成并通过验证,仓库已推送到 `code.guzhujushi.cn`。 -**结论:全部完成。** 两套环境均已建成并通过验证;仓库已推送到 `code.guzhujushi.cn`。 +**第二阶段(进行中)**:按 `docs/03-roadmap.md` 的 L0 → L1 顺序**逐个实现函数**, +用户本人写 `src/`、Codex 负责审查 + `tests/`(决策 25)。 +当前 3/12:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`。 ## 任务清单 @@ -58,7 +62,7 @@ - [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`), `~/.ssh/config` 加了一条域名别名,remote 里仍写域名 -**E. L0 实现进度(2/12)** +**E. L0 实现进度(3/12)** - [x] `core/result.py` 的 `Result` - `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印 (十万级数组的 repr 实测仍 < 200 字符);`success` 恒在 @@ -75,7 +79,14 @@ - `set_seed` 同时调 `np.random.seed`,覆盖 sklearn / statsmodels 走的 numpy 旧全局 (全项目**唯一**碰旧接口的地方,带 `# noqa: NPY002` 与理由) - 测试:`TestRandom` 共 7 条(可复现、透传、无参跟随、旧全局、np.integer、非法类型、共享推进) -- [ ] 剩余 10 项(11 个函数):`load_table` `clean_missing` +- [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` + - 测试:`tests/test_data.py` 由 1 条(且被模块级 skip 掉)扩到 **8 条** +- [ ] 剩余 9 项(10 个函数):`clean_missing` `detect_outliers` `normalize` `standardize` `describe` `correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure` @@ -83,15 +94,16 @@ | 检查 | venv | conda `mcm-toolkit` | |---|---|---| -| `pytest` | 31 passed, 45 skipped | 31 passed, 45 skipped | +| `pytest` | **39 passed, 44 skipped** | **39 passed, 44 skipped** | | `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 | | `ruff check` | All checks passed | All checks passed | | `scripts/doctor.ps1` | 跑通 | — | | SALib / pymoo / pyright | 缺失 | 已装 | - base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致) -- skip 的都是等实现的算法行为测试;通过的 31 条 = 20 条公开 API 契约测试 +- skip 的都是等实现的算法行为测试;通过的 39 条 = 20 条公开 API 契约测试 (`tests/test_api_surface.py`)+ 11 条 `core` 行为测试(`tests/test_core.py`) + + 8 条 `data` 行为测试(`tests/test_data.py`) - 三个 `.ps1` 通过 PowerShell AST 语法解析 ### ⚠️ 需要注意 / 遗留 @@ -101,7 +113,7 @@ 只差 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 完成 2/12(`Result`、`get_rng`/`set_seed`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进; +- [ ] L0 完成 **3/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`),其余按 `docs/03-roadmap.md` 的 L0 → L1 顺序推进; 详情见上面 E 节 - [ ] 本机 `exec_command` 在沙箱模式下**完全不可用** (`helper_unknown_error: setup refresh had errors`),只有提权 @@ -142,11 +154,20 @@ | 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` 零覆盖,直到有人手动数才被发现 | +| 27 | base 里的 zstandard 残骸目录**改名禁用**,不删除、不重装 | 删掉不可逆;`pip install zstandard` 会给 base 塞一个 conda 记录里根本没有的包,下次 `conda update --all` 又要重新判断。改名后 `import` 变回正常 `ModuleNotFoundError`,pandas 自动跳过 | ## 下一步 -1. ~~接入 Gitea~~ **已完成**。以后推送前先确认 Tailscale 在线,然后 `git push` - (细节与换机步骤见 `docs/05-git-workflow.md` §3)。 -2. 继续 L0,**下一个:`data/io.py` 的 `load_table`**(`save_table` 属 L1,顺手也可做)。 +1. **`data/io.py` 待你修两处**(`src/` 归你,Codex 不代写,见决策 25): + - `save_table` 把 `path.parent.mkdir(parents=True, exist_ok=True)` 放在了**后缀校验之前**, + 传个 `.txt` 会先建出一个空目录再抛 `ValueError`。 + 把校验(后缀 + `isinstance(df, pd.DataFrame)`)挪到 `mkdir` 前面即可。 + - 两个函数都缺 `Raises` 段落(`docs/02-architecture.md` §4.7 要求写全); + `load_table` 的返回注解建议从 `Any` 改成 `pd.DataFrame` —— docstring 已经承诺返回 DataFrame, + 注解写 `Any` 等于同一个契约写了两遍(`docs/dev/personal-pitfalls.md` 条目 3,本次已复发一次)。 +2. 继续 L0,**下一个:`data/clean.py` 的 `clean_missing`**(L0 / A 档,直接能写)。 + 同文件的 `detect_outliers` 是 **S 档**,要先按 `docs/dev/function-card-template.md` + 把函数卡片写出来再动手。 3. 每写完一个函数,在 `examples/` 里加一行最小用法,并更新本文件。 4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。 diff --git a/docs/04-environment.md b/docs/04-environment.md index a1f0e59..df82a50 100644 --- a/docs/04-environment.md +++ b/docs/04-environment.md @@ -129,6 +129,41 @@ Remove-Item -LiteralPath 'D:\anaconda\envs\mcm-toolkit' -Recurse -Force 环境拿到的是更新的版本(pandas 从 2.3.3 跳到 **3.0.6**,是 major 升级)。 见 §5,写代码时注意别依赖两套环境都有的行为。 +**坑 6 —— conda 升级留下的 `*.conda_trash` 残骸会被 Python 当成包导入。** +base 从 2025.12 升到 2026.07 后,`D:\anaconda\Lib\site-packages\zstandard\` 里只剩一个 +`backend_c.cp313-win_amd64.pyd.c~.conda_trash`。目录没了 `__init__.py`,Python 就把它当作 +**namespace package** 导入成功,于是: + +```text +>>> import zstandard +>>> zstandard.__file__ # None —— 导入"成功"了,但什么都没拿到 +>>> zstandard.__version__ +AttributeError +``` + +pandas 3.0 的 `import_optional_dependency("zstandard")` 拿到这种半残包时,抛的不是 +`ModuleNotFoundError`(那会被正常忽略),而是 +`ImportError: Can't determine version for zstandard`,直接**打断 `pd.read_csv`**。 +症状很迷惑:同一个脚本今天能跑明天不能跑,报错还指向 csv 读取。 + +**怎么查**(关键是 `__file__` 和 `__version__` 两项一起看): + +```powershell +& 'D:\anaconda\python.exe' -c "import zstandard; print(zstandard.__file__, getattr(zstandard,'__version__','MISSING'))" +Get-ChildItem -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' -Force +``` + +**怎么修**:确认 conda 记录里本来就没有这个包(`conda list -n base | findstr zstandard` +只该看到 `backports.zstd` / `zstd`),再把这个残骸目录改名禁用(比删掉可逆): + +```powershell +Move-Item -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' ` + -Destination 'D:\anaconda\Lib\site-packages\zstandard.disabled-conda-trash' +``` + +改名后 `import zstandard` 变成正常的 `ModuleNotFoundError`,pandas 会安静地跳过它。 +本次已按此处理,`.venv` 的 `pd.read_csv` 恢复正常。 + --- ## 五、两套环境的实际版本(已对齐) diff --git a/docs/dev/personal-pitfalls.md b/docs/dev/personal-pitfalls.md index d35e87c..df62861 100644 --- a/docs/dev/personal-pitfalls.md +++ b/docs/dev/personal-pitfalls.md @@ -55,7 +55,7 @@ 两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。 - 以后怎么防:改签名和改 docstring 必须同一次做完。 本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。 -- 首次:2026-10-01|复发:0 +- 首次:2026-10-01|复发:1 ### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局