Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49fd84a2df | ||
|
|
b6456d97d8 | ||
|
|
3cdbba4cfb |
No files matched your search
+15
-2
@@ -26,16 +26,29 @@
|
|||||||
`set_seed` 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。
|
`set_seed` 同时覆盖 numpy 旧全局状态,让 sklearn / statsmodels 也可复现。
|
||||||
- `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。
|
- `core.Result`:补上 `__repr__` 与 `summary()` 实现(L0 第 1 项落地)。
|
||||||
`repr` 紧凑单行且不打印 `data` 内容;`summary()` 输出可直接贴进论文的 Markdown 表格。
|
`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
|
### Added
|
||||||
- `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、
|
- `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、
|
||||||
非法类型必须抛 `TypeError`。配套在 `docs/03-roadmap.md` 的开发纪律里加了指针。
|
非法类型必须抛 `TypeError`。配套在 `docs/03-roadmap.md` 的开发纪律里加了指针。
|
||||||
- `docs/dev/personal-pitfalls.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
|
### 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 顺序填实现。
|
这是刻意的:先定接口,再按 `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
|
## [0.0.1] - 2026-10-01
|
||||||
|
|
||||||
|
|||||||
@@ -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`),
|
- [x] 推送通道:公网 22 被防火墙过滤 → 改用 Tailscale(`100.115.86.48:22`),
|
||||||
`~/.ssh/config` 加了一条域名别名,remote 里仍写域名
|
`~/.ssh/config` 加了一条域名别名,remote 里仍写域名
|
||||||
|
|
||||||
**E. L0 实现进度(2/12)**
|
**E. L0 实现进度(3/12)**
|
||||||
- [x] `core/result.py` 的 `Result`
|
- [x] `core/result.py` 的 `Result`
|
||||||
- `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印
|
- `__repr__`:单行紧凑;`data` 只给类型与规模,**绝不**整数组打印
|
||||||
(十万级数组的 repr 实测仍 < 200 字符);`success` 恒在
|
(十万级数组的 repr 实测仍 < 200 字符);`success` 恒在
|
||||||
@@ -75,7 +79,14 @@
|
|||||||
- `set_seed` 同时调 `np.random.seed`,覆盖 sklearn / statsmodels 走的 numpy 旧全局
|
- `set_seed` 同时调 `np.random.seed`,覆盖 sklearn / statsmodels 走的 numpy 旧全局
|
||||||
(全项目**唯一**碰旧接口的地方,带 `# noqa: NPY002` 与理由)
|
(全项目**唯一**碰旧接口的地方,带 `# noqa: NPY002` 与理由)
|
||||||
- 测试:`TestRandom` 共 7 条(可复现、透传、无参跟随、旧全局、np.integer、非法类型、共享推进)
|
- 测试:`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`
|
`detect_outliers` `normalize` `standardize` `describe` `correlation_matrix`
|
||||||
`bootstrap_ci` `monte_carlo` `save_figure`
|
`bootstrap_ci` `monte_carlo` `save_figure`
|
||||||
|
|
||||||
@@ -83,15 +94,16 @@
|
|||||||
|
|
||||||
| 检查 | venv | conda `mcm-toolkit` |
|
| 检查 | 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 项 |
|
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||||
| `ruff check` | All checks passed | All checks passed |
|
| `ruff check` | All checks passed | All checks passed |
|
||||||
| `scripts/doctor.ps1` | 跑通 | — |
|
| `scripts/doctor.ps1` | 跑通 | — |
|
||||||
| SALib / pymoo / pyright | 缺失 | 已装 |
|
| SALib / pymoo / pyright | 缺失 | 已装 |
|
||||||
|
|
||||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||||
- skip 的都是等实现的算法行为测试;通过的 31 条 = 20 条公开 API 契约测试
|
- skip 的都是等实现的算法行为测试;通过的 39 条 = 20 条公开 API 契约测试
|
||||||
(`tests/test_api_surface.py`)+ 11 条 `core` 行为测试(`tests/test_core.py`)
|
(`tests/test_api_surface.py`)+ 11 条 `core` 行为测试(`tests/test_core.py`)
|
||||||
|
+ 8 条 `data` 行为测试(`tests/test_data.py`)
|
||||||
- 三个 `.ps1` 通过 PowerShell AST 语法解析
|
- 三个 `.ps1` 通过 PowerShell AST 语法解析
|
||||||
|
|
||||||
### ⚠️ 需要注意 / 遗留
|
### ⚠️ 需要注意 / 遗留
|
||||||
@@ -101,7 +113,7 @@
|
|||||||
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
||||||
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
||||||
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
||||||
- [ ] L0 完成 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 节
|
详情见上面 E 节
|
||||||
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
|
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
|
||||||
(`helper_unknown_error: setup refresh had errors`),只有提权
|
(`helper_unknown_error: setup refresh had errors`),只有提权
|
||||||
@@ -142,11 +154,20 @@
|
|||||||
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
|
| 23 | `set_seed` 内部**允许**调用 `np.random.seed`(带 noqa) | sklearn / statsmodels 默认走 numpy 旧全局状态,只记自己的 `_GLOBAL_SEED` 覆盖不到它们,"提交能重跑"就是假的。旧接口在这一个点上不是"取随机数"而是"设种子",性质不同 |
|
||||||
| 24 | `get_rng(None)` **复用同一个模块级生成器**(会推进),不每次新建 | **用户本人提出**,取代 Codex 原来"每次新建 `default_rng(全局种子)`"的写法——那样两个都省略 `random_state` 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进 |
|
| 24 | `get_rng(None)` **复用同一个模块级生成器**(会推进),不每次新建 | **用户本人提出**,取代 Codex 原来"每次新建 `default_rng(全局种子)`"的写法——那样两个都省略 `random_state` 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进 |
|
||||||
| 25 | `src/` 下的实现默认由**用户本人**写,Codex 只改 `tests/` 与 `docs/` | 目标是"一边学一边自己写"。曾出现 Codex 直接落盘 `random.py`、与用户编辑器缓冲区冲突导致保存失败。需要代写时用户会明说 |
|
| 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`
|
1. **`data/io.py` 待你修两处**(`src/` 归你,Codex 不代写,见决策 25):
|
||||||
(细节与换机步骤见 `docs/05-git-workflow.md` §3)。
|
- `save_table` 把 `path.parent.mkdir(parents=True, exist_ok=True)` 放在了**后缀校验之前**,
|
||||||
2. 继续 L0,**下一个:`data/io.py` 的 `load_table`**(`save_table` 属 L1,顺手也可做)。
|
传个 `.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/` 里加一行最小用法,并更新本文件。
|
3. 每写完一个函数,在 `examples/` 里加一行最小用法,并更新本文件。
|
||||||
4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
|
4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
|
||||||
@@ -129,6 +129,41 @@ Remove-Item -LiteralPath 'D:\anaconda\envs\mcm-toolkit' -Recurse -Force
|
|||||||
环境拿到的是更新的版本(pandas 从 2.3.3 跳到 **3.0.6**,是 major 升级)。
|
环境拿到的是更新的版本(pandas 从 2.3.3 跳到 **3.0.6**,是 major 升级)。
|
||||||
见 §5,写代码时注意别依赖两套环境都有的行为。
|
见 §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` 恢复正常。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、两套环境的实际版本(已对齐)
|
## 五、两套环境的实际版本(已对齐)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。
|
两处打架时合法用法会被 IDE 标红,真正的错误反而看不出来。
|
||||||
- 以后怎么防:改签名和改 docstring 必须同一次做完。
|
- 以后怎么防:改签名和改 docstring 必须同一次做完。
|
||||||
本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。
|
本项目所有 `random_state` 的注解统一写 `int | np.integer | np.random.Generator | None`。
|
||||||
- 首次:2026-10-01|复发:0
|
- 首次:2026-10-01|复发:1
|
||||||
|
|
||||||
### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局
|
### 4. 只覆盖自己的状态,忘了第三方库还在用 numpy 旧全局
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
__all__ = ["load_table", "save_table"]
|
__all__ = ["load_table", "save_table"]
|
||||||
|
|
||||||
|
|
||||||
@@ -27,11 +29,41 @@ def load_table(path: str | Path, **kwargs: Any) -> Any:
|
|||||||
>>> df = load_table("data.csv") # doctest: +SKIP
|
>>> df = load_table("data.csv") # doctest: +SKIP
|
||||||
>>> df.shape # doctest: +SKIP
|
>>> df.shape # doctest: +SKIP
|
||||||
"""
|
"""
|
||||||
# TODO(L0): 编码探测 + 分派;Excel 缺 openpyxl 时给出可读报错。
|
path = Path(path)
|
||||||
raise NotImplementedError
|
suffix = path.suffix.lower()
|
||||||
|
|
||||||
|
encodings = ['utf-8','utf-8-sig','gbk','gb18030']
|
||||||
|
kwargs.setdefault('na_values',['','NA','N/A','#N/A','null','-'])
|
||||||
|
|
||||||
|
if suffix == ".csv":
|
||||||
|
if 'encoding' in kwargs:
|
||||||
|
return pd.read_csv(path,**kwargs)
|
||||||
|
for enc in encodings:
|
||||||
|
try:
|
||||||
|
return pd.read_csv(path,encoding=enc,**kwargs)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise FileNotFoundError(f"在这里找不到文件:{path}") from None
|
||||||
|
raise ValueError(f"无法解码文件,目前支持解码方式:{encodings}")
|
||||||
|
if suffix in ['.xlsx','.xls']:
|
||||||
|
return pd.read_excel(path,**kwargs)
|
||||||
|
raise ValueError(f"不支持的文件后缀:{suffix}")
|
||||||
|
|
||||||
|
|
||||||
def save_table(df: Any, path: str | Path, index: bool = False, **kwargs: Any) -> Path:
|
def save_table(df: pd.DataFrame, path: str | Path, index: bool = False, **kwargs: Any) -> Path:
|
||||||
"""保存 DataFrame,返回实际写入的路径。"""
|
"""保存 DataFrame,返回实际写入的路径。"""
|
||||||
# TODO(L0)
|
path = Path(path)
|
||||||
raise NotImplementedError
|
path.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
suffix = path.suffix.lower()
|
||||||
|
|
||||||
|
if not isinstance(df,pd.DataFrame):
|
||||||
|
raise TypeError(f"df 必须是一个pandas DataFrame,但是你上传了{type(df)}")
|
||||||
|
|
||||||
|
if suffix == '.csv':
|
||||||
|
df.to_csv(path,index=index,**kwargs)
|
||||||
|
elif suffix in ['.xlsx','.xls']:
|
||||||
|
df.to_excel(path,index=index,**kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的文件后缀:{suffix}")
|
||||||
|
return path
|
||||||
+142
-47
@@ -1,58 +1,22 @@
|
|||||||
"""data 包测试。"""
|
"""data 包测试。
|
||||||
|
|
||||||
|
L0 阶段的约定:**只测已实现的行为**。未实现的算法用逐条 skip 标出,
|
||||||
|
不要用模块级 pytestmark —— 那会把同文件里已实现的函数一起跳过,
|
||||||
|
让 load_table 这种已经写好的函数变成零覆盖。
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1")
|
_NOT_IMPLEMENTED = "未实现:见 docs/03-roadmap.md L0/L1"
|
||||||
|
|
||||||
|
|
||||||
def test_detect_outliers_finds_planted_points():
|
# ---------------------------------------------------------------- io: load_table
|
||||||
"""人工埋 3 个离群点,IQR 法应正好命中 3 个。"""
|
|
||||||
from mcm_toolkit.data import detect_outliers
|
|
||||||
|
|
||||||
x = np.concatenate([np.arange(100.0), [1e6, -1e6, 1e7]])
|
|
||||||
mask = np.asarray(detect_outliers(x, method="iqr"))
|
|
||||||
assert mask.sum() == 3
|
|
||||||
assert mask[-3:].all()
|
|
||||||
|
|
||||||
|
|
||||||
def test_detect_outliers_returns_mask_not_filtered_data():
|
|
||||||
"""约定:只标不删——返回与原数据同形的布尔掩码。"""
|
|
||||||
from mcm_toolkit.data import detect_outliers
|
|
||||||
|
|
||||||
x = np.arange(50.0)
|
|
||||||
mask = np.asarray(detect_outliers(x, method="zscore"))
|
|
||||||
assert mask.shape == x.shape
|
|
||||||
assert mask.dtype == bool
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_minmax_range(rng):
|
|
||||||
from mcm_toolkit.data import normalize
|
|
||||||
|
|
||||||
x = rng.normal(size=(50, 3))
|
|
||||||
out = np.asarray(normalize(x, method="minmax"))
|
|
||||||
assert np.allclose(out.min(axis=0), 0.0)
|
|
||||||
assert np.allclose(out.max(axis=0), 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_standardize_has_zero_mean_unit_std(rng):
|
|
||||||
from mcm_toolkit.data import standardize
|
|
||||||
|
|
||||||
x = rng.normal(5, 3, size=1000)
|
|
||||||
out = np.asarray(standardize(x))
|
|
||||||
assert abs(out.mean()) < 1e-9
|
|
||||||
assert abs(out.std() - 1.0) < 1e-9
|
|
||||||
|
|
||||||
|
|
||||||
def test_train_test_split_sizes_and_no_overlap(rng):
|
|
||||||
from mcm_toolkit.data import train_test_split
|
|
||||||
|
|
||||||
X = rng.normal(size=(100, 2))
|
|
||||||
Xtr, Xte = train_test_split(X, test_size=0.2, random_state=0)
|
|
||||||
assert len(Xtr) == 80 and len(Xte) == 20
|
|
||||||
assert len({tuple(r) for r in Xtr} & {tuple(r) for r in Xte}) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_table_detects_gbk(tmp_path):
|
def test_load_table_detects_gbk(tmp_path):
|
||||||
@@ -63,3 +27,134 @@ def test_load_table_detects_gbk(tmp_path):
|
|||||||
p.write_bytes("城市,人口\n北京,2189\n".encode("gbk"))
|
p.write_bytes("城市,人口\n北京,2189\n".encode("gbk"))
|
||||||
df = load_table(p)
|
df = load_table(p)
|
||||||
assert list(df.columns) == ["城市", "人口"]
|
assert list(df.columns) == ["城市", "人口"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_table_keeps_utf8_readable(tmp_path):
|
||||||
|
"""UTF-8 文件必须按 UTF-8 读出,不能先试 GBK 变成乱码。"""
|
||||||
|
from mcm_toolkit.data import load_table
|
||||||
|
|
||||||
|
p = tmp_path / "utf8.csv"
|
||||||
|
p.write_text("省份,GDP增速\n广东,5.8\n", encoding="utf-8")
|
||||||
|
df = load_table(p)
|
||||||
|
assert list(df.columns) == ["省份", "GDP增速"]
|
||||||
|
assert df.loc[0, "省份"] == "广东"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_table_maps_na_like_tokens_to_nan(tmp_path):
|
||||||
|
"""赛题里的缺失值长得五花八门,默认 na_values 要全部吃成 NaN。"""
|
||||||
|
from mcm_toolkit.data import load_table
|
||||||
|
|
||||||
|
p = tmp_path / "na.csv"
|
||||||
|
p.write_text("a,b,c\n1,NA,\n2,-,#N/A\n", encoding="utf-8")
|
||||||
|
df = load_table(p)
|
||||||
|
assert df["b"].isna().all()
|
||||||
|
assert df["c"].isna().all()
|
||||||
|
assert df["a"].notna().all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_table_respects_explicit_encoding(tmp_path):
|
||||||
|
"""显式给了 encoding 就照办,不再做探测。"""
|
||||||
|
from mcm_toolkit.data import load_table
|
||||||
|
|
||||||
|
p = tmp_path / "gbk.csv"
|
||||||
|
p.write_bytes("城市,人口\n上海,2487\n".encode("gbk"))
|
||||||
|
df = load_table(p, encoding="gbk")
|
||||||
|
assert list(df.columns) == ["城市", "人口"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_table_rejects_unknown_suffix(tmp_path):
|
||||||
|
"""不支持的扩展名要抛 ValueError,而且要带上后缀名。"""
|
||||||
|
from mcm_toolkit.data import load_table
|
||||||
|
|
||||||
|
p = tmp_path / "data.txt"
|
||||||
|
p.write_text("a,b\n1,2\n", encoding="utf-8")
|
||||||
|
with pytest.raises(ValueError, match=r"\.txt"):
|
||||||
|
load_table(p)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_table_missing_file_raises(tmp_path):
|
||||||
|
"""文件不存在时要把路径原样带出来,方便定位。"""
|
||||||
|
from mcm_toolkit.data import load_table
|
||||||
|
|
||||||
|
missing = tmp_path / "nope.csv"
|
||||||
|
with pytest.raises(FileNotFoundError, match=r"nope\.csv"):
|
||||||
|
load_table(missing)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- io: save_table
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_table_round_trip(tmp_path):
|
||||||
|
"""存进去再读出来,内容要一致,并且返回实际写入的 Path。"""
|
||||||
|
from mcm_toolkit.data import load_table, save_table
|
||||||
|
|
||||||
|
df = pd.DataFrame({"城市": ["北京", "上海"], "人口": [2189.0, 2487.0]})
|
||||||
|
out = save_table(df, tmp_path / "out.csv")
|
||||||
|
assert isinstance(out, Path)
|
||||||
|
assert out.exists()
|
||||||
|
back = load_table(out)
|
||||||
|
assert list(back.columns) == ["城市", "人口"]
|
||||||
|
assert back["人口"].tolist() == [2189.0, 2487.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_table_rejects_non_dataframe(tmp_path):
|
||||||
|
"""传错类型要当场报错,不要写出一个奇怪的文件。"""
|
||||||
|
from mcm_toolkit.data import save_table
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
save_table([1, 2, 3], tmp_path / "bad.csv")
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------- 以下为未实现算法(L0/L1 待写)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
def test_detect_outliers_finds_planted_points():
|
||||||
|
"""人工埋 3 个离群点,IQR 法应正好命中 3 个。"""
|
||||||
|
from mcm_toolkit.data import detect_outliers
|
||||||
|
|
||||||
|
x = np.concatenate([np.arange(100.0), [1e6, -1e6, 1e7]])
|
||||||
|
mask = np.asarray(detect_outliers(x, method="iqr"))
|
||||||
|
assert mask.sum() == 3
|
||||||
|
assert mask[-3:].all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
def test_detect_outliers_returns_mask_not_filtered_data():
|
||||||
|
"""约定:只标不删——返回与原数据同形的布尔掩码。"""
|
||||||
|
from mcm_toolkit.data import detect_outliers
|
||||||
|
|
||||||
|
x = np.arange(50.0)
|
||||||
|
mask = np.asarray(detect_outliers(x, method="zscore"))
|
||||||
|
assert mask.shape == x.shape
|
||||||
|
assert mask.dtype == bool
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
def test_normalize_minmax_range(rng):
|
||||||
|
from mcm_toolkit.data import normalize
|
||||||
|
|
||||||
|
x = rng.normal(size=(50, 3))
|
||||||
|
out = np.asarray(normalize(x, method="minmax"))
|
||||||
|
assert np.allclose(out.min(axis=0), 0.0)
|
||||||
|
assert np.allclose(out.max(axis=0), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
def test_standardize_has_zero_mean_unit_std(rng):
|
||||||
|
from mcm_toolkit.data import standardize
|
||||||
|
|
||||||
|
x = rng.normal(5, 3, size=1000)
|
||||||
|
out = np.asarray(standardize(x))
|
||||||
|
assert abs(out.mean()) < 1e-9
|
||||||
|
assert abs(out.std() - 1.0) < 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
|
def test_train_test_split_sizes_and_no_overlap(rng):
|
||||||
|
from mcm_toolkit.data import train_test_split
|
||||||
|
|
||||||
|
X = rng.normal(size=(100, 2))
|
||||||
|
Xtr, Xte = train_test_split(X, test_size=0.2, random_state=0)
|
||||||
|
assert len(Xtr) == 80 and len(Xte) == 20
|
||||||
|
assert len({tuple(r) for r in Xtr} & {tuple(r) for r in Xte}) == 0
|
||||||
Reference in new issue
Block a user