Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c683cfb4d |
No files matched your search
@@ -1,30 +0,0 @@
|
||||
# 统一编辑器行为。多人(三人都用同一仓库)时能省掉大量格式争吵。
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.py]
|
||||
max_line_length = 100
|
||||
|
||||
[*.{yml,yaml,json,toml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# PowerShell 脚本:UTF-8 无 BOM + LF(见 AGENTS.md)。
|
||||
# 且必须是纯 ASCII —— Windows PowerShell 5.1 在无 BOM 时按 ANSI 解析 .ps1,
|
||||
# 非 ASCII(例如中文)会变成非法字节并直接导致语法错误。
|
||||
[*.ps1]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.{bat,cmd}]
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
@@ -1,51 +0,0 @@
|
||||
# 行尾策略:仓库内统一 LF,checkout 不做自动转换。
|
||||
#
|
||||
# 理由见 AGENTS.md:shell 脚本必须 UTF-8 无 BOM + LF。
|
||||
# 这个项目还会被队友在 Windows / WSL / Linux 上 clone,
|
||||
# 不固定行尾的话,每次提交都会出现"整文件都变了"的假 diff。
|
||||
* text=auto eol=lf
|
||||
|
||||
# ---- 文本,显式声明 ----
|
||||
*.py text eol=lf
|
||||
*.md text eol=lf
|
||||
*.toml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.json text eol=lf
|
||||
*.cfg text eol=lf
|
||||
*.txt text eol=lf
|
||||
*.tex text eol=lf
|
||||
*.bib text eol=lf
|
||||
.gitignore text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
.editorconfig text eol=lf
|
||||
|
||||
# PowerShell:必须是纯 ASCII + LF(Windows PowerShell 5.1 无 BOM 时按 ANSI 解析,
|
||||
# 中文会直接导致语法错误)。见 docs/04-environment.md §8。
|
||||
*.ps1 text eol=lf
|
||||
|
||||
# 批处理必须 CRLF,否则 cmd 解析会出问题
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# ---- 二进制,禁止任何转换 ----
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.pdf binary
|
||||
*.eps binary
|
||||
*.svg text
|
||||
*.xlsx binary
|
||||
*.xls binary
|
||||
*.docx binary
|
||||
*.pptx binary
|
||||
*.zip binary
|
||||
*.exe binary
|
||||
*.dll binary
|
||||
*.db binary
|
||||
*.ico binary
|
||||
|
||||
# ---- diff 行为 ----
|
||||
# 笔记本是 JSON 文本,可以正常 diff,但不要参与语言统计
|
||||
*.ipynb -linguist-detectable
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
# ---- Python ----
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
*.egg
|
||||
build/
|
||||
dist/
|
||||
.eggs/
|
||||
|
||||
# ---- 虚拟环境 ----
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
.conda/
|
||||
*.conda-env/
|
||||
|
||||
# ---- 测试 / 类型检查 ----
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
.pyright/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# ---- Jupyter ----
|
||||
.ipynb_checkpoints/
|
||||
*.ipynb.bak
|
||||
|
||||
# ---- IDE ----
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# ---- 论文产出(本地生成,不入库)----
|
||||
# 图表的中间产物不入库;定稿的成品图请放到 problems/<年份>_<题号>/figures/ 再提交
|
||||
figures/*.png
|
||||
figures/*.pdf
|
||||
!figures/.gitkeep
|
||||
tables/*.csv
|
||||
!tables/.gitkeep
|
||||
|
||||
# ---- 真题原始数据 ----
|
||||
# 赛题原始数据文件(体积大、可重新下载)不入库,只提交你写的处理脚本和结论
|
||||
problems/**/raw/
|
||||
problems/**/*.xlsx
|
||||
problems/**/*.csv
|
||||
!problems/**/README.md
|
||||
|
||||
# ---- 临时文件 ----
|
||||
tmp/
|
||||
*.log
|
||||
*.tmp
|
||||
.probe.tmp
|
||||
|
||||
# ---- 系统 ----
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# 受限/沙箱环境里 PowerShell 会以当前工作目录为基准生成字面量 "%SystemDrive%"
|
||||
# 目录(实际内容是 %SystemDrive%\ProgramData\Microsoft\Windows\Caches,
|
||||
# 即 COM 注册表缓存)。正常终端不会出现,删掉也会再生成,所以直接忽略。
|
||||
%SystemDrive%/
|
||||
@@ -1,31 +0,0 @@
|
||||
# 更新日志
|
||||
|
||||
格式参考 [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
|
||||
|
||||
### Notes
|
||||
- 所有函数体**尚未实现**(统一 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记)。
|
||||
这是刻意的:先定接口,再按 `docs/03-roadmap.md` 的 L0 → L1 顺序填实现。
|
||||
- 已知缺口:`SALib`(Sobol/Morris)、`pymoo`、`pyright` 未安装(本机离线)。
|
||||
|
||||
## [0.0.1] - 2026-10-01
|
||||
|
||||
### Added
|
||||
- 项目初始化,包结构落盘(实现为空)。
|
||||
@@ -1,21 +1,18 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 孤竹居士
|
||||
Copyright (c) 2026 guzhujushi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,72 +1,3 @@
|
||||
# MCM/ICM Toolkit
|
||||
# MCM-ICM_Toolkit
|
||||
|
||||
> 这是我参加美赛时用的学习和准备复用的代码,有需要可以自行克隆和修改。
|
||||
|
||||
美赛(MCM/ICM)编程手的**竞赛层工具箱**:
|
||||
把比赛中反复手写的代码沉淀成"高频算法自己实现 + 成熟库薄封装 + 统一结果接口"。
|
||||
|
||||
> **不**替代 SciPy / scikit-learn / statsmodels / NetworkX / SALib / SymPy。
|
||||
> 只做它们到"4 天赛程能用"之间的那一层。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```powershell
|
||||
# 1) 建环境(两条路选一条)
|
||||
.\scripts\setup-venv.ps1 # venv,零下载,复用 Anaconda base 的科学栈
|
||||
.\scripts\setup-conda.ps1 # conda 环境 mcm-toolkit(精准包列表,走代理下载)
|
||||
|
||||
# 2) 自检
|
||||
.\scripts\doctor.ps1
|
||||
|
||||
# 3) 激活
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
```python
|
||||
import mcm_toolkit as mcm
|
||||
|
||||
mcm.set_seed(42)
|
||||
r = mcm.bootstrap_ci(data, statistic=np.median, random_state=42)
|
||||
print(r.params["low"], r.params["high"])
|
||||
```
|
||||
|
||||
## 当前状态(v0.0.1)
|
||||
|
||||
本地 git 仓库已初始化(`main`,首次提交 `5319589`),远程 Gitea 待配置。
|
||||
|
||||
**接口已定,实现未填。** 所有函数体是 `raise NotImplementedError` + `TODO(L0/L1/v0.2)` 标记,
|
||||
按 [docs/03-roadmap.md](docs/03-roadmap.md) 的 L0 → L1 顺序填。
|
||||
|
||||
## 目录
|
||||
|
||||
| 路径 | 说明 |
|
||||
|---|---|
|
||||
| `src/mcm_toolkit/` | 包源码,7 个子包 |
|
||||
| `tests/` | 测试,与包一一对应(测已知解析解,不测实现细节) |
|
||||
| `examples/` | 每个模块一个能跑的最小例子 |
|
||||
| `problems/` | 历年真题(每做完一题,反向沉淀 1~3 个通用函数) |
|
||||
| `notebooks/` | 探索用 |
|
||||
| `docs/` | 需求、架构、路线图、环境、规范 |
|
||||
| `scripts/` | 环境脚本 |
|
||||
| `STATUS.md` | 当前进度 |
|
||||
|
||||
## 子包
|
||||
|
||||
| 包 | 职责 | 关键函数 |
|
||||
|---|---|---|
|
||||
| `core` | 基础设施 | `Result` `get_rng` `check_*` |
|
||||
| `data` | 读 / 洗 / 变换 / 采样 | `load_table` `detect_outliers` `normalize` |
|
||||
| `stats` | 描述 / 推断 / bootstrap | `describe` `bootstrap_ci` `fit_distribution` |
|
||||
| `models` | 回归 / 时序 / ML | `fit_linear` `fit_arima` `compare_models` |
|
||||
| `solve` | 优化 / 模拟 / 敏感性 | `monte_carlo` `simulated_annealing` `sobol_analyze` |
|
||||
| `decision` | 评价决策 | `entropy_weights` `ahp_weights` `topsis` `gm11` |
|
||||
| `viz` | 出图出表 | `plot` `save_figure` `save_table` |
|
||||
|
||||
## 文档
|
||||
|
||||
开始动手前请先读 [docs/01-suggestion-review.md](docs/01-suggestion-review.md)——
|
||||
它记录了"为什么最终结构是这样",以及**明确不做什么**。
|
||||
|
||||
## 许可
|
||||
|
||||
MIT,见 [LICENSE](LICENSE)。
|
||||
这是我参加美赛时用的学习和准备复用的代码,有需要可以自行克隆和修改
|
||||
@@ -1,118 +0,0 @@
|
||||
# STATUS — 当前进度
|
||||
|
||||
> 更新日期:2026-10-01
|
||||
|
||||
## 本次任务
|
||||
|
||||
审查并精简 ChatGPT 的建库建议 → 生成骨架文件 → 配置 conda 与 venv 环境 → 初始化 git 仓库。
|
||||
|
||||
**结论:全部完成。** 两套环境均已建成并通过验证;本地仓库已初始化并完成首次提交。
|
||||
|
||||
## 任务清单
|
||||
|
||||
### ✅ 已完成
|
||||
|
||||
**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)
|
||||
|
||||
### 验证结果
|
||||
|
||||
| 检查 | venv | conda `mcm-toolkit` |
|
||||
|---|---|---|
|
||||
| `pytest` | 20 passed, 49 skipped | 20 passed, 49 skipped |
|
||||
| `import mcm_toolkit` | 0.0.1 / 16 项 | 0.0.1 / 16 项 |
|
||||
| `ruff check` | All checks passed | — |
|
||||
| `scripts/doctor.ps1` | 跑通 | — |
|
||||
| SALib / pymoo / pyright | 缺失 | 已装 |
|
||||
|
||||
- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致)
|
||||
- skip 的都是等实现的算法行为测试;通过的 20 条是 `tests/test_api_surface.py` 的公开 API 契约测试
|
||||
- 三个 `.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。
|
||||
- [ ] **Gitea 远程尚未配置**:本地仓库已就绪(`main` / `5319589`),
|
||||
只差你自己的 Gitea 地址。命令见 `docs/05-git-workflow.md` §3。
|
||||
- [ ] 所有函数实现尚未开始(按 L0 → L1 顺序,见 `docs/03-roadmap.md`)
|
||||
- [ ] 本机 `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 |
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 在 Gitea 建**空**仓库(不要初始化)→ `git remote add origin ...` → `git push -u origin main`
|
||||
(命令见 `docs/05-git-workflow.md` §3,HTTPS 与 SSH 两版都有)。
|
||||
2. 按 `docs/03-roadmap.md` 的 **L0(12 个函数)** 顺序填实现,先写测试再写代码。
|
||||
3. 每写完一个函数,在 `examples/` 里加一行最小用法,并更新本文件。
|
||||
4. L0 跑通后,找一道真题(建议 2025 C)走完整流程,反向沉淀函数。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,207 +0,0 @@
|
||||
# ChatGPT 建库建议 · 审查与精简
|
||||
|
||||
> 被审查对象:仓库根目录 `chatGPT_Suggestion.md`(33 节,约 2.4 万字)
|
||||
> 审查日期:2026-10-01
|
||||
|
||||
---
|
||||
|
||||
## 一、总裁决
|
||||
|
||||
**方向对,量级错。**
|
||||
|
||||
**方向对的地方(保留)**:把比赛的即用代码沉淀成"竞赛层薄封装 + 高频算法自己实现 + 统一结果接口",而不是攒一堆题解。这个定位是全文最有价值的部分,也是这个仓库存在的理由。
|
||||
|
||||
**量级错的地方(砍)**:原文给出 14 个子包、约 250 个函数、每个函数强制 10 项文档。这是一个人 **6~12 个月**的个人项目体量,不是支撑 **4 天**比赛的体量。按原文执行,最可能的结局是:前两周写完 `core` 和 `data`,然后在 `optimization/` 的 6 个文件面前失去动力,仓库停在半成品——这比"只有一个 20 个函数的 `mcm_toolkit.py`"更糟,因为半成品既不能用于比赛,也不好看。
|
||||
|
||||
**精简后的目标**:
|
||||
|
||||
| 维度 | ChatGPT 版 | 精简版 |
|
||||
|---|---|---|
|
||||
| 子包 | 14 | **7** |
|
||||
| 函数(v0.1) | ~250 | **~40**(其中第一周只做 12 个) |
|
||||
| Result 类 | 5 | **1** |
|
||||
| 单函数文档要求 | 10 项,全体强制 | **2 档**,只有 ~15 个"招牌算法"做全卡片 |
|
||||
| 预计完成周期 | —— | **4~6 周**出可用 v0.1 |
|
||||
|
||||
---
|
||||
|
||||
## 二、必须保留的(原文的干货)
|
||||
|
||||
| # | 原文主张 | 为什么留 |
|
||||
|---|---|---|
|
||||
| 1 | 「不要重新造轮子。`mcm_toolkit` 做竞赛层封装,而不是替代 SciPy/sklearn/NetworkX」 | 全文最重要的一句。它同时是设计原则和**防膨胀闸门** |
|
||||
| 2 | 统一结果对象(`result.params / metrics / success`) | 真实痛点。比赛时最烦的就是每个函数返回不一样的东西 |
|
||||
| 3 | 依赖分层:核心 4 个包 + `[project.optional-dependencies]` | 符合轻量化,且让 CI 和赛场安装都能可控 |
|
||||
| 4 | 顶层 `__init__` 不 `import *`,只导出子包 + 少量高频快捷函数 | 保留 |
|
||||
| 5 | 「`__all__` 是**公开 API 清单**,不是导出按钮」 | 保留,这条纪律要从第一版养成 |
|
||||
| 6 | `problems/` 沉淀历年真题、`notebooks/` 做探索 | "每做一题反向沉淀一个通用函数"是这套东西能长大的唯一机制 |
|
||||
| 7 | `save_figure(fig, path, dpi=300, formats=("png","pdf"))` | 美赛送稿硬需求,原文提了但严重低估——见第四节 |
|
||||
| 8 | 参考库定位:SALib / NetworkX / statsmodels / pymoo / SymPy / sklearn | 六条定位全部准确,可直接当"别自己写"的白名单 |
|
||||
| 9 | A / B / C 三级(自己写 / 自己封装 / 直接调用)的分类框架 | 框架留,**边界要下调**(见第三节第 10 条) |
|
||||
|
||||
---
|
||||
|
||||
## 三、要砍的
|
||||
|
||||
### 3.1 结构层:14 包 → 7 包
|
||||
|
||||
| 原文的包 | 处置 | 理由 |
|
||||
|---|---|---|
|
||||
| `core/` | **留**(4 → 3 文件) | 基础设施,必须有 |
|
||||
| `data/` | **留**(4 文件) | 最高频 |
|
||||
| `statistics/` | **留**,改名 `stats/`(5 → 3 文件) | S 级模块 |
|
||||
| `regression/` + `ml/` | **合并为 `models/`** | 原文这两块 `fit_linear_regression` 与 `fit_linear` 明显重复,一套 `fit/predict/score` 协议就够了 |
|
||||
| `optimization/` + `simulation/` + `sensitivity/` | **合并为 `solve/`** | 这三者是同一件事的三种形态:"把模型反复算"。分开建三个包,共享的 `Result`、`rng`、并行约定要在三处重复 |
|
||||
| `dynamics/` | **降级**为 `solve/dynamics.py`,进 v0.2 | 原文自己承认"不一定主攻 MCM A"。MCM A 一年一题,且 SymPy+SciPy 封装只需 4 个函数,不值得一个独立包 |
|
||||
| `network/` | **降级**为 `solve/graph.py`,进 v0.2 | 只有 ICM D 用;NetworkX 已全部提供。原文列了 5 个子模块 20+ 函数,其中 `degree_centrality` 之类就是一行转发 |
|
||||
| `decision/` | **留但砍半**:8 → 4 文件 | AHP / TOPSIS / 熵权 / 灰色是中文赛高频,值得留;`dea`、`fuzzy` 见下 |
|
||||
| `visualization/` | **改 `viz/`**,13 个 `plot_*` → 1 个 `plot(kind=...)` + 3 个专用图 | 见 3.3 |
|
||||
| `utils/metrics.py` | **取消**,指标并入 `models/metrics.py` | 原文第 26 节又建议单列 `analysis/` 放同样的函数,三处重复 |
|
||||
| 第 26 节的 `analysis/` | **取消** | 与 `utils/metrics.py` 功能完全重叠 |
|
||||
| `docs/api/` `docs/tutorials/` | 保留占位,**不写内容** | 自动 API 文档用 mkdocs/sphinx 生成,手写必然过期 |
|
||||
|
||||
### 3.2 设计层
|
||||
|
||||
| 砍什么 | 理由 | 替代 |
|
||||
|---|---|---|
|
||||
| 5 个 Result 类 | 第一版没有任何一个函数需要区分这五种;过早分类会导致大量样板代码 | **1 个 `Result`**:`name / params / metrics / data / success / message`,后续真需要时再派生 |
|
||||
| 每个函数 10 项文档(数学定义/适用场景/参数/实现/示例/测试/复杂度/数值稳定性/常见错误/MCM 案例) | 对一个一行转发的封装函数写 10 项文档,写作成本是实现的 20 倍。**这是最可能导致项目停滞的一条** | 两档制,见第七节 |
|
||||
| 「`__init__.py` 里再导出少量高频函数」的具体选择 | 原文选的是 `solve_ode`——但 ODE 已被降级 | 改成 `monte_carlo / bootstrap_ci / minimize / parameter_sweep / topsis / compare_models` |
|
||||
|
||||
### 3.3 函数层:要删的清单
|
||||
|
||||
> 判断标准只有两条:**美赛近 10 年题面出现过吗?** / **成熟库是不是一行就够?** 两条都不满足就删。
|
||||
|
||||
| 删除项 | 理由 | 替代 |
|
||||
|---|---|---|
|
||||
| `maximize()` | 就是 `minimize` 取负 | `minimize` 加 `sense="max"` 参数 |
|
||||
| `kendall_correlation` / `partial_correlation` | 美赛几乎不用;`df.corr(method="kendall")` 一行 | pandas |
|
||||
| `modified_zscore_outliers` / `zscore_outliers` | IQR 和 z-score 两种覆盖 95% 场景 | 只留 `detect_outliers(method="iqr"|"zscore")` |
|
||||
| `distribution_pdf/cdf/ppf` 三个独立函数 | 纯转发 scipy | `fit_distribution` 返回的对象上挂 `.pdf/.cdf/.ppf` |
|
||||
| `sample_without_replacement` / `sample_with_replacement` | `rng.choice(..., replace=)` 一行 | numpy |
|
||||
| `brownian_motion` / `geometric_brownian_motion` | niche,只有金融题偶尔用 | 用到再写;`random_walk` 已覆盖 |
|
||||
| `mmc_metrics` / `simulate_queue` | 排队论在美赛出现频率极低,且 `simulate_queue` 是个完整的离散事件仿真引擎,成本极高 | 只留 `mm1_metrics`(闭式解,10 行) |
|
||||
| `crowding_distance` / `hypervolume` | NSGA-II 内部件,属于原文自己划的 C 类 | pymoo |
|
||||
| `dea_efficiency` | 低频;且要解一堆 LP,实现成本不低 | 真要用,现场用 `scipy.optimize.linprog` 拼 |
|
||||
| `fuzzy_comprehensive_evaluation` | 低频,且"隶属度函数"没有通用形式,做不了通用封装 | 删 |
|
||||
| `value_iteration` + `policy_iteration` + `dynamic_programming` 三个 | 三者是同一族的三种写法,第一版同时做属于过度设计 | 只留 `dynamic_programming`(通用)+ `knapsack_dp`(模板) |
|
||||
| `weighted_moving_average` / `holt_trend` / `seasonal_decompose` | statsmodels 一行。原文自己也在第 7 节说"做薄封装" | 只留 `exponential_smoothing` 一个包装 |
|
||||
| `grid_sweep` | `parameter_sweep` 的特例 | 合入 `parameter_sweep` |
|
||||
| 13 个 `plot_*` | 每个都是 `ax` 上加两行;拆成 13 个函数后,改一次全局样式要改 13 处 | `plot(x, y, kind="line"|"scatter"|"hist"|"box"|"heatmap")` + `plot_correlation_matrix` / `plot_residuals` / `plot_convergence` 三个有真实逻辑的 |
|
||||
| `normalize` 与 `minmax_scale` 并存 | 是同一个东西的两个名字 | 只留 `normalize` |
|
||||
| `standardize` 与 `zscore` 并存(一个在 data、一个在 stats) | 同上 | 只留 `data.standardize` |
|
||||
| `higher-level`:`sample_distribution`、`distribution_pdf` 系列 | 转发 | 删 |
|
||||
|
||||
**净效果:约 250 → 约 40 个函数。**
|
||||
|
||||
### 3.4 下调 A/B/C 的边界
|
||||
|
||||
原文的 A 类("强烈建议自己实现")里有 `dynamic programming`、`genetic algorithm`、`sensitivity analysis` 等。**用于学习时**这个清单没问题;但**比赛现场**要反过来:
|
||||
|
||||
- 比赛时:**能用库就用库**(SALib 的 `sobol_analyze` 一定比自己写的对)。
|
||||
- 平时:自己实现,目的是"读得懂、改得动、能解释给队友"。
|
||||
- 所以自己实现的代码要单独标 `_from_scratch` 或放在 `mcm_toolkit/algo/`,默认走库版本。
|
||||
|
||||
原文把这两个场景混在一起了。
|
||||
|
||||
---
|
||||
|
||||
## 四、ChatGPT 漏掉的(真正决定比赛成败的)
|
||||
|
||||
原文 33 节几乎全是"代码结构",但**美赛是论文比赛,不是代码比赛**。以下六条原文完全没提或严重低估:
|
||||
|
||||
1. **论文产出链路(最严重的缺失)**
|
||||
工具箱的最终消费者是**论文**。原文只给了一个 `save_figure`。至少要补:
|
||||
- `save_table(df, path, fmt=("csv","md","latex"))` —— 结果表直接出 LaTeX,粘贴进论文
|
||||
- 全局图样式 `mcm_style()`:字号 ≥ 9pt、线宽 ≥ 1.5、`dpi=300`、色盲友好调色板、双栏宽度
|
||||
- 图注(`fig.caption`)与"图号—正文引用"的约定
|
||||
|
||||
2. **量纲与方向**
|
||||
`normalize` 没区分"无量纲化"和"标准化",更没提**效益型 / 成本型指标方向**。评价类题目(TOPSIS/熵权)翻车基本都翻在这里。
|
||||
|
||||
3. **时间预算**
|
||||
4 天比赛,函数的价值 = 正确性 ÷ 上手时间。原文没有一条关于"够快"的约定:MC 必须向量化、`n_resamples` 默认值要能在一分钟内跑完、不要引入编译开销。
|
||||
|
||||
4. **可复现性**
|
||||
提了 `get_rng`,但没提"提交要能复现"。约定:所有带随机的公开函数第一个参数是 `random_state`,论文附录必须写 seed。
|
||||
|
||||
5. **测试测什么**
|
||||
说了 pytest,没说测什么。应测 `已知解析解`:MC 积分 vs 解析积分、正态 CI vs 公式值、`bootstrap_ci` 大样本下收敛到 CLT 值。**不要测实现细节**。
|
||||
|
||||
6. **离线约束**
|
||||
原文的 extras 设计假设比赛机器能联网。现场装不上包是真实风险,必须有 `environment.yml` 锁版本 + 一份"最小可用依赖"清单。
|
||||
|
||||
另外两条工程问题:
|
||||
|
||||
7. **原文第 32 节把 `problems/` `notebooks/` 和 `src/` 混在一个仓库**——这是对的,但要注意 `problems/` 会带来大量数据文件,`.gitignore` 要提前写好(见仓库根目录的 `.gitignore`)。
|
||||
8. **没有"不做"清单**。原文用"建议不要",但真正能防膨胀的是写死的"本仓库**永远不**实现 X"。已补进 `docs/03-roadmap.md`。
|
||||
|
||||
---
|
||||
|
||||
## 五、精简后的结构
|
||||
|
||||
```text
|
||||
MCM&ICM_Toolkit/
|
||||
├── src/mcm_toolkit/
|
||||
│ ├── core/ 基础设施:Result / rng / 校验 ← 第一个做
|
||||
│ ├── data/ 读、洗、变换、采样 ← 最高频
|
||||
│ ├── stats/ 描述、推断、bootstrap ← S 级
|
||||
│ ├── models/ 回归 + 时序 + ML 薄封装(统一 fit/predict/score)
|
||||
│ ├── solve/ 优化、模拟、敏感性、图、动力学 ← 竞赛主力
|
||||
│ ├── decision/ 熵权 / AHP / TOPSIS / 灰色
|
||||
│ └── viz/ 统一绘图 + 图表导出
|
||||
├── tests/ tests/test_*.py(按包一一对应)
|
||||
├── examples/ 每个模块一个能跑的最小例子
|
||||
├── problems/ 历年真题(2025_C/ ...)
|
||||
├── notebooks/ 探索用
|
||||
├── docs/ 本目录
|
||||
├── scripts/ setup-conda.ps1 / setup-venv.ps1
|
||||
├── pyproject.toml
|
||||
├── environment.yml
|
||||
└── STATUS.md 进度
|
||||
```
|
||||
|
||||
包结构详情与每个包的边界见 `02-architecture.md`。
|
||||
|
||||
---
|
||||
|
||||
## 六、功能清单分级
|
||||
|
||||
见 `03-roadmap.md`。摘要:
|
||||
|
||||
- **L0(第一周,12 个)**:`Result` `get_rng` `load_table` `clean_missing` `detect_outliers` `normalize` `standardize` `describe` `correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure`
|
||||
- **L1(v0.1,共约 40 个)**:L0 + 回归/时序/优化/敏感性/评价/绘图
|
||||
- **L2(v0.2)**:`dynamics`、`graph`
|
||||
- **永久不做**:见 `03-roadmap.md` 末节
|
||||
|
||||
---
|
||||
|
||||
## 七、开发纪律:文档从 10 项降到 2 档
|
||||
|
||||
原文要求**每个**函数都写 10 项。改成两档:
|
||||
|
||||
**S 档(招牌算法,约 15 个)** —— 用 `docs/dev/function-card-template.md` 的完整卡片:
|
||||
数学定义 / 适用场景 / 参数 / 复杂度 / 数值稳定性 / 常见错误 / 最小示例 / 单测 / 赛题用法。
|
||||
覆盖:`bootstrap_ci` `monte_carlo` `simulated_annealing` `dynamic_programming` `parameter_sweep` `sobol_analyze` `topsis` `entropy_weights` `ahp_weights` `gm11` `detect_outliers` `local_sensitivity` 等。
|
||||
|
||||
**A 档(其余,薄封装)** —— 只要求:
|
||||
1. docstring 里**一句话数学含义 + 一句话什么时候用**;
|
||||
2. 一条 `pytest`(对齐库结果或已知值)。
|
||||
|
||||
不单独写文档。
|
||||
|
||||
> 判断标准:**我会不会去改它的内部实现?** 会 → S 档;只当转发器用 → A 档。
|
||||
|
||||
---
|
||||
|
||||
## 八、执行顺序(时间盒)
|
||||
|
||||
| 周 | 目标 | 交付 |
|
||||
|---|---|---|
|
||||
| 1 | `core` + `data` + `stats` 骨架;S 档先写 `bootstrap_ci` `monte_carlo` `normalize` `detect_outliers` | L0 全部可跑通 |
|
||||
| 2–3 | `models` + `solve/optimize` + `solve/simulate` | `compare_models` 能跑 |
|
||||
| 4 | `solve/sensitivity` + `decision` + `viz` | 一套端到端 example |
|
||||
| 5–6 | 拿一道真题(如 2025 C)从头走一遍,反向补函数 | `problems/2025_C/` 完成 |
|
||||
| 之后 | 每做一题 → 沉淀 1~3 个函数 + 对应 test | 仓库自然长大 |
|
||||
|
||||
**这个顺序的关键是"先能跑通一条完整链路,再补宽度"**,与原文"先把 14 个包建齐"相反。
|
||||
@@ -1,178 +0,0 @@
|
||||
# 架构设计
|
||||
|
||||
> 上游依据:`01-suggestion-review.md`。本文件定义"代码往哪写、边界在哪、接口长什么样"。
|
||||
|
||||
---
|
||||
|
||||
## 一、设计原则
|
||||
|
||||
1. **薄封装优先。** 能用 SciPy / sklearn / statsmodels / NetworkX / SALib 一行解决的,就只做一行转发 + 参数校验 + `Result` 包装。本仓库**不**替代这些库。
|
||||
2. **自己实现是有目的的。** 只有"库里没有、或者自己写才学得会"的算法才手写(bootstrap、Monte Carlo、DP、SA、熵权、TOPSIS、灰色……)。手写实现要能被读懂、能被改。
|
||||
3. **统一出口。** 凡是"拟合 / 求解 / 模拟"类函数,一律返回 `Result`,字段固定。
|
||||
4. **可复现。** 所有含随机的公开函数都接受 `random_state`,默认 `None`(用全局 RNG)。
|
||||
5. **默认值面向比赛。** 默认参数应当在 4 天赛程里"直接能跑完",不用调。
|
||||
|
||||
---
|
||||
|
||||
## 二、包结构(7 个包)
|
||||
|
||||
```text
|
||||
src/mcm_toolkit/
|
||||
├── __init__.py 只导出 6 个高频快捷函数 + 7 个子包
|
||||
├── _version.py
|
||||
│
|
||||
├── core/ ① 基础设施 —— 所有包的地基
|
||||
│ ├── result.py Result
|
||||
│ ├── random.py set_seed / get_rng
|
||||
│ └── validate.py check_array / check_same_length / check_probability / check_square
|
||||
│
|
||||
├── data/ ② 数据进出 —— 最高频
|
||||
│ ├── io.py load_table / save_table
|
||||
│ ├── clean.py clean_missing / impute_missing / detect_outliers / winsorize
|
||||
│ ├── transform.py normalize / standardize / log_transform / one_hot_encode
|
||||
│ └── sample.py train_test_split / kfold_split / bootstrap_sample
|
||||
│
|
||||
├── stats/ ③ 统计 —— S 级模块
|
||||
│ ├── descriptive.py describe / zscore / coefficient_of_variation
|
||||
│ ├── inference.py confidence_interval / t_test / anova / chi_square_test
|
||||
│ │ fit_distribution / goodness_of_fit
|
||||
│ └── bootstrap.py bootstrap_statistic / bootstrap_ci ★ 自己实现
|
||||
│
|
||||
├── models/ ④ 建模 —— 回归 / 时序 / ML,统一协议
|
||||
│ ├── base.py Model 协议:fit / predict / score
|
||||
│ ├── metrics.py rmse / mae / mape / r2_score / adjusted_r2 / compare_models
|
||||
│ ├── regression.py fit_linear / fit_polynomial / fit_nonlinear / predict
|
||||
│ ├── timeseries.py exponential_smoothing / fit_arima / forecast
|
||||
│ └── ml.py fit_forest / fit_logistic / kmeans / pca / cross_validate
|
||||
│
|
||||
├── solve/ ⑤ 求解 —— 竞赛主力(把模型反复算)
|
||||
│ ├── optimize.py minimize / linear_program / integer_program
|
||||
│ │ dynamic_programming / knapsack_dp ★ 自己实现
|
||||
│ │ simulated_annealing ★ 自己实现
|
||||
│ │ pareto_front
|
||||
│ ├── simulate.py monte_carlo / monte_carlo_probability / monte_carlo_integral
|
||||
│ │ random_walk / markov_simulate / markov_steady_state
|
||||
│ │ mm1_metrics
|
||||
│ ├── sensitivity.py local_sensitivity / one_at_a_time / parameter_sweep
|
||||
│ │ robustness_test / sobol_analyze / morris_analyze
|
||||
│ ├── graph.py 【v0.2】build_graph / shortest_path / centrality / pagerank / max_flow
|
||||
│ └── dynamics.py 【v0.2】solve_ode / solve_system / find_equilibrium / local_stability
|
||||
│
|
||||
├── decision/ ⑥ 评价决策 —— 中文赛特色
|
||||
│ ├── weighting.py entropy_weights / critic_weights / equal_weights
|
||||
│ ├── ahp.py ahp_weights / ahp_consistency_ratio
|
||||
│ ├── topsis.py topsis
|
||||
│ └── grey.py grey_relational_analysis / gm11_fit_predict
|
||||
│
|
||||
└── viz/ ⑦ 出图出表 —— 论文产出的最后一公里
|
||||
├── style.py mcm_style
|
||||
├── plot.py plot / plot_correlation_matrix / plot_residuals / plot_convergence
|
||||
└── export.py save_figure / save_table
|
||||
```
|
||||
|
||||
★ = 自己实现(S 档,配完整函数卡片)
|
||||
|
||||
---
|
||||
|
||||
## 三、与 ChatGPT 原方案的对照
|
||||
|
||||
| 原方案 | 现方案 | 变更理由 |
|
||||
|---|---|---|
|
||||
| 14 个包 | 7 个包 | 见 `01-suggestion-review.md` §3.1 |
|
||||
| `regression/` + `ml/` | 合并 `models/` | 原方案里 `fit_linear` 与 `fit_linear_regression` 重复 |
|
||||
| `optimization/` + `simulation/` + `sensitivity/` | 合并 `solve/` | 三者共享 Result / rng / 并行约定,分开会重复三遍 |
|
||||
| `dynamics/` `network/` | 降级为 `solve/dynamics.py` `solve/graph.py`,进 v0.2 | 低频,不值得独立包 |
|
||||
| `utils/metrics.py` + 单列 `analysis/` | 合并 `models/metrics.py` | 原方案三处重复放同一批指标 |
|
||||
| 13 个 `plot_*` | `plot(kind=...)` + 3 个专用图 | 改全局样式不必改 13 处 |
|
||||
| 5 个 Result 类 | 1 个 `Result` | 第一版没有一个函数需要区分五种结果 |
|
||||
|
||||
---
|
||||
|
||||
## 四、关键约定
|
||||
|
||||
### 4.1 `Result`(唯一结果对象)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Result:
|
||||
name: str # 产生它的函数名
|
||||
params: dict | None = None # 参数估计 / 决策变量取值
|
||||
metrics: dict | None = None # 评估指标
|
||||
data: Any = None # 主要载荷(预测值 / 轨迹 / 排序……)
|
||||
success: bool = True
|
||||
message: str = ""
|
||||
```
|
||||
|
||||
约定:
|
||||
- 任何"拟合 / 求解 / 模拟 / 评价"函数**只返回 `Result`**,不返回裸 tuple。
|
||||
- `Result` 支持 `repr` 直接打印成"指标表",方便贴进论文草稿。
|
||||
- 后续真需要区分类型时再派生,不提前建 5 个类。
|
||||
|
||||
### 4.2 随机数
|
||||
|
||||
```python
|
||||
set_seed(seed) -> None # 全局种子
|
||||
get_rng(random_state=None) -> np.random.Generator
|
||||
```
|
||||
|
||||
- 所有含随机的函数签名统一含 `random_state=None`,内部第一行 `rng = get_rng(random_state)`。
|
||||
- **禁止**直接用 `np.random.*` 旧接口,禁止裸用 `random` 模块。
|
||||
- 论文附录必须写明 seed。
|
||||
|
||||
### 4.3 模型协议
|
||||
|
||||
```python
|
||||
class Model(Protocol):
|
||||
def fit(self, X, y, **kw) -> "Model": ...
|
||||
def predict(self, X) -> np.ndarray: ...
|
||||
def score(self, X, y) -> dict: ... # 返回指标 dict,不是单个 float
|
||||
```
|
||||
|
||||
- 这样 `compare_models({"linear": m1, "rf": m2}, X, y)` 才能统一执行。
|
||||
- `compare_models` 返回 `pd.DataFrame`(行=模型,列=指标),可直接 `save_table(..., fmt="latex")`。
|
||||
|
||||
### 4.4 指标方向与量纲(评价类必读)
|
||||
|
||||
- `normalize` 只做**无量纲化**(min-max / 向量归一),不改变指标方向。
|
||||
- 方向(效益型 / 成本型)在 `decision` 层处理,用参数 `benefit_columns` 显式声明。
|
||||
- **不允许**默认假设"越大越好"。
|
||||
|
||||
### 4.5 import 规则
|
||||
|
||||
- 顶层 `mcm_toolkit/__init__.py` 只导出子包 + 6 个快捷函数,**不 `import *`**。
|
||||
- 子包之间允许单向依赖:`core ← data / stats / models / solve / decision / viz`;`viz` 可依赖其余全部;**禁止反向依赖**。
|
||||
- `__all__` 是公开 API 清单,不写"内部工具"。
|
||||
|
||||
### 4.6 依赖分层
|
||||
|
||||
| 层 | 包 | 说明 |
|
||||
|---|---|---|
|
||||
| 核心(必装) | `numpy` `scipy` `pandas` `matplotlib` | `core` `data` `stats` `viz` 只需要这些 |
|
||||
| `[ml]` | `scikit-learn` `statsmodels` | `models/` |
|
||||
| `[graph]` | `networkx` | `solve/graph.py` |
|
||||
| `[sensitivity]` | `SALib` | `sobol_analyze` / `morris_analyze` |
|
||||
| `[symbolic]` | `sympy` | `solve/dynamics.py` |
|
||||
| `[moo]` | `pymoo` | 真需要 NSGA-II 时 |
|
||||
| `[excel]` | `openpyxl` | Excel 读写 |
|
||||
| `[dev]` | `pytest` `ruff` `pyright` | 开发 |
|
||||
|
||||
**可选依赖缺失时必须给出可读报错**,例如 `sobol_analyze` 在没装 SALib 时提示 `pip install "mcm-toolkit[sensitivity]"`,而不是 `ModuleNotFoundError` 裸抛。
|
||||
|
||||
---
|
||||
|
||||
## 五、测试策略
|
||||
|
||||
- 目录 `tests/` 与包一一对应(`tests/test_stats.py` ↔ `stats/`)。
|
||||
- **测已知解析解,不测实现细节**:
|
||||
|
||||
| 函数 | 测法 |
|
||||
|---|---|
|
||||
| `monte_carlo_integral` | 对 `∫₀¹ x² dx = 1/3` 收敛,误差 < 1e-2(n=1e5) |
|
||||
| `bootstrap_ci` | 正态样本大 n 下,百分位 CI ≈ 解析 CI(±5%) |
|
||||
| `confidence_interval` | 与 `scipy.stats.norm.interval` 对齐 |
|
||||
| `fit_polynomial` | 与 `np.polyfit` 系数对齐(`allclose`) |
|
||||
| `detect_outliers` | 人工构造带 3 个离群点的数组,命中数 = 3 |
|
||||
| `topsis` | 与手算的排序一致 |
|
||||
|
||||
- 随机相关测试一律固定 `random_state`。
|
||||
- 每条断言都要能说清"错了说明什么"。
|
||||
@@ -1,112 +0,0 @@
|
||||
# 路线图与开发纪律
|
||||
|
||||
> 依据:`01-suggestion-review.md` §6–§8。
|
||||
> 进度看根目录 `STATUS.md`,本文件只定义"做什么、做到什么程度、什么不做"。
|
||||
|
||||
---
|
||||
|
||||
## 一、功能清单分级
|
||||
|
||||
### L0 —— 第一周必做(12 个)
|
||||
|
||||
没有这 12 个,工具箱根本跑不起来;有了这 12 个,已经能应付大部分题目。
|
||||
|
||||
| 函数 | 所在模块 | 档位 |
|
||||
|---|---|---|
|
||||
| `Result` | `core/result.py` | A |
|
||||
| `get_rng` / `set_seed` | `core/random.py` | A |
|
||||
| `load_table` | `data/io.py` | A |
|
||||
| `clean_missing` | `data/clean.py` | A |
|
||||
| `detect_outliers` | `data/clean.py` | **S** |
|
||||
| `normalize` | `data/transform.py` | A |
|
||||
| `standardize` | `data/transform.py` | A |
|
||||
| `describe` | `stats/descriptive.py` | A |
|
||||
| `correlation_matrix` | `stats/inference.py` | A |
|
||||
| `bootstrap_ci` | `stats/bootstrap.py` | **S** |
|
||||
| `monte_carlo` | `solve/simulate.py` | **S** |
|
||||
| `save_figure` | `viz/export.py` | A |
|
||||
|
||||
### L1 —— v0.1(共约 40 个)
|
||||
|
||||
在 L0 基础上补齐:
|
||||
|
||||
| 模块 | 函数 |
|
||||
|---|---|
|
||||
| `core` | `check_array` `check_same_length` `check_probability` `check_square_matrix` |
|
||||
| `data` | `impute_missing` `winsorize` `log_transform` `one_hot_encode` `train_test_split` `kfold_split` `bootstrap_sample` |
|
||||
| `stats` | `zscore` `coefficient_of_variation` `confidence_interval` `t_test` `anova` `chi_square_test` `fit_distribution` `goodness_of_fit` `bootstrap_statistic` |
|
||||
| `models` | `rmse` `mae` `mape` `smape` `r2_score` `adjusted_r2` `compare_models` `fit_linear` `fit_polynomial` `fit_nonlinear` `predict` `exponential_smoothing` `fit_arima` `forecast` `fit_forest` `fit_logistic` `kmeans` `pca` `cross_validate` |
|
||||
| `solve` | `minimize` `linear_program` `integer_program` `dynamic_programming` `knapsack_dp` `simulated_annealing` `pareto_front` `monte_carlo_probability` `monte_carlo_integral` `random_walk` `markov_simulate` `markov_steady_state` `mm1_metrics` `local_sensitivity` `one_at_a_time` `parameter_sweep` `robustness_test` `sobol_analyze` `morris_analyze` |
|
||||
| `decision` | `equal_weights` `entropy_weights` `critic_weights` `ahp_weights` `ahp_consistency_ratio` `topsis` `grey_relational_analysis` `gm11_fit_predict` |
|
||||
| `viz` | `mcm_style` `plot` `plot_correlation_matrix` `plot_residuals` `plot_convergence` `save_table` |
|
||||
|
||||
### L2 —— v0.2
|
||||
|
||||
- `solve/graph.py`:`build_graph` `shortest_path` `min_spanning_tree` `betweenness_centrality` `pagerank` `max_flow` `community_detection`
|
||||
- `solve/dynamics.py`:`solve_ode` `solve_system` `find_equilibrium` `local_stability`
|
||||
|
||||
**触发条件**:在真题里真的用到了,或者要为 MCM A / ICM D 备赛时再写。
|
||||
|
||||
---
|
||||
|
||||
## 二、永久不做(防膨胀清单)
|
||||
|
||||
> 原文用"建议不要",这里写成**硬规则**。往这个清单里加东西,必须先改这份文档。
|
||||
|
||||
- 不重写 SciPy 的优化器、`scipy.stats` 的分布、`scipy.integrate` 的积分器
|
||||
- 不重写 sklearn 的 estimator(`fit/predict` 之外一律不做)
|
||||
- 不重写 statsmodels 的时间序列框架
|
||||
- 不重写 NetworkX 的图算法
|
||||
- 不重写 SALib 的 Sobol / Morris / FAST
|
||||
- 不实现 NSGA-II / SPEA2 等完整多目标进化算法(用 pymoo)
|
||||
- 不实现符号计算引擎(用 SymPy)
|
||||
- 不实现完整离散事件仿真引擎(`simulate_queue` 那种)
|
||||
- 不做深度学习 / PyTorch 相关任何东西
|
||||
- 不做 GUI、不做 Web 界面、不做 CLI 之外的产品化
|
||||
- 不做自动微分、不做 GPU 后端
|
||||
- 不为"完整性"补函数——**没在真题里用过三次的函数不写**
|
||||
|
||||
---
|
||||
|
||||
## 三、开发纪律
|
||||
|
||||
### 3.1 文档两档制
|
||||
|
||||
**S 档**(招牌算法,自实现):完整函数卡片,模板见 `dev/function-card-template.md`。
|
||||
覆盖:`detect_outliers` `bootstrap_ci` `monte_carlo` `monte_carlo_probability`
|
||||
`monte_carlo_integral` `dynamic_programming` `knapsack_dp` `simulated_annealing`
|
||||
`local_sensitivity` `sobol_analyze` `entropy_weights` `topsis` `ahp_weights`
|
||||
`gm11_fit_predict`。约 15 个。
|
||||
|
||||
**A 档**(薄封装):docstring 里一句话数学含义 + 一句话什么时候用 + 一条 pytest。
|
||||
|
||||
判断标准:**我会不会去改它的内部实现?** 会 → S 档。
|
||||
|
||||
### 3.2 每写一个函数
|
||||
|
||||
1. 先想"这道题里它替代了哪段手写代码"——想不出来就先不写。
|
||||
2. 先在 `tests/` 里写一条会失败的断言(测已知解析解,不测实现细节)。
|
||||
3. 实现到测试通过。
|
||||
4. 在 `examples/` 里加一行最小用法。
|
||||
5. 更新 `STATUS.md`。
|
||||
|
||||
### 3.3 提交前
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\python.exe -m pytest -q
|
||||
.venv\Scripts\python.exe -m ruff check src tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、时间盒
|
||||
|
||||
| 周 | 目标 | 完成标志 |
|
||||
|---|---|---|
|
||||
| 1 | `core` + `data` + `stats`;S 档先写 `bootstrap_ci` `monte_carlo` `detect_outliers` | L0 全部通过测试 |
|
||||
| 2–3 | `models` + `solve/optimize` + `solve/simulate` | `compare_models` 出对比表 |
|
||||
| 4 | `solve/sensitivity` + `decision` + `viz` | 一条端到端 example |
|
||||
| 5–6 | 用一道真题走完整流程,反向补函数 | `problems/<年份>_<题号>/` 完成 |
|
||||
| 之后 | 每做一题沉淀 1~3 个函数 + 对应 test | 仓库自然长大 |
|
||||
|
||||
**核心原则:先跑通一条完整链路,再补宽度。**
|
||||
@@ -1,238 +0,0 @@
|
||||
# 环境配置
|
||||
|
||||
> 记录本机(Windows / PowerShell)上 conda 与 venv 两套环境的**实际**配置方式、
|
||||
> 实测数据、以及踩过的坑。相关脚本在 `scripts/`。
|
||||
>
|
||||
> 状态:**两套环境均已建好、版本已对齐、验证通过**(2026-10-01)。
|
||||
|
||||
---
|
||||
|
||||
## 一、结论速查
|
||||
|
||||
| | venv | conda env |
|
||||
|---|---|---|
|
||||
| 路径 | `D:\MCM&ICM_Toolkit\.venv` | `D:\anaconda\envs\mcm-toolkit` |
|
||||
| 解释器 | Anaconda base,Python **3.13.9** | Python **3.13.15** |
|
||||
| 占用 | 10.5 MB | 1.55 GB(+ 共用 pkgs 缓存) |
|
||||
| 来源 | `--system-site-packages` 复用 base | conda 从 defaults 通道安装 |
|
||||
| 用途 | 轻量开发 / 离线兜底 | **跑题主力环境** |
|
||||
| 缺什么 | SALib、pymoo、pyright | 无缺口 |
|
||||
|
||||
**两套的数值计算栈已完全一致**(见 §5),只差 Python 补丁号。可以放心在两套之间切换。
|
||||
|
||||
---
|
||||
|
||||
## 二、venv(轻量、离线可用)
|
||||
|
||||
```powershell
|
||||
D:\anaconda\python.exe -m venv --system-site-packages --prompt "mcm-toolkit" .venv
|
||||
.venv\Scripts\python.exe -m pip install -e . --no-deps --no-build-isolation
|
||||
```
|
||||
|
||||
一键:`./scripts/setup-venv.ps1`
|
||||
|
||||
### 为什么用 `--system-site-packages`
|
||||
|
||||
让 venv 直接复用 Anaconda base 里已有的包,**零下载**就有可用的科学栈,
|
||||
而且只有 10.5 MB,可以安心放在项目目录里。
|
||||
|
||||
已验证可导入(版本随 base 走,2026-10-01 base 升级后为):
|
||||
|
||||
`text
|
||||
numpy 2.4.6 scipy 1.18.1 pandas 3.0.6 matplotlib 3.11.2
|
||||
scikit-learn 1.9.1 statsmodels 0.15.0 networkx 3.6.1 sympy 1.14.0
|
||||
`
|
||||
|
||||
### `--no-deps --no-build-isolation` 是必须的
|
||||
|
||||
* `--no-deps`:依赖已由 base 提供,不必联网解析;
|
||||
* `--no-build-isolation`:否则 pip 会新建隔离环境去下载 setuptools 构建后端。
|
||||
|
||||
---
|
||||
|
||||
## 三、conda 环境 `mcm-toolkit`(跑题主力)
|
||||
|
||||
```powershell
|
||||
# 一键(推荐)
|
||||
.\scripts\setup-conda.ps1
|
||||
|
||||
# 等价的手工命令
|
||||
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes' # 见 §4 坑 1
|
||||
conda create -n mcm-toolkit -y python=3.13 ^
|
||||
numpy scipy pandas matplotlib scikit-learn statsmodels ^
|
||||
networkx sympy openpyxl pytest ruff pip
|
||||
```
|
||||
|
||||
**实测:约 14.5 分钟(868 秒),下载约 3 GB,环境本体 1.55 GB。**
|
||||
|
||||
建完后补装 pip-only 的依赖并安装本项目:
|
||||
|
||||
```powershell
|
||||
$py = 'D:\anaconda\envs\mcm-toolkit\python.exe'
|
||||
& $py -m pip install SALib pymoo pyright
|
||||
& $py -m pip install -e . --no-deps --no-build-isolation
|
||||
```
|
||||
|
||||
这些 `setup-conda.ps1` 已全部自动完成。
|
||||
|
||||
### 为什么不推荐 `--clone base`
|
||||
|
||||
原以为 `conda create -n X --clone base` 是"本地复制、秒级完成、不用下载"。
|
||||
**实测不是。** 在 conda 25.11 上它会重放 base 的完整包列表并逐个下载安装
|
||||
(连 pandoc、m2-msys2-runtime、anaconda-navigator 这些都用不上的也照下),
|
||||
跑 253 秒才下完约 2 GB 就已被中止,按比例算远慢于精准列表。
|
||||
|
||||
所以 `setup-conda.ps1` 的默认路径是**精准包列表**;`-CloneBase` 只在你确实想
|
||||
要一份 base 的完整副本时才用。
|
||||
|
||||
### 断网怎么办
|
||||
|
||||
**提前建好环境**,并保留 `D:\anaconda\pkgs` 缓存(现在 8.4 GB)。
|
||||
包已在缓存里时 `conda create --offline` 才能工作;否则会失败。
|
||||
比赛现场不要指望临时建环境,走 venv(它 100% 离线可用)。
|
||||
|
||||
---
|
||||
|
||||
## 四、踩过的坑
|
||||
|
||||
**坑 1 —— Anaconda ToS 插件要联网。**
|
||||
Anaconda 25.x 自带 `conda_anaconda_tos` 插件,任何 `create/install` 之前都会去
|
||||
`repo.anaconda.com` 拉 ToS 元数据。断网时它不是报"没网",而是抛一长串 traceback。
|
||||
|
||||
```powershell
|
||||
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes' # 推荐:跳过联网,保留 libmamba 求解器
|
||||
conda --no-plugins create ... # 彻底关插件(退化成经典求解器,慢)
|
||||
```
|
||||
|
||||
**坑 2 —— `envs_dirs` / `pkgs_dirs` 必须可写。**
|
||||
不可写时报 `NotWritableError` 或
|
||||
`Error encountered while attempting to create cache directory`。排查:
|
||||
|
||||
```powershell
|
||||
conda info --json | Select-String "envs_dirs","pkgs_dirs"
|
||||
conda config --show envs_dirs pkgs_dirs
|
||||
```
|
||||
|
||||
**坑 3 —— 被中止的 `conda create` 用 `conda env remove` 删不干净。**
|
||||
本次中止后 `conda env remove -n mcm-toolkit -y` 跑完,`D:\anaconda\envs\mcm-toolkit`
|
||||
**仍然存在**且 `conda env list` 里已看不到它。要手工删目录:
|
||||
|
||||
```powershell
|
||||
Remove-Item -LiteralPath 'D:\anaconda\envs\mcm-toolkit' -Recurse -Force
|
||||
```
|
||||
|
||||
**坑 4 —— 别把 conda env 建在项目目录里。**
|
||||
`conda create -p ./.conda` 会把 GB 级目录放进仓库,脏 `.gitignore` 也容易误提交。
|
||||
|
||||
**坑 5 —— 版本漂移。**
|
||||
本机 base 是 2025-10 装的,defaults 通道已是 2026-10 的版本,于是新建的 conda
|
||||
环境拿到的是更新的版本(pandas 从 2.3.3 跳到 **3.0.6**,是 major 升级)。
|
||||
见 §5,写代码时注意别依赖两套环境都有的行为。
|
||||
|
||||
---
|
||||
|
||||
## 五、两套环境的实际版本(已对齐)
|
||||
|
||||
**2026-10-01 已把 base 从 Anaconda 2025.12 升到 2026.07,两套环境版本已对齐。**
|
||||
|
||||
| 包 | venv(继承 base) | conda `mcm-toolkit` | |
|
||||
|---|---|---|---|
|
||||
| Python | 3.13.9 | 3.13.15 | 补丁号,可忽略 |
|
||||
| numpy | 2.4.6 | 2.4.6 | ✅ 一致 |
|
||||
| scipy | 1.18.1 | 1.18.1 | ✅ 一致(连 build 号都相同) |
|
||||
| pandas | 3.0.6 | 3.0.6 | ✅ 一致 |
|
||||
| matplotlib | 3.11.2 | 3.11.2 | ✅ 一致 |
|
||||
| scikit-learn | 1.9.1 | 1.9.1 | ✅ 一致 |
|
||||
| statsmodels | 0.15.0 | 0.15.0 | ✅ 一致 |
|
||||
| networkx | 3.6.1 | 3.6.1 | ✅ 一致 |
|
||||
| sympy | 1.14.0 | 1.14.0 | ✅ 一致 |
|
||||
| pytest | 9.1.1 | 9.1.1 | ✅ 一致 |
|
||||
| SALib / pymoo / pyright | 缺失 | 已装 | venv 本来就只做轻量开发 |
|
||||
| BLAS 后端 | **MKL** | **MKL** | ✅ 一致 |
|
||||
|
||||
### base 升级记录
|
||||
|
||||
[FENCE]powershell
|
||||
conda update -n base --all -y # 365 个包更新,下载 638 MB,耗时 1033 秒
|
||||
conda install -n mcm-toolkit numpy=2.4.6 -y # 把项目环境对齐到 base 的 numpy
|
||||
[FENCE]
|
||||
|
||||
* `conda` 本身从 25.11.1 升到 **26.7.3**。
|
||||
* 升级前 `conda list -n base --revisions` 里已留有 revision 记录,可回滚:
|
||||
`conda install -n base --revision <N>`。
|
||||
* **踩过的坑**:base 想装 `numpy=2.5.3` 时求解器会顺手把 BLAS 从 **MKL 换成 OpenBLAS**
|
||||
(`_anaconda_depends 2026.07 ..._mkl_1 → ..._openblas_1`、`blas 1.0-mkl → 1.0-openblas`)。
|
||||
`_anaconda_depends` 把 base 的 numpy 钉在 2.4.6,硬升会牵动整套线性代数后端,
|
||||
所以选择把项目环境的 numpy **降到 2.4.6** 来对齐,而不是动 base。
|
||||
* 升级期间遇到 `repo.anaconda.com` 读超时(默认 9.15 秒太短),已把
|
||||
`remote_read_timeout_secs` 调到 120、`remote_connect_timeout_secs` 调到 30、
|
||||
`remote_max_retries` 调到 5。这几项写在 `~/.condarc` 里。
|
||||
|
||||
## 六、国内镜像(需要装新包时)
|
||||
|
||||
```powershell
|
||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
|
||||
conda config --set show_channel_urls yes
|
||||
|
||||
# pip(对 venv 和 conda 环境都生效)
|
||||
python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
> 本机当前通过 `http://127.0.0.1:7993` 的 UniClash 代理直连,未配镜像。镜像只影响**下载**,
|
||||
> 不影响坑 1 的 ToS 行为。
|
||||
|
||||
---
|
||||
|
||||
## 七、可选依赖
|
||||
|
||||
核心 4 个包(`numpy scipy pandas matplotlib`)两套环境都有。其余按需:
|
||||
|
||||
```powershell
|
||||
python -m pip install -e ".[ml]" # sklearn statsmodels 已有
|
||||
python -m pip install -e ".[graph]" # networkx 已有
|
||||
python -m pip install -e ".[sensitivity]" # SALib conda 环境已有
|
||||
python -m pip install -e ".[symbolic]" # sympy 已有
|
||||
python -m pip install -e ".[moo]" # pymoo conda 环境已有
|
||||
python -m pip install -e ".[excel]" # openpyxl 已有
|
||||
python -m pip install -e ".[dev]" # pytest ruff pyright
|
||||
```
|
||||
|
||||
缺依赖时对应函数应当给出可读的安装提示,而不是裸抛 `ImportError`
|
||||
(写成约定,见 `docs/02-architecture.md` §4.6)。
|
||||
|
||||
---
|
||||
|
||||
## 八、脚本的编码约定(踩过的坑)
|
||||
|
||||
`scripts/` 下的 `.ps1` **必须是纯 ASCII + UTF-8 无 BOM + LF**。
|
||||
|
||||
原因是 Windows PowerShell 5.1 **在没有 BOM 时按 ANSI(本机 GBK)解析 `.ps1`**,
|
||||
中文注释和中文消息会被拆成非法字节,报出来的却是一堆莫名其妙的
|
||||
`Missing closing '}' in statement block` / `The string is missing the terminator`。
|
||||
(实测:带中文的 `doctor.ps1` 直接语法错误;改成纯 ASCII 后立刻正常。)
|
||||
|
||||
两种可行方案,本项目选了第二种:
|
||||
|
||||
1. 保留中文 → 必须存成 **UTF-8 with BOM**(违反 AGENTS.md 的"无 BOM"约定);
|
||||
2. **纯 ASCII,注释与输出用英文** → 符合 AGENTS.md,且任何编码下都能解析。← 采用
|
||||
|
||||
另一个相关的坑:**PowerShell 5.1 向原生程序传参时会吃掉双引号**,
|
||||
所以 `doctor.ps1` 里传给 `python -c` 的探测代码只能用单引号字符串。
|
||||
|
||||
---
|
||||
|
||||
## 九、已知的沙箱相关现象
|
||||
|
||||
如果在受限环境(文件系统只读 / 无网络)里跑 matplotlib,会看到:
|
||||
|
||||
```text
|
||||
Matplotlib created a temporary cache directory at ... because there was an issue with
|
||||
the default path (C:\Users\guzhu\.matplotlib)
|
||||
```
|
||||
|
||||
这不是配置问题,是 `~\.matplotlib` 不可写。正常终端里不会出现;
|
||||
真遇到时设置 `MPLCONFIGDIR` 指向可写目录即可:
|
||||
|
||||
```powershell
|
||||
$env:MPLCONFIGDIR = "$env:TEMP\matplotlib"
|
||||
```
|
||||
@@ -1,156 +0,0 @@
|
||||
# Git 工作流与远程托管
|
||||
|
||||
> 本仓库**已初始化**:分支 `main`,首次提交 `5319589`(76 文件),工作区干净。
|
||||
> 尚未配置远程,之后托管到你自己的 Gitea。
|
||||
|
||||
---
|
||||
|
||||
## 一、仓库约定
|
||||
|
||||
### 行尾
|
||||
|
||||
由 `.gitattributes` 强制:**仓库内一律 LF**,checkout 时不做自动转换。
|
||||
|
||||
原因:AGENTS.md 要求 shell 脚本 UTF-8 无 BOM + LF;而这个仓库会被队友在
|
||||
Windows / WSL / Linux 上 clone。不固定行尾的话,每次提交都会出现
|
||||
"整个文件都变了"的假 diff,review 时根本看不出真实改动。
|
||||
|
||||
例外:`*.bat` / `*.cmd` 保持 CRLF(cmd 解析需要)。
|
||||
|
||||
Windows 用户建议同时设:
|
||||
|
||||
```powershell
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
```
|
||||
|
||||
### 什么不入库
|
||||
|
||||
见 `.gitignore`。重点是三条:
|
||||
|
||||
| 忽略 | 为什么 |
|
||||
|---|---|
|
||||
| `.venv/`、`.conda/` | 环境可由 `scripts/setup-*.ps1` 重建,不该进版本库 |
|
||||
| `problems/**/raw/`、`**/*.xlsx`、`problems/**/*.csv` | 赛题原始数据体积大、可重新下载;只提交**处理脚本和结论** |
|
||||
| `figures/*.png`、`figures/*.pdf`、`tables/*.csv` | 中间产物。定稿的图请放进 `problems/<年份>_<题号>/figures/` 再提交 |
|
||||
|
||||
**判断标准:这个东西能不能由仓库里的其他文件重新生成?**
|
||||
能 → 不入库。
|
||||
|
||||
---
|
||||
|
||||
## 二、日常提交
|
||||
|
||||
```powershell
|
||||
git add -A
|
||||
git commit -m "feat(stats): 实现 bootstrap_ci"
|
||||
```
|
||||
|
||||
提交信息建议带模块前缀,方便以后回溯"这个函数是哪次加的":
|
||||
|
||||
| 前缀 | 用途 |
|
||||
|---|---|
|
||||
| `feat(<模块>)` | 新增函数 / 功能 |
|
||||
| `fix(<模块>)` | 修 bug |
|
||||
| `test(<模块>)` | 只改测试 |
|
||||
| `docs` | 只改文档 |
|
||||
| `env` | 环境、依赖、脚本 |
|
||||
| `chore` | 其他杂项 |
|
||||
|
||||
提交前跑一遍:
|
||||
|
||||
```powershell
|
||||
.\scripts\doctor.ps1
|
||||
```
|
||||
|
||||
它会同时检查 venv 和 conda 两套环境并跑测试。
|
||||
|
||||
---
|
||||
|
||||
## 三、推到自己的 Gitea
|
||||
|
||||
### 1. 在 Gitea 上建空仓库
|
||||
|
||||
不要勾选"初始化仓库"(不要生成 README / .gitignore / LICENSE),
|
||||
否则首次 push 会冲突。
|
||||
|
||||
### 2. 加远程并推送
|
||||
|
||||
```powershell
|
||||
cd D:\MCM&ICM_Toolkit
|
||||
|
||||
# 换成你自己的 Gitea 地址
|
||||
git remote add origin https://git.你的域名/用户名/mcm-icm-toolkit.git
|
||||
|
||||
# 如果 Gitea 用的是自签证书,先在浏览器里访问一次并信任,或:
|
||||
# git config --global http.sslBackend schannel
|
||||
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 3. 用 SSH(推荐,免密)
|
||||
|
||||
```powershell
|
||||
ssh-keygen -t ed25519 -C "guzhu@mcm-toolkit"
|
||||
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | Set-Clipboard
|
||||
# 粘贴到 Gitea → 设置 → SSH/GPG 密钥
|
||||
|
||||
git remote set-url origin git@git.你的域名:用户名/mcm-icm-toolkit.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 4. 仓库归属注意(重要)
|
||||
|
||||
`.git` 必须是**你自己的账户**创建的。若用沙箱化的 AI agent 通道代跑 `git init`,
|
||||
它可能以别的账户身份创建 `.git`,之后你自己跑 git 就会报:
|
||||
|
||||
fatal: detected dubious ownership in repository
|
||||
|
||||
正确做法是删掉重建(以本人账户):
|
||||
|
||||
Remove-Item -LiteralPath .git -Recurse -Force
|
||||
git init -b main
|
||||
|
||||
**不要**用 `git config --global --add safe.directory` 绕过——那只是把问题藏起来,
|
||||
而且会让 git 在真正的归属异常时也不再警告。
|
||||
|
||||
同理,沙箱通道写出的文件 Owner 会是沙箱账户,但权限继承自
|
||||
`Authenticated Users`(Modify),读写不受影响,只有 git 的归属检查会误报。
|
||||
|
||||
### 5. 如果不想公开
|
||||
|
||||
Gitea 建仓库时选 **Private**。队友通过
|
||||
仓库设置 → 协作者 → 添加成员 授权。
|
||||
|
||||
---
|
||||
|
||||
## 四、给队友的说明
|
||||
|
||||
队友 clone 后只需要:
|
||||
|
||||
```powershell
|
||||
git clone <你的仓库地址>
|
||||
cd mcm-icm-toolkit
|
||||
.\scripts\setup-venv.ps1 # 零下载,复用他们自己的 Anaconda base
|
||||
.\scripts\doctor.ps1
|
||||
```
|
||||
|
||||
`setup-venv.ps1` 用的是 `--system-site-packages`,所以他们本机得先有 Anaconda。
|
||||
如果队友没有 Anaconda,让他走 `scripts/setup-conda.ps1`。
|
||||
|
||||
---
|
||||
|
||||
## 五、回滚
|
||||
|
||||
环境升级出问题时的回退手段:
|
||||
|
||||
```powershell
|
||||
# conda 自己保留了 revision 历史
|
||||
conda list -n base --revisions
|
||||
conda install -n base --revision <N>
|
||||
|
||||
# 代码回滚
|
||||
git log --oneline
|
||||
git revert <commit>
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
# 文档索引
|
||||
|
||||
本目录是 MCM/ICM Toolkit 的需求、架构、方案与开发规范的**唯一落盘处**。
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [01-suggestion-review.md](01-suggestion-review.md) | 对 ChatGPT 建库建议的**审查与精简**:留什么、砍什么、补什么 |
|
||||
| [02-architecture.md](02-architecture.md) | 精简后的包结构、边界划分、统一接口约定 |
|
||||
| [03-roadmap.md](03-roadmap.md) | 功能清单分级(L0 / L1 / L2 / 不做)与开发纪律 |
|
||||
| [04-environment.md](04-environment.md) | conda / venv 环境配置、镜像、离线约束 |
|
||||
| [dev/function-card-template.md](dev/function-card-template.md) | S 档算法的「函数卡片」模板 |
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
1. 先读 `01-suggestion-review.md` —— 它决定后面所有事。
|
||||
2. 再读 `02-architecture.md` —— 它决定代码往哪写。
|
||||
3. 开工前读 `03-roadmap.md` 的对应条目 —— 它决定这次做什么、做到什么程度。
|
||||
4. 环境出问题看 `04-environment.md`。
|
||||
|
||||
> 项目级进度看根目录的 `STATUS.md`;本目录只放**不会频繁变**的内容。
|
||||
@@ -1,26 +0,0 @@
|
||||
# 算法卡片
|
||||
|
||||
S 档(自己实现的招牌算法)的**函数卡片**放这里,模板见
|
||||
[`../dev/function-card-template.md`](../dev/function-card-template.md)。
|
||||
|
||||
计划要写的卡片(见 `../03-roadmap.md` §3.1,约 15 张):
|
||||
|
||||
| 卡片 | 对应函数 | 状态 |
|
||||
|---|---|---|
|
||||
| `detect_outliers.md` | `data.detect_outliers` | ⬜ |
|
||||
| `bootstrap_ci.md` | `stats.bootstrap_ci` | ⬜ |
|
||||
| `monte_carlo.md` | `solve.monte_carlo` | ⬜ |
|
||||
| `monte_carlo_probability.md` | `solve.monte_carlo_probability` | ⬜ |
|
||||
| `monte_carlo_integral.md` | `solve.monte_carlo_integral` | ⬜ |
|
||||
| `dynamic_programming.md` | `solve.dynamic_programming` | ⬜ |
|
||||
| `knapsack_dp.md` | `solve.knapsack_dp` | ⬜ |
|
||||
| `simulated_annealing.md` | `solve.simulated_annealing` | ⬜ |
|
||||
| `local_sensitivity.md` | `solve.local_sensitivity` | ⬜ |
|
||||
| `sobol_analyze.md` | `solve.sobol_analyze` | ⬜ |
|
||||
| `entropy_weights.md` | `decision.entropy_weights` | ⬜ |
|
||||
| `topsis.md` | `decision.topsis` | ⬜ |
|
||||
| `ahp_weights.md` | `decision.ahp_weights` | ⬜ |
|
||||
| `gm11_fit_predict.md` | `decision.gm11_fit_predict` | ⬜ |
|
||||
|
||||
> 卡片是写给自己半年后看的,不是给评审看的。
|
||||
> 如果一张卡片最后只复述了 docstring,说明这个函数其实是 A 档,应该降档删卡片。
|
||||
@@ -1,18 +0,0 @@
|
||||
# API 参考
|
||||
|
||||
**本目录不放手写 API 文档。** 手写的 API 文档一定会和代码脱节。
|
||||
|
||||
接口的权威来源是三个地方,按优先级:
|
||||
|
||||
1. **源码 docstring** —— 每个函数的数学含义、参数、返回值、Notes、示例都写在那里;
|
||||
2. **`__all__`** —— 每个子包的公开 API 清单,`tests/test_api_surface.py` 会校验它和实际导出一致;
|
||||
3. **`docs/02-architecture.md`** —— 跨函数的约定(`Result` 字段、RNG 规则、`fit/predict/score` 协议)。
|
||||
|
||||
需要生成站点时用 mkdocs + mkdocstrings(后者直接从 docstring 生成):
|
||||
|
||||
```powershell
|
||||
pip install mkdocs mkdocstrings[python]
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
**本仓库不把 mkdocs 放进依赖**——它是可选的、和比赛无关的工具。
|
||||
@@ -1,137 +0,0 @@
|
||||
# S 档「函数卡片」模板
|
||||
|
||||
> **只给 S 档用**(约 15 个招牌算法,见 `docs/03-roadmap.md` §3.1)。
|
||||
> 其余 A 档函数只要求 docstring 一句话 + 一条 pytest,不写卡片。
|
||||
>
|
||||
> 卡片写在哪:`docs/algorithms/<函数名>.md`。
|
||||
> 为什么写:S 档是自己实现、以后会去改内部实现的算法,
|
||||
> 卡片是"半年后还看得懂当时为什么这么写"的唯一保障。
|
||||
|
||||
---
|
||||
|
||||
## 卡片模板
|
||||
|
||||
```markdown
|
||||
# <函数名>
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 模块 | `mcm_toolkit.<包>.<模块>` |
|
||||
| 档位 | S |
|
||||
| 参考来源 | <教材 / 论文 / 库;自己实现时参考了什么> |
|
||||
|
||||
## 1. 数学定义
|
||||
|
||||
<公式。用 LaTeX 写,例如 \`\\hat{\\theta}^* = \\arg\\min_\\theta ...\`。
|
||||
一句话说清输入输出分别是什么数学对象。>
|
||||
|
||||
## 2. 适用场景
|
||||
|
||||
**用**:<什么题型、什么数据条件下该用它>
|
||||
**不用**:<什么条件下它失效或不该用>
|
||||
|
||||
## 3. 参数与返回值
|
||||
|
||||
| 参数 | 类型 | 默认 | 含义 | 取值影响 |
|
||||
|---|---|---|---|---|
|
||||
| | | | | |
|
||||
|
||||
返回 `Result`:
|
||||
|
||||
| 字段 | 内容 |
|
||||
|---|---|
|
||||
| `params` | |
|
||||
| `metrics` | |
|
||||
| `data` | |
|
||||
|
||||
## 4. 复杂度
|
||||
|
||||
- 时间:`O(...)`
|
||||
- 空间:`O(...)`
|
||||
- **什么时候会跑不完**:<给出具体数字量级,例如"n > 1e6 时内存 8GB 打不住">
|
||||
|
||||
## 5. 数值稳定性
|
||||
|
||||
<浮点误差、除零、log(0)、上溢下溢、病态矩阵……
|
||||
写清"什么输入会出问题"和"代码里怎么防的"。>
|
||||
|
||||
## 6. 常见错误
|
||||
|
||||
1. <错误用法> → <后果> → <正确做法>
|
||||
2. …
|
||||
|
||||
## 7. 最小示例
|
||||
|
||||
```python
|
||||
# 能直接复制进 notebook 跑通的 5~15 行
|
||||
```
|
||||
|
||||
## 8. 单元测试要点
|
||||
|
||||
| 测什么 | 期望 |
|
||||
|---|---|
|
||||
| <已知解析解> | <精度要求> |
|
||||
| <边界输入> | <行为> |
|
||||
|
||||
## 9. MCM/ICM 用法
|
||||
|
||||
- **哪类题**:<例如"评价类 / 预测类 / 优化类">
|
||||
- **论文里怎么表述**:<一句话,能直接改写成论文的"方法"小节>
|
||||
- **图**:<配哪张图,用哪个 `viz` 函数>
|
||||
- **踩坑提醒**:<评审最可能质疑什么>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示例:`bootstrap_ci` 的卡片(示范,未定稿)
|
||||
|
||||
```markdown
|
||||
# bootstrap_ci
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 模块 | `mcm_toolkit.stats.bootstrap` |
|
||||
| 档位 | S |
|
||||
| 参考来源 | Efron (1979); Efron & Tibshirani (1993) *An Introduction to the Bootstrap* |
|
||||
|
||||
## 1. 数学定义
|
||||
|
||||
给定样本 \`x_1..x_n\` 与统计量 \`T(·)\`,从 \`x\` 中有放回抽 \`n\` 个得到 \`x^*\`,
|
||||
重复 \`B\` 次得 \`T^*_1..T^*_B\`。百分位法区间为
|
||||
\`[T^*_{(α/2·B)}, T^*_{(1-α/2)·B}]\`。
|
||||
|
||||
## 2. 适用场景
|
||||
|
||||
**用**:统计量没有解析分布(中位数、极差、自定义指标)、样本有偏、想给区间但不想假设正态。
|
||||
**不用**:\`n < 10\`(经验分布不可靠);数据有强时间相关性时(要用 block bootstrap)。
|
||||
|
||||
## 3. 参数与返回值
|
||||
|
||||
| 参数 | 类型 | 默认 | 含义 | 取值影响 |
|
||||
|---|---|---|---|---|
|
||||
| `statistic` | callable | `np.mean` | 任意 `f(sample) -> float` | 换 `np.median` 就得到中位数的 CI |
|
||||
| `n_resamples` | int | 10000 | 重采样次数 `B` | 越大区间端点越稳;10000 通常够 |
|
||||
| `confidence` | float | 0.95 | 置信水平 | |
|
||||
| `method` | str | `"percentile"` | `"percentile"` / `"bca"` | BCa 更准但慢 |
|
||||
|
||||
## 4. 复杂度
|
||||
|
||||
时间 \`O(B·n)\`;空间 \`O(B)\`(**按块处理,不落地 B×n 矩阵**)。
|
||||
`n=1e6, B=1e4` 约 \`1e10\` 次操作 → **跑不完**,此时应改用解析近似或降 `B`。
|
||||
|
||||
## 7. 最小示例
|
||||
|
||||
\`\`\`python
|
||||
import numpy as np
|
||||
from mcm_toolkit.stats import bootstrap_ci
|
||||
r = bootstrap_ci(heights, statistic=np.median, n_resamples=10000, random_state=42)
|
||||
print(r.params["low"], r.params["high"])
|
||||
\`\`\`
|
||||
|
||||
## 9. MCM/ICM 用法
|
||||
|
||||
- **哪类题**:任何要"给结论 + 给不确定性"的题,尤其评价类和预测类。
|
||||
- **论文里怎么表述**:"采用非参数 bootstrap(B=10000,seed=42)估计中位数的 95% 置信区间。"
|
||||
- **图**:预测曲线 + 阴影区间;用 `viz.plot`。
|
||||
- **踩坑提醒**:必须报 `B` 和 seed;BCa 与 percentile 结果不同时要说明选了哪个及理由。
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
# 教程
|
||||
|
||||
按"题型"组织的实战教程放这里,每篇对应一类赛题。
|
||||
|
||||
**目前为空是刻意的**:教程应该在**做过至少一道该类型的真题之后**再写,
|
||||
否则写出来的都是"字典式罗列",没有踩坑经验,没有价值。
|
||||
|
||||
计划(做完对应真题后再写):
|
||||
|
||||
| 教程 | 对应题型 | 触发条件 |
|
||||
|---|---|---|
|
||||
| `评价类.md` | 无量纲化 → 赋权 → TOPSIS/灰色 → 敏感性 | 做完一道评价类真题 |
|
||||
| `预测类.md` | 描述 → 分布拟合 → ARIMA/GM(1,1) → 区间 → 误差分析 | 做完一道预测类真题 |
|
||||
| `优化类.md` | 建模 → LP/IP → 启发式 → 多目标 → 敏感性 | 做完一道优化类真题 |
|
||||
| `仿真类.md` | 参数分布 → Monte Carlo → 收敛性 → 鲁棒性 | 做完一道仿真类真题 |
|
||||
|
||||
面向"怎么用这个工具箱"的快速上手看 [`../04-environment.md`](../04-environment.md) 和
|
||||
仓库根目录的 `README.md`;这里放的是**建模思路**。
|
||||
@@ -1,53 +0,0 @@
|
||||
# conda 环境复现文件(换机器 / 精确重建时用)。
|
||||
#
|
||||
# 它需要联网从 repo.anaconda.com 下载。断网时不要用它 —— 正确做法是
|
||||
# **比赛前就把环境建好**,并保留 D:\anaconda\pkgs 缓存。
|
||||
# 注意 conda create --clone base 在 conda 25.11 上不是"离线本地复制",
|
||||
# 实测会重放 base 的完整包列表并逐个下载,非常慢。
|
||||
#
|
||||
# 实测(2026-10-01):conda create 精准包列表约 14.5 分钟,下载约 3 GB,
|
||||
# 环境本体 1.55 GB。详见 docs/04-environment.md。
|
||||
#
|
||||
# 用法:
|
||||
# conda env create -n mcm-toolkit -f environment.yml
|
||||
# conda env update -n mcm-toolkit -f environment.yml --prune
|
||||
|
||||
name: mcm-toolkit
|
||||
|
||||
channels:
|
||||
- defaults
|
||||
|
||||
dependencies:
|
||||
# 解释器:与 2026-10 本机 Anaconda base 一致(3.13.9)
|
||||
- python=3.13
|
||||
|
||||
# 核心四件套(pyproject.toml 里的必装依赖)
|
||||
- numpy>=2.0
|
||||
- scipy>=1.14
|
||||
- pandas>=2.2
|
||||
- matplotlib>=3.9
|
||||
|
||||
# [ml]
|
||||
- scikit-learn>=1.5
|
||||
- statsmodels>=0.14
|
||||
|
||||
# [graph]
|
||||
- networkx>=3.3
|
||||
|
||||
# [symbolic]
|
||||
- sympy>=1.13
|
||||
|
||||
# [excel]
|
||||
- openpyxl>=3.1
|
||||
|
||||
# [dev]
|
||||
- pytest>=8.0
|
||||
- ruff>=0.6
|
||||
|
||||
# pip 部分:conda 通道里没有或版本不合的
|
||||
- pip
|
||||
- pip:
|
||||
- SALib>=1.4 # [sensitivity] Sobol / Morris
|
||||
- pymoo>=0.6 # [moo] 多目标优化
|
||||
- pyright>=1.1 # [dev] 类型检查
|
||||
- -e . # 本项目(可编辑安装)
|
||||
@@ -1,42 +0,0 @@
|
||||
"""端到端最小流程:读数据 → 清洗 → 描述 → bootstrap → 出图。
|
||||
|
||||
体量刻意压到最小,只用来确认"环境 + 包 + 出图"这条链路是通的。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
import mcm_toolkit as mcm
|
||||
|
||||
|
||||
def main() -> None:
|
||||
mcm.set_seed(20260101)
|
||||
|
||||
# 1) 造一份带缺失和离群点的数据(真实赛题里这步是 load_table)
|
||||
rng = np.random.default_rng(20260101)
|
||||
heights = rng.normal(170, 8, size=300)
|
||||
heights[[3, 77]] = [260.0, -10.0] # 两个录入错误
|
||||
|
||||
# 2) 找出离群点(约定:只标不删,删不删是人来决定)
|
||||
mask = np.asarray(mcm.data.detect_outliers(heights, method="iqr"))
|
||||
print(f"检出离群点 {mask.sum()} 个")
|
||||
|
||||
clean = heights[~mask]
|
||||
|
||||
# 3) 描述统计
|
||||
print(mcm.stats.describe(clean))
|
||||
|
||||
# 4) 中位数的 bootstrap 置信区间(统计量没有解析分布时的通用答案)
|
||||
ci = mcm.bootstrap_ci(clean, statistic=np.median, n_resamples=10000, random_state=20260101)
|
||||
print(f"中位数 = {np.median(clean):.2f}, 95% CI = [{ci.params['low']:.2f}, {ci.params['high']:.2f}]")
|
||||
|
||||
# 5) 出图(默认同时写 png + pdf,300 dpi)
|
||||
mcm.viz.mcm_style()
|
||||
fig, ax = mcm.viz.plot(clean, kind="hist", title="身高分布", xlabel="cm")
|
||||
paths = mcm.viz.save_figure(fig, "figures/quickstart_hist")
|
||||
print("已写出:", [str(p) for p in paths])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,48 +0,0 @@
|
||||
"""蒙特卡洛:估计 π、积 x²、算概率,并画收敛曲线。
|
||||
|
||||
三个例子覆盖 MC 的三种典型用法:
|
||||
积分(几何量)、概率估计、以及"n 要取多大才够"的收敛性验证。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mcm_toolkit.solve import monte_carlo_integral, monte_carlo_probability
|
||||
from mcm_toolkit.viz import mcm_style, plot_convergence, save_figure
|
||||
|
||||
|
||||
def main() -> None:
|
||||
mcm_style()
|
||||
|
||||
# --- 1) 估 π:单位正方形内落入四分之一圆的比例 × 4 ---
|
||||
def in_circle(X: np.ndarray) -> np.ndarray:
|
||||
return (X**2).sum(axis=1) <= 1.0
|
||||
|
||||
r = monte_carlo_probability(in_circle, lambda rng, **k: rng.uniform(-1, 1, size=(1, 2)), n=200_000, random_state=1)
|
||||
print(f"π ≈ {4 * r.metrics['probability']:.5f} (真值 3.14159)")
|
||||
|
||||
# --- 2) 积分 ∫₀¹ x² dx = 1/3 ---
|
||||
r2 = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200_000, random_state=2)
|
||||
print(f"∫₀¹x²dx ≈ {r2.data:.5f} (真值 0.33333)")
|
||||
|
||||
# --- 3) 收敛性:MC 误差是 O(1/√n),论文里要给这张图证明 n 选得够 ---
|
||||
ns = np.unique(np.round(np.logspace(2, 6, 40)).astype(int))
|
||||
errors = []
|
||||
for n in ns:
|
||||
est = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=int(n), random_state=7)
|
||||
errors.append(abs(est.data - 1 / 3))
|
||||
|
||||
fig, _ = plot_convergence(
|
||||
np.column_stack([ns, errors]),
|
||||
log=True,
|
||||
reference=None,
|
||||
title="Monte Carlo 误差随样本量收敛",
|
||||
xlabel="样本量 n",
|
||||
ylabel="绝对误差",
|
||||
)
|
||||
save_figure(fig, "figures/mc_convergence")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,45 +0,0 @@
|
||||
"""评价类题目的标准流程:无量纲化 → 赋权 → TOPSIS 排序 → 敏感性。
|
||||
|
||||
这是中文建模赛出现频率最高的题型之一。
|
||||
关键点(也是最容易翻车的地方):**指标方向必须显式声明**。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from mcm_toolkit.decision import entropy_weights, topsis
|
||||
from mcm_toolkit.solve import robustness_test
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# 3 个城市 × 4 个指标
|
||||
# 人均GDP(效益) 空气PM2.5(成本) 绿化率(效益) 通勤时间(成本)
|
||||
X = np.array(
|
||||
[
|
||||
[120.0, 35.0, 42.0, 38.0],
|
||||
[95.0, 55.0, 36.0, 29.0],
|
||||
[140.0, 28.0, 45.0, 47.0],
|
||||
]
|
||||
)
|
||||
labels = ["城市A", "城市B", "城市C"]
|
||||
benefit = [0, 2] # 只有第 0、2 列是"越大越好"
|
||||
|
||||
# 1) 客观赋权
|
||||
w = entropy_weights(X, benefit_columns=benefit)
|
||||
print("熵权:", np.round(w.params["weights"], 4))
|
||||
|
||||
# 2) TOPSIS 排序
|
||||
result = topsis(X, weights=w.params["weights"], benefit_columns=benefit, labels=labels)
|
||||
print(result.data)
|
||||
|
||||
# 3) 鲁棒性:权重扰动 ±10% 后排名还稳不稳?(论文里必须回答这个问题)
|
||||
def model(**kw):
|
||||
return topsis(X, weights=kw["weights"], benefit_columns=benefit).data["rank"].tolist()
|
||||
|
||||
rob = robustness_test(model, params={"weights": w.params["weights"]}, perturbation=0.1, n_samples=500, random_state=0)
|
||||
print("排名稳定性:", rob.metrics)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,21 +0,0 @@
|
||||
# 例子
|
||||
|
||||
每个模块一个能直接跑的最小例子。
|
||||
|
||||
> **当前状态**:这些脚本是**用法规格**,函数实现填好之前跑不通
|
||||
> (会抛 `NotImplementedError`)。实现顺序见 `docs/03-roadmap.md`。
|
||||
|
||||
| 文件 | 对应模块 | 覆盖的 L 档 |
|
||||
|---|---|---|
|
||||
| `00_quickstart.py` | 全流程 | L0 |
|
||||
| `01_clean_and_describe.py` | `data` + `stats` | L0 |
|
||||
| `02_regression_compare.py` | `models` | L1 |
|
||||
| `03_monte_carlo.py` | `solve/simulate` | L0 |
|
||||
| `04_decision_topsis.py` | `decision` | L1 |
|
||||
| `05_sensitivity.py` | `solve/sensitivity` | L1 |
|
||||
|
||||
跑法:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe examples\03_monte_carlo.py
|
||||
```
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -1,25 +0,0 @@
|
||||
# 历年真题
|
||||
|
||||
每做一道题,就把"题目 → 思路 → 代码 → 结论"沉淀到这里,
|
||||
并**反向补 1~3 个通用函数进 `src/mcm_toolkit/`**。
|
||||
|
||||
这是这个仓库能长期长大的唯一机制:做的是题,积累的是框架。
|
||||
|
||||
## 目录约定
|
||||
|
||||
```text
|
||||
problems/
|
||||
└── 2025_C/
|
||||
├── README.md 题目摘要 + 思路 + 结论 + 复盘(**必写**)
|
||||
├── data/ 处理后的数据(原始数据不入库,见 .gitignore)
|
||||
├── src/ 这道题的专用代码(不通用、不抽进 toolkit 的部分)
|
||||
├── figures/ 定稿图(png + pdf)
|
||||
└── paper/ 论文片段 / LaTeX
|
||||
```
|
||||
|
||||
## 复盘问题(每道题做完必答)
|
||||
|
||||
1. 这道题用到了 toolkit 里的哪些函数?哪些是**第一次用**?
|
||||
2. 哪些代码是**第二次、第三次写**了?→ 该抽进 toolkit。
|
||||
3. 哪个函数**不够用 / 用起来别扭**?→ 改接口。
|
||||
4. 论文里被评审/队友质疑的地方是什么?→ 对应的函数要补文档或补鲁棒性检验。
|
||||
@@ -1,64 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "mcm-toolkit"
|
||||
version = "0.0.1"
|
||||
description = "MCM/ICM 竞赛层工具箱:高频算法自实现 + 成熟库薄封装 + 统一结果接口"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "孤竹居士" }]
|
||||
keywords = ["MCM", "ICM", "mathematical-modeling", "competition"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Topic :: Scientific/Engineering :: Mathematics",
|
||||
]
|
||||
# 核心依赖只留 4 个:core / data / stats / viz 只需要这些。
|
||||
# 其余全部走 optional-dependencies,保证"比赛现场装不上包也能跑一半"。
|
||||
dependencies = [
|
||||
"numpy>=1.24",
|
||||
"scipy>=1.10",
|
||||
"pandas>=2.0",
|
||||
"matplotlib>=3.7",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
ml = ["scikit-learn>=1.3", "statsmodels>=0.14"]
|
||||
graph = ["networkx>=3.1"]
|
||||
sensitivity = ["SALib>=1.4"]
|
||||
symbolic = ["sympy>=1.12"]
|
||||
moo = ["pymoo>=0.6"]
|
||||
excel = ["openpyxl>=3.1"]
|
||||
all = ["mcm-toolkit[ml,graph,sensitivity,symbolic,moo,excel]"]
|
||||
dev = ["pytest>=7.4", "ruff>=0.4", "pyright>=1.1"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = { "" = "src" }
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py310"
|
||||
src = ["src", "tests"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "UP", "B", "SIM", "NPY", "RET", "PTH"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-q"
|
||||
filterwarnings = ["error::RuntimeWarning"]
|
||||
|
||||
[tool.pyright]
|
||||
include = ["src", "tests"]
|
||||
typeCheckingMode = "basic"
|
||||
@@ -1,103 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Environment self-check: interpreters, conda envs, dependencies, package import, tests.
|
||||
|
||||
.DESCRIPTION
|
||||
Run this on day one of a contest so environment problems surface before you
|
||||
start working, instead of three hours before the deadline.
|
||||
|
||||
Checks both environments:
|
||||
* .venv lightweight, offline-safe (10 MB)
|
||||
* conda env "mcm-toolkit" the main working environment
|
||||
|
||||
NOTE: this file is intentionally pure ASCII, UTF-8 without BOM, LF endings.
|
||||
Windows PowerShell 5.1 parses .ps1 as ANSI when there is no BOM, so non-ASCII
|
||||
characters (e.g. Chinese) turn into illegal bytes and cause syntax errors.
|
||||
|
||||
.PARAMETER SkipTests
|
||||
Do not run pytest.
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\doctor.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipTests
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
# Python snippet kept as a single-quoted here-string: it must not contain double
|
||||
# quotes, because PowerShell 5.1 strips them when passing arguments to native programs.
|
||||
$probe = @'
|
||||
import importlib
|
||||
mods = ['numpy','scipy','pandas','matplotlib','sklearn','statsmodels',
|
||||
'networkx','sympy','pytest','ruff','SALib','pymoo','pyright','openpyxl']
|
||||
for name in mods:
|
||||
try:
|
||||
mod = importlib.import_module(name)
|
||||
ver = getattr(mod, '__version__', '')
|
||||
print(' OK {:12s} {}'.format(name, ver))
|
||||
except Exception as exc:
|
||||
print(' MISS {:12s} ({})'.format(name, type(exc).__name__))
|
||||
'@
|
||||
|
||||
function Report-Interpreter {
|
||||
param([string]$Label, [string]$Py)
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "--- $Label ---" -ForegroundColor Cyan
|
||||
if (-not (Test-Path -LiteralPath $Py)) {
|
||||
Write-Host " not found: $Py" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
& $Py -c "import sys; print(' exe :', sys.executable); print(' python:', sys.version.split()[0])"
|
||||
& $Py -c $probe
|
||||
& $Py -c "import mcm_toolkit as m; print(' mcm_toolkit', m.__version__, '/', len(m.__all__), 'public names')"
|
||||
}
|
||||
|
||||
Write-Host '=== MCM/ICM Toolkit environment check ===' -ForegroundColor Cyan
|
||||
Write-Host "repo: $repoRoot"
|
||||
|
||||
Report-Interpreter -Label 'venv (.venv) - lightweight / offline-safe' -Py (Join-Path $repoRoot '.venv\Scripts\python.exe')
|
||||
|
||||
$conda = Get-Command conda -ErrorAction SilentlyContinue
|
||||
$condaExe = if ($conda) { $conda.Source } else { $null }
|
||||
if (-not $condaExe) {
|
||||
foreach ($c in @('D:\anaconda\Scripts\conda.exe', "$env:USERPROFILE\anaconda3\Scripts\conda.exe")) {
|
||||
if (Test-Path -LiteralPath $c) { $condaExe = $c; break }
|
||||
}
|
||||
}
|
||||
|
||||
if ($condaExe) {
|
||||
Write-Host ''
|
||||
Write-Host "--- conda ($condaExe) ---" -ForegroundColor Cyan
|
||||
& $condaExe --version | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ' environments:'
|
||||
& $condaExe env list | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
$condaPy = Join-Path (Split-Path -Parent (Split-Path -Parent $condaExe)) 'envs\mcm-toolkit\python.exe'
|
||||
Report-Interpreter -Label 'conda env "mcm-toolkit" - main working env' -Py $condaPy
|
||||
} else {
|
||||
Write-Host ''
|
||||
Write-Host '--- conda ---' -ForegroundColor Cyan
|
||||
Write-Host ' conda not found on PATH or in the usual locations' -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not $SkipTests) {
|
||||
Write-Host ''
|
||||
Write-Host '--- tests ---' -ForegroundColor Cyan
|
||||
Push-Location $repoRoot
|
||||
foreach ($py in @((Join-Path $repoRoot '.venv\Scripts\python.exe'), $condaPy)) {
|
||||
if ($py -and (Test-Path -LiteralPath $py)) {
|
||||
Write-Host " $py"
|
||||
& $py -m pytest -p no:cacheprovider -q 2>&1 | Select-Object -Last 1 | ForEach-Object { Write-Host " $_" }
|
||||
}
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Check finished.' -ForegroundColor Cyan
|
||||
@@ -1,136 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create the conda environment "mcm-toolkit".
|
||||
|
||||
.DESCRIPTION
|
||||
Three ways to build the environment, in order of preference:
|
||||
|
||||
(default) explicit package list, installed from the "defaults" channel.
|
||||
Needs internet. Measured on this machine: ~14.5 min, ~3 GB download.
|
||||
-FromYml same, but driven by environment.yml (better for reproducing later).
|
||||
-CloneBase mirrors the whole base environment. NOTE: on conda 25.11 this is NOT
|
||||
a fast local copy -- conda replays base's full package list and
|
||||
downloads/installs it. Use it only when you want an exact copy of base.
|
||||
|
||||
To work offline later, build the environment BEFORE the contest and keep the
|
||||
conda package cache (D:\anaconda\pkgs) intact.
|
||||
|
||||
NOTE: this file is intentionally pure ASCII, UTF-8 without BOM, LF endings.
|
||||
Windows PowerShell 5.1 parses .ps1 as ANSI when there is no BOM, so non-ASCII
|
||||
characters (e.g. Chinese) turn into illegal bytes and cause syntax errors.
|
||||
|
||||
.PARAMETER Name
|
||||
conda environment name. Default: mcm-toolkit.
|
||||
|
||||
.PARAMETER FromYml
|
||||
Build from environment.yml instead of the inline package list.
|
||||
|
||||
.PARAMETER CloneBase
|
||||
Clone the base environment instead of installing a explicit package list.
|
||||
|
||||
.PARAMETER Force
|
||||
Remove and recreate the environment if it already exists.
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\setup-conda.ps1
|
||||
Create "mcm-toolkit" with the science stack (needs internet).
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\setup-conda.ps1 -FromYml -Force
|
||||
Rebuild from environment.yml.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Name = 'mcm-toolkit',
|
||||
[switch]$FromYml,
|
||||
[switch]$CloneBase,
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
# Packages required by pyproject.toml's core deps plus the [ml] / [graph] /
|
||||
# [symbolic] / [excel] / [dev] extras. SALib, pymoo and pyright are pip-only
|
||||
# (see install after the conda step).
|
||||
$packages = @(
|
||||
'python=3.13',
|
||||
'numpy', 'scipy', 'pandas', 'matplotlib',
|
||||
'scikit-learn', 'statsmodels',
|
||||
'networkx', 'sympy', 'openpyxl',
|
||||
'pytest', 'ruff',
|
||||
'pip'
|
||||
)
|
||||
$pipOnly = @('SALib', 'pymoo', 'pyright')
|
||||
|
||||
function Find-Conda {
|
||||
$candidates = @(
|
||||
"$env:USERPROFILE\anaconda3\Scripts\conda.exe",
|
||||
"$env:USERPROFILE\miniconda3\Scripts\conda.exe",
|
||||
'D:\anaconda\Scripts\conda.exe',
|
||||
'C:\ProgramData\Anaconda3\Scripts\conda.exe',
|
||||
'C:\ProgramData\miniconda3\Scripts\conda.exe'
|
||||
)
|
||||
foreach ($c in $candidates) { if (Test-Path -LiteralPath $c) { return $c } }
|
||||
$onPath = Get-Command conda -ErrorAction SilentlyContinue
|
||||
if ($onPath) { return $onPath.Source }
|
||||
throw 'conda not found. Install Anaconda/Miniconda first.'
|
||||
}
|
||||
|
||||
$conda = Find-Conda
|
||||
Write-Host "[1/5] conda = $conda"
|
||||
|
||||
# PITFALL: the Anaconda 25.x ToS plugin fetches channel metadata from
|
||||
# repo.anaconda.com before every create/install and crashes when offline.
|
||||
# This env var auto-accepts the ToS: it skips the network call while keeping
|
||||
# the libmamba solver (unlike --no-plugins, which disables all plugins).
|
||||
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes'
|
||||
$env:CONDA_AUTO_UPDATE_CONDA = 'false'
|
||||
|
||||
# PITFALL: envs_dirs and pkgs_dirs must be writable, otherwise conda fails with
|
||||
# NotWritableError or "Error encountered while attempting to create cache directory".
|
||||
Write-Host '[2/5] current envs_dirs / pkgs_dirs:'
|
||||
& $conda config --show envs_dirs pkgs_dirs 2>$null | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
$existing = (& $conda env list --json | ConvertFrom-Json).envs |
|
||||
Where-Object { (Split-Path -Leaf $_) -eq $Name }
|
||||
if ($existing) {
|
||||
if (-not $Force) {
|
||||
Write-Host "Environment '$Name' already exists ($existing). Use -Force to rebuild." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
Write-Host "Environment '$Name' exists, -Force given, removing first ..."
|
||||
& $conda env remove -n $Name -y
|
||||
}
|
||||
|
||||
Write-Host '[3/5] creating the environment (this can take 10-20 minutes) ...'
|
||||
$sw = [Diagnostics.Stopwatch]::StartNew()
|
||||
if ($FromYml) {
|
||||
$yml = Join-Path $repoRoot 'environment.yml'
|
||||
if (-not (Test-Path -LiteralPath $yml)) { throw "Not found: $yml" }
|
||||
& $conda env create -n $Name -f $yml
|
||||
} elseif ($CloneBase) {
|
||||
& $conda create -n $Name --clone base -y
|
||||
} else {
|
||||
& $conda create -n $Name -y @packages
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) { throw "conda create failed (exit code $LASTEXITCODE)" }
|
||||
Write-Host (" done in {0:N0}s" -f $sw.Elapsed.TotalSeconds)
|
||||
|
||||
$envPy = Join-Path (Split-Path -Parent (Split-Path -Parent $conda)) "envs\$Name\python.exe"
|
||||
if (-not (Test-Path -LiteralPath $envPy)) {
|
||||
$envsRoot = (& $conda config --show envs_dirs | Select-String -Pattern '\S' | Select-Object -First 1).ToString().Trim()
|
||||
$envPy = Join-Path $envsRoot "$Name\python.exe"
|
||||
}
|
||||
|
||||
Write-Host '[4/5] installing pip-only extras (SALib, pymoo, pyright) + this project ...'
|
||||
& $envPy -m pip install --disable-pip-version-check --no-warn-script-location -q @pipOnly
|
||||
& $envPy -m pip install -e $repoRoot --no-deps --no-build-isolation --disable-pip-version-check -q
|
||||
|
||||
Write-Host '[5/5] verifying ...'
|
||||
& $envPy -c "import numpy, scipy, pandas, matplotlib, sklearn, statsmodels, networkx, sympy, SALib, pymoo, mcm_toolkit; print(' numpy', numpy.__version__, '| pandas', pandas.__version__, '| sklearn', sklearn.__version__); print(' mcm_toolkit', mcm_toolkit.__version__)"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Done. Activate with:' -ForegroundColor Green
|
||||
Write-Host " conda activate $Name"
|
||||
@@ -1,71 +0,0 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Create .venv inside the repo and install mcm-toolkit in editable mode.
|
||||
|
||||
.DESCRIPTION
|
||||
Uses --system-site-packages so the venv reuses the scientific stack already
|
||||
installed in Anaconda base. That makes the whole step OFFLINE: no package is
|
||||
downloaded, yet numpy / scipy / pandas / matplotlib / sklearn / statsmodels /
|
||||
networkx / sympy are all importable.
|
||||
|
||||
The editable install uses --no-deps --no-build-isolation: dependencies are
|
||||
already provided by base, and pip must not go online for a build backend.
|
||||
|
||||
NOTE: this file is intentionally pure ASCII and UTF-8 without BOM.
|
||||
Windows PowerShell 5.1 parses .ps1 as ANSI when there is no BOM, so non-ASCII
|
||||
characters (e.g. Chinese) turn into illegal bytes and cause syntax errors.
|
||||
|
||||
.PARAMETER Python
|
||||
Interpreter used to create the venv. Default: D:\anaconda\python.exe
|
||||
|
||||
.PARAMETER Recreate
|
||||
Delete and recreate .venv if it already exists.
|
||||
|
||||
.EXAMPLE
|
||||
.\scripts\setup-venv.ps1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Python = 'D:\anaconda\python.exe',
|
||||
[switch]$Recreate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$venvDir = Join-Path $repoRoot '.venv'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Python)) {
|
||||
$cmd = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) { throw "Python interpreter not found: $Python" }
|
||||
$Python = $cmd.Source
|
||||
Write-Host "Using python from PATH: $Python" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $venvDir) {
|
||||
if (-not $Recreate) {
|
||||
Write-Host '.venv already exists. Use -Recreate to rebuild.' -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host 'Removing existing .venv ...'
|
||||
Remove-Item -LiteralPath $venvDir -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $venvDir)) {
|
||||
Write-Host '[1/3] creating .venv (--system-site-packages, reuses Anaconda base) ...'
|
||||
& $Python -m venv --system-site-packages --prompt 'mcm-toolkit' $venvDir
|
||||
if ($LASTEXITCODE -ne 0) { throw 'venv creation failed' }
|
||||
}
|
||||
|
||||
$venvPy = Join-Path $venvDir 'Scripts\python.exe'
|
||||
|
||||
Write-Host '[2/3] editable install of mcm-toolkit (--no-deps --no-build-isolation) ...'
|
||||
& $venvPy -m pip install -e $repoRoot --no-deps --no-build-isolation --disable-pip-version-check
|
||||
if ($LASTEXITCODE -ne 0) { throw 'pip install -e failed' }
|
||||
|
||||
Write-Host '[3/3] verifying ...'
|
||||
& $venvPy -c "import mcm_toolkit, numpy, scipy, pandas, matplotlib; print('mcm_toolkit', mcm_toolkit.__version__); print('numpy', numpy.__version__)"
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Done. Activate with:' -ForegroundColor Green
|
||||
Write-Host ' .\.venv\Scripts\Activate.ps1'
|
||||
@@ -1,54 +0,0 @@
|
||||
"""mcm_toolkit —— MCM/ICM 竞赛层工具箱。
|
||||
|
||||
设计原则见 docs/02-architecture.md:
|
||||
|
||||
* 薄封装优先:能一行转发 SciPy / sklearn / statsmodels / NetworkX / SALib 的,就不自己写。
|
||||
* 自己实现是有目的的:只写"库里没有、或自己写才学得会"的算法。
|
||||
* 统一出口:拟合 / 求解 / 模拟 / 评价类函数一律返回 :class:`Result`。
|
||||
* 可复现:含随机的函数都接受 `random_state`。
|
||||
|
||||
顶层只导出子包与 6 个高频快捷函数,不做 `import *`,避免污染命名空间。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# 子包:按 `import mcm_toolkit as mcm` 之后 `mcm.stats.xxx` 使用
|
||||
from . import (
|
||||
core, # noqa: F401
|
||||
data, # noqa: F401
|
||||
decision, # noqa: F401
|
||||
models, # noqa: F401
|
||||
solve, # noqa: F401
|
||||
stats, # noqa: F401
|
||||
viz, # noqa: F401
|
||||
)
|
||||
from ._version import __version__
|
||||
|
||||
# 高频快捷函数:比赛时最常用的几个,允许 `mcm.monte_carlo(...)` 直接用
|
||||
from .core import Result, get_rng, set_seed # noqa: F401
|
||||
from .decision import topsis # noqa: F401
|
||||
from .models import compare_models # noqa: F401
|
||||
from .solve import monte_carlo, parameter_sweep # noqa: F401
|
||||
from .stats import bootstrap_ci # noqa: F401
|
||||
|
||||
# 公开 API 清单(不是"导出按钮",只列对外承诺稳定的名字)
|
||||
__all__ = [
|
||||
"__version__",
|
||||
# 子包
|
||||
"core",
|
||||
"data",
|
||||
"stats",
|
||||
"models",
|
||||
"solve",
|
||||
"decision",
|
||||
"viz",
|
||||
# 快捷函数
|
||||
"Result",
|
||||
"get_rng",
|
||||
"set_seed",
|
||||
"bootstrap_ci",
|
||||
"compare_models",
|
||||
"monte_carlo",
|
||||
"parameter_sweep",
|
||||
"topsis",
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
"""版本号唯一来源。"""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
@@ -1,23 +0,0 @@
|
||||
"""core —— 基础设施:结果对象、随机数、参数校验。
|
||||
|
||||
其他所有包都依赖本包;本包**不**依赖任何其他子包。
|
||||
"""
|
||||
|
||||
from .random import get_rng, set_seed
|
||||
from .result import Result
|
||||
from .validate import (
|
||||
check_array,
|
||||
check_probability,
|
||||
check_same_length,
|
||||
check_square_matrix,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Result",
|
||||
"get_rng",
|
||||
"set_seed",
|
||||
"check_array",
|
||||
"check_probability",
|
||||
"check_same_length",
|
||||
"check_square_matrix",
|
||||
]
|
||||
@@ -1,45 +0,0 @@
|
||||
"""随机数约定。
|
||||
|
||||
规则(见 docs/02-architecture.md §4.2):
|
||||
|
||||
* 所有含随机的公开函数签名都带 `random_state=None`,内部第一行取 rng。
|
||||
* 禁止使用 `np.random.*` 旧式全局接口,也禁止裸用标准库 `random`。
|
||||
* 论文附录必须写明 seed —— 提交要能复现。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
__all__ = ["get_rng", "set_seed"]
|
||||
|
||||
|
||||
def get_rng(random_state: int | None = None) -> np.random.Generator:
|
||||
"""取一个随机数生成器。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
random_state:
|
||||
`int` 时返回固定种子的独立生成器(可复现);
|
||||
`None` 时返回基于全局种子的生成器;
|
||||
已经是 :class:`np.random.Generator` 时原样返回(便于函数间串联)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> rng = get_rng(42)
|
||||
>>> bool(rng.random() == get_rng(42).random())
|
||||
True
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def set_seed(seed: int) -> None:
|
||||
"""设置全局随机种子。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> set_seed(42)
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
@@ -1,51 +0,0 @@
|
||||
"""统一结果对象。
|
||||
|
||||
第一版只有**一个** :class:`Result`(原建议里的 5 个 Result 类属于过早分类)。
|
||||
任何"拟合 / 求解 / 模拟 / 评价"函数都只返回它,不返回裸 tuple。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
"""所有计算型函数的统一返回值。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name:
|
||||
产生该结果的方法名,例如 `"monte_carlo"`。
|
||||
params:
|
||||
参数估计 / 决策变量取值 / 权重等"输入侧"的结论。
|
||||
metrics:
|
||||
评估指标,例如 `{"rmse": 0.31, "r2": 0.94}`。
|
||||
data:
|
||||
主要载荷:预测值、模拟轨迹、排序结果、收益数组等。
|
||||
success:
|
||||
是否正常完成。失败时仍返回 Result,但 `success=False` 且 `message` 说明原因。
|
||||
message:
|
||||
给人看的一句话说明,可直接抄进论文的"方法说明"。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = Result("demo", params={"alpha": 0.5}, metrics={"r2": 0.9})
|
||||
>>> r.success
|
||||
True
|
||||
"""
|
||||
|
||||
name: str
|
||||
params: dict[str, Any] | None = None
|
||||
metrics: dict[str, Any] | None = None
|
||||
data: Any = None
|
||||
success: bool = True
|
||||
message: str = ""
|
||||
extras: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# TODO(L0): 实现 __repr__,打印成"方法名 + 指标表"的紧凑形式,
|
||||
# 方便在 notebook 里直接肉眼检查、或复制进论文草稿。
|
||||
# TODO(L0): 实现 summary() -> str,返回可直接贴进论文的 Markdown 表格。
|
||||
def __repr__(self) -> str: # pragma: no cover - 待实现
|
||||
raise NotImplementedError
|
||||
@@ -1,54 +0,0 @@
|
||||
"""参数校验。
|
||||
|
||||
目的不是"教用户用库",而是**把错误提前到调用点**:
|
||||
比赛时最贵的是 debug 时间,一个 `ValueError: 数组长度不一致` 比
|
||||
十层 traceback 里的 `IndexError` 值钱得多。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
__all__ = [
|
||||
"check_array",
|
||||
"check_probability",
|
||||
"check_same_length",
|
||||
"check_square_matrix",
|
||||
]
|
||||
|
||||
|
||||
def check_array(x: Any, ndim: int | None = None, finite: bool = True) -> np.ndarray:
|
||||
"""转成 ndarray 并做基本检查(维度、有限值)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> check_array([1, 2, 3]).shape
|
||||
(3,)
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def check_same_length(*arrays: Any) -> None:
|
||||
"""检查多个数组长度一致,不一致时抛出带长度明细的 ValueError。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> check_same_length([1, 2], [3, 4])
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def check_probability(p: float) -> float:
|
||||
"""检查 p 落在 [0, 1],返回 float。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def check_square_matrix(a: Any) -> np.ndarray:
|
||||
"""检查是方阵,返回 ndarray。AHP 判断矩阵、转移矩阵都要用。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
@@ -1,26 +0,0 @@
|
||||
"""data —— 数据进出与清洗(最高频的一层)。
|
||||
|
||||
真实赛题数据往往缺列、单位不统一、日期格式混乱。
|
||||
本包的目标是让"拿到数据 → 能建模"这一步不超过 10 分钟。
|
||||
"""
|
||||
|
||||
from .clean import clean_missing, detect_outliers, impute_missing, winsorize
|
||||
from .io import load_table, save_table
|
||||
from .sample import bootstrap_sample, kfold_split, train_test_split
|
||||
from .transform import log_transform, normalize, one_hot_encode, standardize
|
||||
|
||||
__all__ = [
|
||||
"load_table",
|
||||
"save_table",
|
||||
"clean_missing",
|
||||
"impute_missing",
|
||||
"detect_outliers",
|
||||
"winsorize",
|
||||
"normalize",
|
||||
"standardize",
|
||||
"log_transform",
|
||||
"one_hot_encode",
|
||||
"train_test_split",
|
||||
"kfold_split",
|
||||
"bootstrap_sample",
|
||||
]
|
||||
@@ -1,81 +0,0 @@
|
||||
"""缺失值、离群值、缩尾。
|
||||
|
||||
`detect_outliers` 属于 S 档:不只是"算出来",还要能解释给队友听
|
||||
为什么要踢掉这几个点(论文里必须交代),所以配完整函数卡片
|
||||
(docs/dev/function-card-template.md)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
__all__ = ["clean_missing", "detect_outliers", "impute_missing", "winsorize"]
|
||||
|
||||
|
||||
def clean_missing(data: Any, axis: int = 0, threshold: float | None = None) -> Any:
|
||||
"""删除缺失过多的行/列。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis:
|
||||
0 = 按行删,1 = 按列删。
|
||||
threshold:
|
||||
缺失比例超过该值就删;`None` 表示只要有缺失就删。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> clean_missing(df, threshold=0.5) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def impute_missing(data: Any, method: str = "mean", **kwargs: Any) -> Any:
|
||||
"""缺失值填补。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"mean"` / `"median"` / `"mode"` / `"ffill"` / `"interpolate"` / `"knn"`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
论文里必须说明用了哪种填补、为什么。填 `mean` 之前先想一下
|
||||
"这个缺失是不是非随机缺失(MNAR)",否则结论可能被填补方式带偏。
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def detect_outliers(data: Any, method: Literal["iqr", "zscore"] = "iqr", **kwargs: Any) -> Any:
|
||||
"""标出离群点位置(返回布尔掩码,不直接删)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"iqr"`:Q1-1.5·IQR 之外(`k` 可调,默认 1.5);
|
||||
`"zscore"`:|z| > threshold(默认 3.0)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
与原数据同形的布尔 DataFrame/ndarray,`True` 表示该点是离群点。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**只标不删**是刻意的:删点是有代价的判断,必须由人来做,
|
||||
并且要在论文里写清楚删了几个、依据是什么。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> mask = detect_outliers([1, 2, 3, 4, 100], method="iqr")
|
||||
>>> bool(mask[-1])
|
||||
True
|
||||
"""
|
||||
# TODO(L0) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def winsorize(data: Any, lower: float = 0.01, upper: float = 0.99) -> Any:
|
||||
"""缩尾:把超出分位数的值压到分位点,而不是删除。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,37 +0,0 @@
|
||||
"""读写表格。
|
||||
|
||||
只保留两个函数:`load_table` / `save_table` 按扩展名分派。
|
||||
原建议里的 `load_csv` `load_excel` `save_csv` `save_excel` 四个函数
|
||||
只是在转发 pandas,合并后调用点更短。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["load_table", "save_table"]
|
||||
|
||||
|
||||
def load_table(path: str | Path, **kwargs: Any) -> Any:
|
||||
"""按扩展名自动选择读取方式,返回 DataFrame。
|
||||
|
||||
约定:
|
||||
* `.csv` 默认做编码探测(utf-8 → utf-8-sig → gbk → gb18030),
|
||||
国内赛题数据常见 GBK;
|
||||
* `.xlsx` / `.xls` 需要 `openpyxl`(`pip install "mcm-toolkit[excel]"`);
|
||||
* `na_values` 默认包含空串、`"NA"`、`"N/A"`、`"#N/A"`、`"null"`、`"-"`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> df = load_table("data.csv") # doctest: +SKIP
|
||||
>>> df.shape # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0): 编码探测 + 分派;Excel 缺 openpyxl 时给出可读报错。
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def save_table(df: Any, path: str | Path, index: bool = False, **kwargs: Any) -> Path:
|
||||
"""保存 DataFrame,返回实际写入的路径。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
@@ -1,46 +0,0 @@
|
||||
"""数据集划分与重采样。
|
||||
|
||||
原建议里的 `sample_with_replacement` / `sample_without_replacement`
|
||||
就是 `rng.choice(..., replace=)` 一行,已删;真正需要的是"划分"和"bootstrap"。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["bootstrap_sample", "kfold_split", "train_test_split"]
|
||||
|
||||
|
||||
def train_test_split(
|
||||
*arrays: Any,
|
||||
test_size: float = 0.2,
|
||||
shuffle: bool = True,
|
||||
stratify: Any = None,
|
||||
random_state: int | None = None,
|
||||
) -> list[Any]:
|
||||
"""按行划分训练/测试集。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=0) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def kfold_split(n_samples: int, n_splits: int = 5, shuffle: bool = True, random_state: int | None = None) -> list[tuple[Any, Any]]:
|
||||
"""返回 `n_splits` 组 (train_idx, test_idx)。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def bootstrap_sample(data: Any, n_resamples: int = 10000, random_state: int | None = None) -> Any:
|
||||
"""有放回重采样,返回形状 `(n_resamples, n)` 的重采样矩阵。
|
||||
|
||||
.. warning::
|
||||
`n_resamples=10000` 且 `n` 很大时这个矩阵会吃内存。
|
||||
只是想算区间的话用 :func:`mcm_toolkit.stats.bootstrap_ci`,
|
||||
它内部按块处理,不落地整个矩阵。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,58 +0,0 @@
|
||||
"""变换与无量纲化。
|
||||
|
||||
**重要约定(见 docs/02-architecture.md §4.4)**:
|
||||
本模块只做"无量纲化",**不改变指标方向**。
|
||||
效益型 / 成本型的方向问题归 `decision` 包处理,用 `benefit_columns` 显式声明,
|
||||
不允许默认假设"越大越好"——评价类题目翻车基本都翻在这里。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["log_transform", "normalize", "one_hot_encode", "standardize"]
|
||||
|
||||
|
||||
def normalize(x: Any, method: str = "minmax", feature_range: tuple[float, float] = (0.0, 1.0)) -> Any:
|
||||
"""无量纲化到给定区间。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"minmax"`:`(x - min) / (max - min)`,线性压缩到 `feature_range`;
|
||||
`"vector"`:`x / ||x||`,保持"份额"含义(熵权法前处理常用);
|
||||
`"max"`:`x / max(x)`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
minmax 对离群值极度敏感:一个 100 倍大的异常值会把其余所有点压成一条线。
|
||||
先跑 `detect_outliers` 再决定。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> normalize([1, 2, 3]).tolist()
|
||||
[0.0, 0.5, 1.0]
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def standardize(x: Any, ddof: int = 0) -> Any:
|
||||
"""标准化 `(x - mean) / std`,输出均值 0、标准差 1。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def log_transform(x: Any, offset: float | None = None) -> Any:
|
||||
"""对数变换,用于压缩右偏分布。
|
||||
|
||||
`offset=None` 时自动取 `1 - min(x)`(含 0 或负值时)保证定义域。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def one_hot_encode(data: Any, columns: list[str] | None = None, drop_first: bool = False) -> Any:
|
||||
"""类别变量独热编码。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,26 +0,0 @@
|
||||
"""decision —— 评价与决策。
|
||||
|
||||
中文赛高频模块(AHP / TOPSIS / 熵权 / 灰色)。
|
||||
原建议列了 8 个子模块,其中 `dea_efficiency`(要解一堆 LP,低频)与
|
||||
`fuzzy_comprehensive_evaluation`(隶属度函数没有通用形式,做不了通用封装)已删。
|
||||
|
||||
**使用顺序很重要**:先 `normalize`(无量纲化)→ 再定方向 `benefit_columns`
|
||||
→ 再 `entropy_weights`/`ahp_weights` → 最后 `topsis`。
|
||||
跳过第一步直接算权重,是评价类题目最常见的翻车方式。
|
||||
"""
|
||||
|
||||
from .ahp import ahp_consistency_ratio, ahp_weights
|
||||
from .grey import gm11_fit_predict, grey_relational_analysis
|
||||
from .topsis import topsis
|
||||
from .weighting import critic_weights, entropy_weights, equal_weights
|
||||
|
||||
__all__ = [
|
||||
"equal_weights",
|
||||
"entropy_weights",
|
||||
"critic_weights",
|
||||
"ahp_weights",
|
||||
"ahp_consistency_ratio",
|
||||
"topsis",
|
||||
"grey_relational_analysis",
|
||||
"gm11_fit_predict",
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
"""层次分析法(AHP)。
|
||||
|
||||
不过度设计:原建议第 22 节给出的两个函数就是全部需要的东西。
|
||||
关键在**一致性检验**——CR 不过关的权重不能拿去用,
|
||||
这是 AHP 唯一真正的技术含量所在。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["ahp_consistency_ratio", "ahp_weights"]
|
||||
|
||||
|
||||
def ahp_weights(matrix: Any) -> Any:
|
||||
"""由判断矩阵求权重。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matrix:
|
||||
正互反判断矩阵(`a_ij = 1/a_ji`,`a_ii = 1`),Saaty 1-9 标度。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"weights"}`,`metrics={"lambda_max", "CI", "CR", "consistent"}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
取权重有两种做法:
|
||||
* **特征向量法**(`eigenvector`):求最大特征值对应的特征向量并归一化,理论严谨;
|
||||
* **几何平均法**(`geometric`):各行元素连乘开 n 次方再归一化,手算/答辩时好用,
|
||||
与特征向量法结果几乎一致。
|
||||
本函数默认特征向量法,`method="geometric"` 可切换,两者都返回以便对照。
|
||||
|
||||
**CR > 0.1 一般判定为不一致**(n=2 时 CR 无意义,直接跳过)。
|
||||
有人会"改矩阵直到 CR 过关",这在论文里必须交代调整依据,否则是学术不端。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> A = np.array([[1, 3, 5], [1/3, 1, 2], [1/5, 0.5, 1]])
|
||||
>>> r = ahp_weights(A)
|
||||
>>> bool(r.metrics["CR"] < 0.1)
|
||||
True
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def ahp_consistency_ratio(matrix: Any) -> Any:
|
||||
"""只算一致性指标,不返回权重。
|
||||
|
||||
单独提供它是因为:**拿到判断矩阵后第一步就该看 CR**,
|
||||
不过关时先回去调整矩阵,权重算出来也用不了。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,71 +0,0 @@
|
||||
"""灰色系统:关联分析与 GM(1,1) 预测。
|
||||
|
||||
中文建模赛特色方法。原建议第 23 节的方向没问题,函数名做了合并
|
||||
(`gm11_fit` + `gm11_predict` → `gm11_fit_predict`,因为 GM(1,1) 极少只用一半)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["gm11_fit_predict", "grey_relational_analysis"]
|
||||
|
||||
|
||||
def grey_relational_analysis(X: Any, reference: Any = None, rho: float = 0.5) -> Any:
|
||||
"""灰色关联分析。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reference:
|
||||
参考序列。`None` 时按"每个指标取最优值"自动构造。
|
||||
rho:
|
||||
分辨系数,通常取 0.5。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是各方案与参考序列的关联系数、关联度与排名。
|
||||
|
||||
Notes
|
||||
-----
|
||||
关联度对 `rho` 敏感:`rho` 越小,分辨力越强但稳定性越差。
|
||||
论文里应说明 `rho` 取值并做一次敏感性(`rho = 0.3 / 0.5 / 0.7`)。
|
||||
另外它**要求所有指标同向且无量纲**,前置处理同熵权法。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = grey_relational_analysis([[1, 2], [3, 1], [2, 3]]) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def gm11_fit_predict(series: Any, steps: int = 1) -> Any:
|
||||
"""GM(1,1) 灰色预测(拟合 + 预测一步到位)。
|
||||
|
||||
流程:一次累加生成(1-AGO)→ 建立白化微分方程 `dx/dt + ax = b`
|
||||
→ 最小二乘估 `a, b` → 还原预测序列。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
steps:
|
||||
向前预测步数。GM(1,1) 适合**短序列(4~10 个点)**的短期预测。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"a", "b"}`,`metrics={"c_ratio", "precision"}`,
|
||||
`data` 是含 `actual / fitted / forecast` 三列的 DataFrame。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* **必须做后验差检验**:后验差比值 `C = S2/S1`(残差标准差 / 原序列标准差)
|
||||
和 小误差概率 `P`。`C < 0.35` 一级,`< 0.5` 合格,`>= 0.65` 不能用。
|
||||
只给预测值不给 `C`、`P` 是论文里的硬伤。
|
||||
* GM(1,1) 本质是指数增长外推,对**先增后减**或波动序列效果很差,
|
||||
用之前先看序列形状。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = gm11_fit_predict([2.87, 3.05, 3.24, 3.43, 3.64]) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
@@ -1,59 +0,0 @@
|
||||
"""TOPSIS 逼近理想解排序。
|
||||
|
||||
原建议把标准化(`benefit_normalize` 等 4 个函数)单列一节,
|
||||
这里合并进 `topsis` 的 `method` 参数——因为它们是同一个流程的步骤,
|
||||
拆开反而容易出现"标准化用了一套、权重算的时候又换一套"的错位。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["topsis"]
|
||||
|
||||
|
||||
def topsis(
|
||||
X: Any,
|
||||
weights: Any = None,
|
||||
benefit_columns: Any = None,
|
||||
method: str = "vector",
|
||||
labels: Any = None,
|
||||
) -> Any:
|
||||
"""TOPSIS 综合评价。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X:
|
||||
决策矩阵,行 = 待评价方案,列 = 指标。
|
||||
weights:
|
||||
权重。`None` 时用等权(并在 `message` 里提示)。
|
||||
benefit_columns:
|
||||
效益型指标的列名/下标,其余按成本型处理。
|
||||
**必填或显式全选**,不允许默认假设——方向搞反会让排名完全颠倒。
|
||||
method:
|
||||
列向量的无量纲化方式:`"vector"`(`x/√Σx²`,TOPSIS 原始做法,默认)
|
||||
或 `"minmax"`。
|
||||
labels:
|
||||
方案名,用于结果表。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是含 `D+ / D- / closeness / rank` 四列、按 `rank` 排好的表,
|
||||
`params["weights"]` 回显实际使用的权重。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 正理想解 = 各指标最优值,负理想解 = 各指标最劣值;
|
||||
贴近度 `C = D- / (D+ + D-)`,**C 越大越好**。
|
||||
* TOPSIS 的排名对权重敏感。论文里应附"权重扰动 ±10% 后排名是否稳定"的
|
||||
敏感性分析——直接调 `solve.robustness_test`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = topsis(np.array([[1., 2.], [3., 1.], [2., 3.]]), benefit_columns=[0, 1])
|
||||
>>> list(r.data["rank"])
|
||||
[2, 3, 1]
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
@@ -1,71 +0,0 @@
|
||||
"""客观赋权:熵权法、CRITIC。
|
||||
|
||||
★ `entropy_weights` 是 S 档,建议自己实现。
|
||||
熵权法是中文建模赛出现频率最高的赋权方法,实现只有 20 行,
|
||||
但"指标方向、无量纲化、对数 0 处理"三个坑必须理解清楚才能写对。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["critic_weights", "entropy_weights", "equal_weights"]
|
||||
|
||||
|
||||
def equal_weights(n: int) -> Any:
|
||||
"""等权,即 `1/n`。作为赋权结果的对照基线,论文里应该报。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def entropy_weights(
|
||||
X: Any,
|
||||
benefit_columns: Any = None,
|
||||
eps: float = 1e-12,
|
||||
) -> Any:
|
||||
"""熵权法。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X:
|
||||
决策矩阵,行 = 方案,列 = 指标。**必须已无量纲化且已做正向化**。
|
||||
benefit_columns:
|
||||
效益型(越大越好)指标的列名/下标。其余按成本型处理。
|
||||
`None` 时假设全部是效益型,并在 `message` 里提示——避免静默搞错方向。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"weights": 权重向量}`,
|
||||
`extras["entropy"]` 是各指标的熵值,
|
||||
`extras["redundancy"]` 是差异系数 `1 - e`(权重就来自它的归一化)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
三个必须处理的细节:
|
||||
1. **方向**:成本型指标要先正向化(`max - x` 或 `1/x`),否则权重完全错。
|
||||
2. **量纲**:熵权法要求 `x >= 0`,且对量纲敏感——先做 min-max 或向量归一。
|
||||
3. **零值**:`p·ln(p)` 在 `p=0` 时按 0 处理(极限为 0),不能直接算,否则出 NaN。
|
||||
|
||||
熵权法的缺点是**只看数据离散程度,不看指标的实际重要性**。
|
||||
实践上常和 AHP 做组合赋权:`w = α·w_entropy + (1-α)·w_ahp`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = entropy_weights(np.array([[1., 2.], [3., 1.], [2., 3.]]))
|
||||
>>> bool(np.isclose(r.params["weights"].sum(), 1.0))
|
||||
True
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def critic_weights(X: Any, benefit_columns: Any = None) -> Any:
|
||||
"""CRITIC 赋权。
|
||||
|
||||
与熵权法的区别:熵权法只看"对比强度"(标准差),
|
||||
CRITIC 还乘上"冲突性"(与其他指标的相关性)。
|
||||
`C_j = σ_j · Σ_k (1 - r_jk)`,权重 = `C_j / ΣC`。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,41 +0,0 @@
|
||||
"""models —— 建模:回归 / 时间序列 / 机器学习(薄封装)。
|
||||
|
||||
原建议把 `regression/` 和 `ml/` 拆成两个包,但两边的
|
||||
`fit_linear` 与 `fit_linear_regression` 明显重复。
|
||||
这里合并为一个包,共享一套 `fit / predict / score` 协议和一份指标实现。
|
||||
"""
|
||||
|
||||
from .metrics import (
|
||||
adjusted_r2,
|
||||
compare_models,
|
||||
mae,
|
||||
mape,
|
||||
r2_score,
|
||||
rmse,
|
||||
smape,
|
||||
)
|
||||
from .ml import cross_validate, fit_forest, fit_logistic, kmeans, pca
|
||||
from .regression import fit_linear, fit_nonlinear, fit_polynomial, predict
|
||||
from .timeseries import exponential_smoothing, fit_arima, forecast
|
||||
|
||||
__all__ = [
|
||||
"fit_linear",
|
||||
"fit_polynomial",
|
||||
"fit_nonlinear",
|
||||
"predict",
|
||||
"exponential_smoothing",
|
||||
"fit_arima",
|
||||
"forecast",
|
||||
"fit_forest",
|
||||
"fit_logistic",
|
||||
"kmeans",
|
||||
"pca",
|
||||
"cross_validate",
|
||||
"rmse",
|
||||
"mae",
|
||||
"mape",
|
||||
"smape",
|
||||
"r2_score",
|
||||
"adjusted_r2",
|
||||
"compare_models",
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
"""模型协议。
|
||||
|
||||
所有 `fit_*` 返回的对象都满足这个协议,于是:
|
||||
`compare_models({"linear": m1, "rf": m2}, X, y)`
|
||||
才能对一批异构模型统一执行、统一打分。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
import numpy as np
|
||||
|
||||
__all__ = ["Model"]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Model(Protocol):
|
||||
"""`fit / predict / score` 三件套。"""
|
||||
|
||||
def fit(self, X: Any, y: Any, **kwargs: Any) -> Model:
|
||||
"""拟合,返回 self(支持链式调用)。"""
|
||||
...
|
||||
|
||||
def predict(self, X: Any) -> np.ndarray:
|
||||
"""预测。"""
|
||||
...
|
||||
|
||||
def score(self, X: Any, y: Any) -> dict[str, float]:
|
||||
"""返回指标 dict(不是单个 float),便于直接拼成对比表。"""
|
||||
...
|
||||
@@ -1,94 +0,0 @@
|
||||
"""回归/分类指标 + 模型对比。
|
||||
|
||||
原建议把这批函数同时放在 `regression/`、`utils/metrics.py` 和第 26 节的
|
||||
`analysis/` 三处,这里统一到一处。
|
||||
|
||||
注意:本模块被 `models` 与 `solve` 共用,属于"公共数学",但为了少建一个包,
|
||||
放在 `models` 下;`solve` 单向依赖 `models` 是允许的。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"adjusted_r2",
|
||||
"compare_models",
|
||||
"mae",
|
||||
"mape",
|
||||
"r2_score",
|
||||
"rmse",
|
||||
"smape",
|
||||
]
|
||||
|
||||
|
||||
def rmse(y_true: Any, y_pred: Any) -> float:
|
||||
"""均方根误差。与 y 同量纲,最常用。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def mae(y_true: Any, y_pred: Any) -> float:
|
||||
"""平均绝对误差。对离群值比 RMSE 稳健。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def mape(y_true: Any, y_pred: Any) -> float:
|
||||
"""平均绝对百分比误差。无量纲,但 y 含 0 时会炸(用 smape 替代)。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def smape(y_true: Any, y_pred: Any) -> float:
|
||||
"""对称 MAPE,y 含 0 也安全。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def r2_score(y_true: Any, y_pred: Any) -> float:
|
||||
"""决定系数 R²。注意:加变量一定会升高,跨模型比较要看 adjusted_r2。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def adjusted_r2(y_true: Any, y_pred: Any, n_features: int) -> float:
|
||||
"""调整 R²,惩罚变量个数。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def compare_models(
|
||||
models: dict[str, Any],
|
||||
X: Any,
|
||||
y: Any,
|
||||
metrics: tuple[str, ...] = ("rmse", "mae", "r2"),
|
||||
cv: int = 5,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""把若干模型放在一起打分,返回对比表。
|
||||
|
||||
这是赛题里最省时间的函数之一。队友问"要不要上复杂模型"时,
|
||||
跑一次这个就能用数字回答,不用争论半小时。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
models:
|
||||
`{"linear": m1, "poly3": m2, "random_forest": m3}`。
|
||||
每个模型需满足 :class:`mcm_toolkit.models.base.Model` 协议(有 `fit`/`predict`)。
|
||||
cv:
|
||||
交叉验证折数;`cv=0` 表示在全部数据上拟合后打分(只看拟合能力时用)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame:行 = 模型,列 = 指标;按第一列指标升序(越小越好)或降序排列。
|
||||
可直接 `save_table(result, "compare.tex", fmt="latex")` 贴进论文。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> tbl = compare_models({"linear": m1, "poly": m2}, X, y) # doctest: +SKIP
|
||||
>>> tbl.index[0] # doctest: +SKIP
|
||||
'poly'
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,88 +0,0 @@
|
||||
"""机器学习薄封装。
|
||||
|
||||
对应原建议的 B 类。规模要控制住:
|
||||
第一版只封 commonly-used 的四个模型 + 两个工具函数,
|
||||
不追求把 sklearn 的 estimator 列表抄一遍。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["cross_validate", "fit_forest", "fit_logistic", "kmeans", "pca"]
|
||||
|
||||
|
||||
def fit_forest(X: Any, y: Any, task: str = "auto", n_estimators: int = 300, random_state: int | None = None, **kwargs: Any) -> Any:
|
||||
"""随机森林(分类或回归)。
|
||||
|
||||
`task="auto"`:按 y 的取值类型自动判断分类还是回归。
|
||||
|
||||
Notes
|
||||
-----
|
||||
随机森林最大的价值不是精度,而是 **feature_importances_**——
|
||||
赛题论文里"哪些因素最重要"这一节经常靠它。
|
||||
返回值里带上排序后的重要性表。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = fit_forest(X, y)
|
||||
>>> list(r.metrics) # doctest: +SKIP
|
||||
['r2', 'rmse', 'mae']
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def fit_logistic(X: Any, y: Any, C: float = 1.0, random_state: int | None = None, **kwargs: Any) -> Any:
|
||||
"""逻辑回归(二分类)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
`metrics` 里除了准确率,必须同时给 **混淆矩阵、精确率/召回率、AUC**。
|
||||
只报准确率在不平衡数据上会给出误导性结论。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def kmeans(X: Any, n_clusters: int = 3, standardize_first: bool = True, random_state: int | None = None, **kwargs: Any) -> Any:
|
||||
"""K-means 聚类。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
standardize_first:
|
||||
默认先标准化。**量纲不同的特征不做标准化,K-means 的结果等于随机**。
|
||||
|
||||
Notes
|
||||
-----
|
||||
`metrics` 给轮廓系数(silhouette)而不是 inertia——
|
||||
轮廓系数有界且可比,inertia 只能同数据集内比。
|
||||
选 `n_clusters` 请画肘部图或轮廓系数曲线,不要拍脑袋。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def pca(X: Any, n_components: int | float | None = None, standardize_first: bool = True) -> Any:
|
||||
"""主成分分析。
|
||||
|
||||
`n_components` 为 `float`(如 0.95)时表示"保留 95% 方差"。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是投影后的得分矩阵,`extras["loadings"]` 是载荷矩阵
|
||||
(论文里解释"第一主成分代表什么"要用它)。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def cross_validate(model: Any, X: Any, y: Any, cv: int = 5, scoring: str | None = None, random_state: int | None = None) -> Any:
|
||||
"""交叉验证,返回每折分数与均值±标准差。
|
||||
|
||||
Notes
|
||||
-----
|
||||
报结果时**必须带标准差**。只报均值会把"5 折里 1 折崩了"这种事掩盖掉。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,70 +0,0 @@
|
||||
"""回归(自己封装,底层调用成熟库)。
|
||||
|
||||
对应原建议的 B 类:学的是"怎样把成熟工具变成符合 MCM 工作流的接口",
|
||||
不是重写最小二乘。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["fit_linear", "fit_nonlinear", "fit_polynomial", "predict"]
|
||||
|
||||
|
||||
def fit_linear(X: Any, y: Any, intercept: bool = True, weights: Any | None = None, alpha: float = 0.0) -> Any:
|
||||
"""线性回归。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
weights:
|
||||
加权最小二乘的权重(数据点可信度不同、或想消除异方差时用)。
|
||||
alpha:
|
||||
> 0 时做岭回归(共线性严重时用),= 0 是普通最小二乘。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"coef", "intercept"}`,`metrics={"r2", "adjusted_r2", "rmse", "mae"}`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = fit_linear(X, y)
|
||||
>>> r.metrics["r2"] < 1.0
|
||||
True
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def fit_polynomial(X: Any, y: Any, degree: int = 2, alpha: float = 0.0) -> Any:
|
||||
"""多项式回归。`degree` 大了会过拟合,务必看 `rs.plot_residuals` 和 adjusted_r2。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def fit_nonlinear(func: Any, X: Any, y: Any, p0: Any = None, bounds: Any = None, **kwargs: Any) -> Any:
|
||||
"""非线性最小二乘拟合(底层 `scipy.optimize.curve_fit`)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func:
|
||||
`f(x, *params) -> y` 形式的目标函数,例如 logistic / Gompertz 生长曲线。
|
||||
p0:
|
||||
初值。非线性拟合**对初值极度敏感**,给不出好初值时要先画图估一个。
|
||||
|
||||
Notes
|
||||
-----
|
||||
返回的 `metrics` 里额外给 `pcov`(参数协方差矩阵)与参数标准误,
|
||||
论文里报参数时要带标准误,否则不严谨。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def predict(model: Any, X: Any) -> Any:
|
||||
"""对任意 `fit_*` 返回的 `Result` 或符合协议的模型做预测。
|
||||
|
||||
存在的意义:让 `predict` 既能吃 `Result` 也能吃 sklearn 原生模型,
|
||||
统一比赛代码的写法。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,67 +0,0 @@
|
||||
"""时间序列(MCM C 必备)。
|
||||
|
||||
原建议列了 `moving_average` `weighted_moving_average` `holt_trend`
|
||||
`seasonal_decompose` `lag_features` `forecast_interval` 等一堆函数,
|
||||
但 statsmodels 已经全部提供,按它自己的原则("做薄封装,不要重写框架")
|
||||
只保留三个真正有封装价值的入口。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["exponential_smoothing", "fit_arima", "forecast"]
|
||||
|
||||
|
||||
def exponential_smoothing(series: Any, alpha: float | None = None, trend: str | None = None, seasonal: str | None = None, periods: int | None = None) -> Any:
|
||||
"""指数平滑(底层 `statsmodels.tsa.holtwinters`)。
|
||||
|
||||
`alpha=None` 时自动用极大似然估计平滑系数——比自己拍脑袋定 `alpha` 靠谱。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = exponential_smoothing(y, trend="add") # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def fit_arima(series: Any, order: tuple[int, int, int] = (1, 1, 1), seasonal_order: tuple[int, int, int, int] | None = None, auto: bool = False) -> Any:
|
||||
"""ARIMA 拟合。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
auto:
|
||||
`True` 时按 AIC 网格搜索 (p, d, q),省去手动定阶;
|
||||
数据量大时较慢,比赛里建议先手动看一眼 ACF/PACF。
|
||||
order:
|
||||
`(p, d, q)`——自回归阶数、差分阶数、移动平均阶数。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`metrics={"aic", "bic"}`,`extras["model"]` 是 statsmodels 结果对象
|
||||
(需要 `summary()`、残差诊断时直接拿)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
ARIMA 要求序列**平稳**。`d` 是差分阶数,但差分会丢信息,
|
||||
非平稳序列更推荐先看是不是有结构突变或季节成分。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def forecast(model: Any, steps: int, alpha: float = 0.05) -> Any:
|
||||
"""向后预测 `steps` 步,并给出预测区间。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是含 `forecast / lower / upper` 三列的 DataFrame。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**预测区间随步长急剧变宽是正常的**,不要只画中心线——
|
||||
论文里只画一条线而不给区间,是很常见的扣分点。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,58 +0,0 @@
|
||||
"""solve —— 求解:优化 / 模拟 / 敏感性 / 图 / 动力学。
|
||||
|
||||
竞赛主力包。原建议把 `optimization` `simulation` `sensitivity` 拆成三个包,
|
||||
但它们本质是同一件事的三种形态——"把模型反复算"——共享
|
||||
`Result`、`rng`、并行与缓存约定。合成一个包,这些约定只写一遍。
|
||||
|
||||
`graph` 与 `dynamics` 是 v0.2 内容(低频,且 NetworkX / SymPy 已覆盖大部分需求)。
|
||||
"""
|
||||
|
||||
from .optimize import (
|
||||
dynamic_programming,
|
||||
integer_program,
|
||||
knapsack_dp,
|
||||
linear_program,
|
||||
minimize,
|
||||
pareto_front,
|
||||
simulated_annealing,
|
||||
)
|
||||
from .sensitivity import (
|
||||
local_sensitivity,
|
||||
morris_analyze,
|
||||
one_at_a_time,
|
||||
parameter_sweep,
|
||||
robustness_test,
|
||||
sobol_analyze,
|
||||
)
|
||||
from .simulate import (
|
||||
markov_simulate,
|
||||
markov_steady_state,
|
||||
mm1_metrics,
|
||||
monte_carlo,
|
||||
monte_carlo_integral,
|
||||
monte_carlo_probability,
|
||||
random_walk,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"minimize",
|
||||
"linear_program",
|
||||
"integer_program",
|
||||
"dynamic_programming",
|
||||
"knapsack_dp",
|
||||
"simulated_annealing",
|
||||
"pareto_front",
|
||||
"monte_carlo",
|
||||
"monte_carlo_probability",
|
||||
"monte_carlo_integral",
|
||||
"random_walk",
|
||||
"markov_simulate",
|
||||
"markov_steady_state",
|
||||
"mm1_metrics",
|
||||
"local_sensitivity",
|
||||
"one_at_a_time",
|
||||
"parameter_sweep",
|
||||
"robustness_test",
|
||||
"sobol_analyze",
|
||||
"morris_analyze",
|
||||
]
|
||||
@@ -1,79 +0,0 @@
|
||||
"""微分方程与动力系统(v0.2)。
|
||||
|
||||
原建议把 `dynamics/` 做成独立包。考虑到:
|
||||
1. 只有 MCM A 用得上,一年一题;
|
||||
2. 底层 SymPy(解析)+ SciPy(数值)已经覆盖全部需求;
|
||||
|
||||
降级为一个模块。依赖:`pip install "mcm-toolkit[symbolic]"`(sympy)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"find_equilibrium",
|
||||
"local_stability",
|
||||
"solve_ode",
|
||||
"solve_system",
|
||||
]
|
||||
|
||||
|
||||
def solve_ode(
|
||||
derivative: Callable[..., Any],
|
||||
t_span: tuple[float, float],
|
||||
y0: Any,
|
||||
t_eval: Any = None,
|
||||
method: str = "RK45",
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""解 ODE 初值问题(底层 `scipy.integrate.solve_ivp`)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"RK45"` 通用;`"LSODA"` 刚性系统自动切换(种群/化学反应常见);
|
||||
`"Radau"` / `"BDF"` 强刚性。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**刚性是 ODE 最常踩的坑**:非刚性方法遇到刚性问题会跑到天荒地老
|
||||
且以为是自己代码慢。判据:解里同时存在快慢两个时间尺度。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = solve_ode(lambda t, y: -y, t_span=(0, 5), y0=[1.0]) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def solve_system(equations: Any, variables: Any, **kwargs: Any) -> Any:
|
||||
"""解方程组(底层 `sympy.solve` / `scipy.optimize.root`)。
|
||||
|
||||
`equations` 是符号表达式列表时走 SymPy 解析解,是数值函数时走 root 数值解。
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def find_equilibrium(f: Callable[..., Any], x0: Any, n_starts: int = 10, random_state: int | None = None) -> Any:
|
||||
"""找平衡点。
|
||||
|
||||
**必须多初值搜索**:单个初值只能找到最近的平衡点,
|
||||
而"系统有几个平衡点、分别是什么类型"才是题目真正要问的。
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def local_stability(jacobian_matrix: Any, equilibrium: Any = None) -> Any:
|
||||
"""按 Jacobian 的特征值判定平衡点的局部稳定性。
|
||||
|
||||
判据(连续系统):实部全负 → 稳定结点/焦点;有正实部 → 不稳定;
|
||||
纯虚根 → 中心(临界情形,线性化失效,需要非线性分析)。
|
||||
`equilibrium` 用于把特征值和解的位置一起报出来。
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
@@ -1,86 +0,0 @@
|
||||
"""图与网络(v0.2)。
|
||||
|
||||
原建议把 `network/` 做成一个独立包(5 个子模块、20+ 函数),
|
||||
但里面绝大多数是 NetworkX 的一行转发,且只有 ICM D 用得上。
|
||||
降级为一个模块,只留真正有封装价值的入口。
|
||||
|
||||
依赖:`pip install "mcm-toolkit[graph]"`(networkx)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"betweenness_centrality",
|
||||
"build_graph",
|
||||
"community_detection",
|
||||
"max_flow",
|
||||
"min_spanning_tree",
|
||||
"pagerank",
|
||||
"shortest_path",
|
||||
]
|
||||
|
||||
|
||||
def build_graph(edges: Any, directed: bool = False, weighted: bool = True) -> Any:
|
||||
"""从边表建图。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
edges:
|
||||
`[(u, v), ...]` 或 `[(u, v, w), ...]`,也可以是 DataFrame(列名 `source/target/weight`)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = build_graph([("A", "B"), ("B", "C")]) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def shortest_path(G: Any, source: Any, target: Any = None, weight: str | None = None) -> Any:
|
||||
"""最短路径。`target=None` 时返回从 source 出发到所有点的路径。"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def min_spanning_tree(G: Any, weight: str = "weight") -> Any:
|
||||
"""最小生成树(铺管网、修路这类"连通成本最小"题目)。"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def betweenness_centrality(G: Any, normalized: bool = True) -> Any:
|
||||
"""介数中心性。
|
||||
|
||||
节点的"关键程度"有三种主流度量(度/接近/介数),
|
||||
论文里选哪一个要说明理由:**介数**衡量"必经之路",
|
||||
**度**衡量"连接多",**特征向量**衡量"连到重要节点"。
|
||||
原建议把四个都列了,这里只留最常用的介数,其余直接调 networkx。
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def pagerank(G: Any, alpha: float = 0.85, max_iter: int = 100) -> Any:
|
||||
"""PageRank(有向图的节点重要性)。"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def max_flow(G: Any, source: Any, sink: Any, capacity: str = "capacity") -> Any:
|
||||
"""最大流 / 最小割(网络容量、瓶颈分析)。"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def community_detection(G: Any, method: str = "louvain", random_state: int | None = None) -> Any:
|
||||
"""社区发现。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是 `{节点: 社区编号}`,`metrics={"modularity"}`。
|
||||
**必须报模块度 Q**,否则社区划分好不好无从判断。
|
||||
"""
|
||||
# TODO(v0.2)
|
||||
raise NotImplementedError
|
||||
@@ -1,234 +0,0 @@
|
||||
"""优化。
|
||||
|
||||
分层(对应原建议的 A/B/C 三级):
|
||||
|
||||
* **B 类(薄封装)**:`minimize` `linear_program` `integer_program` —— 底层 SciPy。
|
||||
* **A 类(自己实现,S 档)**:`dynamic_programming` `knapsack_dp` `simulated_annealing`。
|
||||
* **C 类(直接用库)**:NSGA-II 等,用 pymoo,本模块只提供 Pareto 前沿的**后处理**。
|
||||
|
||||
原建议里的 `maximize` 已删:它就是 `minimize` 取负,用 `sense="max"` 参数表达。
|
||||
`value_iteration` / `policy_iteration` 也已删:与 `dynamic_programming` 同族,
|
||||
第一版同时做三个属于过度设计。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"dynamic_programming",
|
||||
"integer_program",
|
||||
"knapsack_dp",
|
||||
"linear_program",
|
||||
"minimize",
|
||||
"pareto_front",
|
||||
"simulated_annealing",
|
||||
]
|
||||
|
||||
|
||||
def minimize(
|
||||
objective: Callable[..., float],
|
||||
x0: Any = None,
|
||||
bounds: Any = None,
|
||||
constraints: Any = None,
|
||||
method: str = "auto",
|
||||
sense: str = "min",
|
||||
random_state: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""连续优化(底层 `scipy.optimize.minimize`)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"auto"` 时按"有无约束 / 有无梯度"自动选:
|
||||
无约束 → BFGS;有边界 → L-BFGS-B;有约束 → SLSQP。
|
||||
sense:
|
||||
`"min"` / `"max"`。取 max 时内部对目标函数取负并还原结果,
|
||||
调用方不用自己写负号(负号写错是这类题最常见的手滑)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"x": 最优解}`,`metrics={"objective": 目标值}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
**结果必须在论文里报"是否收敛"和"初值敏感性"**:
|
||||
用一个 `x0` 跑出来的局部最优,和换 10 个随机初值跑出来的全局最优,
|
||||
结论可能完全不同。返回值里带上 `success` 与 `message` 就是为了这个。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> f = lambda v: (v[0] - 3) ** 2 + (v[1] + 1) ** 2
|
||||
>>> r = minimize(f, x0=[0, 0])
|
||||
>>> round(r.metrics["objective"], 6)
|
||||
0.0
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def linear_program(
|
||||
c: Any,
|
||||
A_ub: Any = None,
|
||||
b_ub: Any = None,
|
||||
A_eq: Any = None,
|
||||
b_eq: Any = None,
|
||||
bounds: Any = None,
|
||||
sense: str = "min",
|
||||
) -> Any:
|
||||
"""线性规划(底层 `scipy.optimize.linprog`)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
c:
|
||||
目标函数系数。`sense="max"` 时内部取负。
|
||||
bounds:
|
||||
`None` 表示默认 `x >= 0`;要无下界显式写 `(None, None)`。
|
||||
**这是最容易踩的坑**:忘记声明变量非负,解出来的方案可能物理上不可行。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = linear_program(c=[1, 2], A_ub=[[1, 1]], b_ub=[4], bounds=[(0, None)] * 2)
|
||||
>>> r.metrics["objective"]
|
||||
4.0
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def integer_program(
|
||||
c: Any,
|
||||
integrality: Any,
|
||||
A_ub: Any = None,
|
||||
b_ub: Any = None,
|
||||
A_eq: Any = None,
|
||||
b_eq: Any = None,
|
||||
bounds: Any = None,
|
||||
sense: str = "min",
|
||||
) -> Any:
|
||||
"""整数规划(底层 `scipy.optimize.milp`)。
|
||||
|
||||
`integrality`:0 = 连续,1 = 整数,2 = 0-1 变量。
|
||||
|
||||
Notes
|
||||
-----
|
||||
整数规划比 LP 慢得多。变量数到几百以上时先想想能不能用
|
||||
线性松弛 + 取整 + 邻域搜索近似,别硬等求解器。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def dynamic_programming(
|
||||
states: Any,
|
||||
actions: Any,
|
||||
transition: Callable[..., Any],
|
||||
reward: Callable[..., float],
|
||||
horizon: int,
|
||||
discount: float = 1.0,
|
||||
) -> Any:
|
||||
"""通用有限期动态规划(A 类,自己实现)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
transition:
|
||||
`f(state, action, rng) -> next_state`。
|
||||
reward:
|
||||
`f(state, action, next_state) -> float`。
|
||||
horizon:
|
||||
期数。`discount < 1` 时按 `γ^t` 折算。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是"最优值函数表 + 最优策略表",`extras["policy"]` 可直接查。
|
||||
|
||||
Notes
|
||||
-----
|
||||
复杂度 `O(horizon × |S| × |A|)`。**状态空间是唯一瓶颈**——
|
||||
如果 `|S| × holiday` 超过内存,要么精简状态离散化粒度,要么改成近似 DP。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> # 背包问题用 knapsack_dp 更直接;本函数用于"逐期决策"类题
|
||||
>>> r = dynamic_programming(states, actions, trans, rew, horizon=10) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def knapsack_dp(weights: Any, values: Any, capacity: float) -> Any:
|
||||
"""0-1 背包(动态规划模板,A 类,自己实现)。
|
||||
|
||||
复杂度 `O(n × capacity)`。`capacity` 在题面里是小数时先做单位缩放取整,
|
||||
否则 DP 表会大到装不下。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"selected": 选中的下标}`,`metrics={"total_value", "total_weight"}`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = knapsack_dp(weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7)
|
||||
>>> r.metrics["total_value"]
|
||||
9.0
|
||||
"""
|
||||
# TODO(L1) ★ S 档
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def simulated_annealing(
|
||||
objective: Callable[..., float],
|
||||
x0: Any,
|
||||
bounds: Any = None,
|
||||
temp0: float = 1.0,
|
||||
cooling: float = 0.95,
|
||||
n_iter: int = 1000,
|
||||
sense: str = "min",
|
||||
random_state: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""模拟退火(A 类,自己实现)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cooling:
|
||||
降温系数,每步 `T *= cooling`。`0.95` 配 `n_iter=1000` 是保守默认值。
|
||||
temp0:
|
||||
初始温度。**经验做法:取"随机扰动的典型劣化幅度"同量级**,
|
||||
太大等于随机游走,太小等于爬山。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`extras["history"]` 是每步的最优值轨迹,可直接喂给 `viz.plot_convergence`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 必须记录收敛曲线并画出来——论文里"算法收敛性分析"那一节就靠它。
|
||||
* 结果对随机种子敏感,**至少跑 10 个种子**,报最好值 + 分布,
|
||||
只报一个种子的结果是不可信的。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = simulated_annealing(lambda v: sum(x * x for x in v), x0=[3.0, 4.0], random_state=0) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def pareto_front(points: Any, sense: str = "min") -> Any:
|
||||
"""从解集里筛出 Pareto 非支配解。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"mask": 布尔掩码, "rank": 每个解的支配层}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
多目标问题的**后处理**入口。真正的 NSGA-II 求解器用 pymoo
|
||||
(`pip install "mcm-toolkit[moo]"`),本函数只负责"给定一组解,挑出前沿"。
|
||||
它也是画 Pareto 前沿图的必要前置步骤。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,214 +0,0 @@
|
||||
"""敏感性分析与鲁棒性检验。
|
||||
|
||||
★ 建议做成招牌模块。原建议这一节方向完全正确,只是函数名偏多,
|
||||
已合并同义项、删掉 `grid_sweep`(`parameter_sweep` 的特例)。
|
||||
|
||||
论文里的"敏感性分析"一节基本就是这四个函数的组合:
|
||||
局部扰动 → OAT 单因素扫描 → 多因素参数扫描 → 全局 Sobol/Morris。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"local_sensitivity",
|
||||
"one_at_a_time",
|
||||
"parameter_sweep",
|
||||
"robustness_test",
|
||||
]
|
||||
|
||||
|
||||
def local_sensitivity(
|
||||
model: Callable[..., Any],
|
||||
params: Any,
|
||||
step: float = 1e-4,
|
||||
relative: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""局部敏感性:各参数的偏导 / 弹性。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
relative:
|
||||
`True` 时算弹性 `(∂y/∂p)·(p/y)`——**无量纲,可以跨参数比较**,
|
||||
这是局部敏感性的正确做法;`False` 时是原始偏导(有量纲,只有同量纲参数之间可比)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是参数 → 弹性 的表,按绝对值降序。
|
||||
|
||||
Notes
|
||||
-----
|
||||
有限差分的步长是精度与截断误差的权衡:太大截断误差大,
|
||||
太小被浮点误差淹没。`1e-4` 是稳健默认值;参数本身量级差很多时用相对步长。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> s = local_sensitivity(lambda p: p["a"] ** 2 * p["b"], {"a": 2.0, "b": 3.0}) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1) ★ S 档
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def one_at_a_time(
|
||||
model: Callable[..., Any],
|
||||
params: Any,
|
||||
parameter: str | None = None,
|
||||
values: Any = None,
|
||||
n_points: int = 20,
|
||||
span: float = 0.5,
|
||||
) -> Any:
|
||||
"""OAT(一次只动一个参数)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parameter:
|
||||
`None` 时对所有参数依次扫描。
|
||||
span:
|
||||
相对基准值的扫描范围,`0.5` 表示 ±50%。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是长表 `(parameter, value, output)`,配 `viz.plot` 直接出蛛网图/敏感性曲线。
|
||||
|
||||
Notes
|
||||
-----
|
||||
OAT 的**已知缺陷**:它看不见参数之间的交互作用。
|
||||
如果两个参数是相乘关系(很常见),OAT 会严重低估整体敏感性——
|
||||
论文里必须说明这一点,并补一个全局敏感性作为交叉验证。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def parameter_sweep(
|
||||
model: Callable[..., Any],
|
||||
ranges: dict[str, Any],
|
||||
n_points: int = 20,
|
||||
method: str = "grid",
|
||||
n_samples: int = 1000,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""多参数扫描(合并了原建议的 `grid_sweep` 与 `random_parameter_sweep`)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ranges:
|
||||
`{"alpha": (0.1, 0.9), "beta": (0, 5)}`。
|
||||
method:
|
||||
`"grid"`:笛卡尔积,点数 = `n_points^d`,**维度一高就爆**(d=5, n=20 是 320 万次);
|
||||
`"lhs"`:拉丁超立方,维度高时用这个;
|
||||
`"random"`:纯随机。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是 `(n_points^d, d + 1)` 的结果表,
|
||||
`params["best"]` 是让目标最优的参数组合。
|
||||
|
||||
Notes
|
||||
-----
|
||||
`d >= 4` 时默认改走 `"lhs"`,否则 `n_points` 会被指数放大到跑不完。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = parameter_sweep(lambda a, b: -(a - 0.3) ** 2 - (b - 2) ** 2,
|
||||
... {"a": (0, 1), "b": (0, 4)}, n_points=11) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def robustness_test(
|
||||
model: Callable[..., Any],
|
||||
scenarios: Any = None,
|
||||
params: Any = None,
|
||||
perturbation: float = 0.1,
|
||||
n_samples: int = 1000,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""鲁棒性检验(合并了原建议的 `perturbation_test`)。
|
||||
|
||||
在参数上施加 ±`perturbation` 的随机扰动,看结论是否翻盘。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`metrics` 含 `"rank_stability"`(原结论在前 X% 的比例)、
|
||||
`"conclusion_flip_rate"`(结论反转频率)。**这两个数字才是审稿人想看的。**
|
||||
|
||||
Notes
|
||||
-----
|
||||
鲁棒性检验和敏感性分析不是一回事:
|
||||
敏感性问"参数变了输出变多少",鲁棒性问"参数变了**结论**还成立吗"。
|
||||
论文里两节都要有,别混着写。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = robustness_test(model, params={"alpha": 0.5}, random_state=0) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def sobol_analyze(
|
||||
model: Callable[..., Any],
|
||||
bounds: Any,
|
||||
n_samples: int = 1024,
|
||||
calc_second_order: bool = False,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""Sobol 全局敏感性(底层 SALib,自己不要重写)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds:
|
||||
`{"alpha": (0, 1), "beta": (0, 5)}` 形式的参数上下界。
|
||||
n_samples:
|
||||
SALib 的基样本数 N;**实际模型调用次数是 `N·(2d + 2)`**
|
||||
(二阶时更多),d=5、N=1024 就是 12288 次调用。先估一下模型单次耗时。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 含 `S1`(一阶)、`ST`(总效应)、`S2`(二阶,可选)三列。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* `ST - S1` 的差距就是**交互作用**的大小——这正是 OAT 看不见的东西。
|
||||
两者差得多,说明不能只看单因素。
|
||||
* 需要 `pip install "mcm-toolkit[sensitivity]"`;缺 SALib 时本函数给出
|
||||
可读的安装提示,而不是裸抛 `ModuleNotFoundError`。
|
||||
* 费用高:**先做 OAT 缩小参数范围,再对少数关键参数做 Sobol**,
|
||||
不要一上来就对全部参数做全局敏感性。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = sobol_analyze(model, bounds={"a": (0, 1), "b": (0, 5)}) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def morris_analyze(
|
||||
model: Callable[..., Any],
|
||||
bounds: Any,
|
||||
n_trajectories: int = 10,
|
||||
n_levels: int = 4,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""Morris 筛选法(底层 SALib)。
|
||||
|
||||
调用次数只与轨迹数成正比(`(d + 1) · n_trajectories`),比 Sobol 便宜一个量级,
|
||||
适合**参数多但只需要找出"哪几个重要"**的筛选阶段。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 含 `mu`、`mu_star`(推荐看的)、`sigma` 三列。
|
||||
`sigma` 大表示该参数的效应是非线性的或与其他参数有交互。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = morris_analyze(model, bounds={"a": (0, 1), "b": (0, 5)}) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,218 +0,0 @@
|
||||
"""随机模拟:Monte Carlo / 马尔可夫 / 随机游走 / 排队。
|
||||
|
||||
★ 本模块的 `monte_carlo` 与 `monte_carlo_integral` 是 S 档,建议自己实现。
|
||||
原建议里的 `brownian_motion` `geometric_brownian_motion` 已删(niche),
|
||||
`simulate_queue` 也已删(等于自己写一个离散事件仿真引擎,成本远高于收益)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"markov_simulate",
|
||||
"markov_steady_state",
|
||||
"mm1_metrics",
|
||||
"monte_carlo",
|
||||
"monte_carlo_integral",
|
||||
"monte_carlo_probability",
|
||||
"random_walk",
|
||||
]
|
||||
|
||||
|
||||
def monte_carlo(
|
||||
sampler: Callable[..., Any],
|
||||
simulator: Callable[..., Any],
|
||||
n: int = 10000,
|
||||
random_state: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""通用蒙特卡洛:采样 → 仿真 → 汇总。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sampler:
|
||||
`f(rng, **kwargs) -> 一组参数`,例如从先验分布里抽参数。
|
||||
simulator:
|
||||
`f(params) -> float`,例如"给定参数下的年收益"。
|
||||
n:
|
||||
模拟次数。**必须向量化**:`simulator` 内部不要写 Python 循环,
|
||||
否则 10^4 次能跑几分钟,10^6 次直接放弃。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是 `n` 个结果组成的数组,
|
||||
`metrics={"mean", "std", "median", "p05", "p95", "ci_low", "ci_high"}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
比赛里 Monte Carlo 的收敛速度是 `O(1/√n)`——
|
||||
想少一半误差要 4 倍样本。所以论文里要给"区间随 n 收敛"的图,
|
||||
证明 `n` 已经够大(这也是 `viz.plot_convergence` 的典型用法)。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = monte_carlo(lambda rng, **k: rng.normal(0, 1), np.mean, n=1000, random_state=0)
|
||||
"""
|
||||
# TODO(L1) ★ S 档
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def monte_carlo_probability(
|
||||
event: Callable[..., bool],
|
||||
sampler: Callable[..., Any],
|
||||
n: int = 10000,
|
||||
confidence: float = 0.95,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""估计事件发生概率,并给出区间。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`metrics={"probability", "ci_low", "ci_high", "std_error"}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
概率估计的相对误差 ≈ `1/√(n·p)`。**小概率事件(p < 1e-3)用朴素 MC 是不可行的**,
|
||||
需要重要性采样或方差缩减——遇到这类题要提前意识到,别硬跑。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = monte_carlo_probability(lambda v: v > 0, lambda rng, **k: rng.normal(0, 1), n=2000, random_state=0)
|
||||
>>> abs(r.metrics["probability"] - 0.5) < 0.1
|
||||
True
|
||||
"""
|
||||
# TODO(L1) ★ S 档
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def monte_carlo_integral(
|
||||
func: Callable[..., Any],
|
||||
bounds: Any,
|
||||
n: int = 10000,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""用蒙特卡洛求多重积分。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bounds:
|
||||
`[(lo, hi), ...]`,维度即积分重数。
|
||||
|
||||
Notes
|
||||
-----
|
||||
高维积分是 MC 相对网格法的**唯一绝对优势**:网格法误差 `O(h^{-d})` 随维度爆炸,
|
||||
MC 始终是 `O(1/√n)`。三维以上优先考虑 MC。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200000, random_state=0)
|
||||
>>> abs(r.data - 1 / 3) < 0.01
|
||||
True
|
||||
"""
|
||||
# TODO(L1) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def random_walk(
|
||||
n_steps: int,
|
||||
n_paths: int = 1,
|
||||
step_sampler: Callable[..., Any] | None = None,
|
||||
start: float = 0.0,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""随机游走轨迹。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
step_sampler:
|
||||
`f(rng, size) -> 增量`,默认为 ±1 等概率(对称随机游走)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 形状 `(n_paths, n_steps + 1)`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = random_walk(n_steps=100, n_paths=5, random_state=0)
|
||||
>>> r.data.shape
|
||||
(5, 101)
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def markov_simulate(
|
||||
transition: Any,
|
||||
initial: Any,
|
||||
steps: int = 100,
|
||||
n_paths: int = 1,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""马尔可夫链模拟,返回状态序列。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def markov_steady_state(transition: Any, max_iter: int = 10000, tol: float = 1e-10) -> Any:
|
||||
"""平稳分布。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
transition:
|
||||
行随机矩阵(每行和为 1)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是平稳分布向量,`metrics={"converged", "iterations"}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
用迭代法而不是特征分解:迭代法能顺便告诉你"收敛快不快",
|
||||
而收敛速度本身就是论文里值得写一句的性质。
|
||||
**不可约 + 非周期**才有唯一平稳分布,周期链会不收敛,要检查。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> P = np.array([[0.9, 0.1], [0.2, 0.8]])
|
||||
>>> r = markov_steady_state(P)
|
||||
>>> bool(np.allclose(r.data, [2 / 3, 1 / 3], atol=1e-6))
|
||||
True
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def mm1_metrics(arrival_rate: float, service_rate: float) -> Any:
|
||||
"""M/M/1 排队系统的闭式指标。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arrival_rate, service_rate:
|
||||
到达率 λ 与服务率 μ。`λ >= μ` 时系统不稳定,队列长度无界——
|
||||
这种情况下函数返回 `success=False` 并说明原因,而不是返回 `inf`。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`metrics={"rho", "L", "Lq", "W", "Wq"}`
|
||||
(系统利用率、系统内平均顾客数、队列长度、平均逗留时间、平均等待时间)。
|
||||
|
||||
Notes
|
||||
-----
|
||||
M/M/1 是唯一值得手写的排队模型:公式是闭式的,10 行搞定。
|
||||
多服务员(M/M/c)和一般服务时间分布(M/G/1)在美赛出现频率极低,
|
||||
真遇到时现场用仿真做,不做通用封装。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = mm1_metrics(arrival_rate=2, service_rate=3)
|
||||
>>> round(r.metrics["rho"], 4)
|
||||
0.6667
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,32 +0,0 @@
|
||||
"""stats —— 描述统计、统计推断、bootstrap。
|
||||
|
||||
S 级模块。`bootstrap_ci` 建议自己实现一遍(S 档,配完整函数卡片):
|
||||
它是赛题里"没有解析解时怎么给区间"的通用答案。
|
||||
"""
|
||||
|
||||
from .bootstrap import bootstrap_ci, bootstrap_statistic
|
||||
from .descriptive import coefficient_of_variation, describe, zscore
|
||||
from .inference import (
|
||||
anova,
|
||||
chi_square_test,
|
||||
confidence_interval,
|
||||
correlation_matrix,
|
||||
fit_distribution,
|
||||
goodness_of_fit,
|
||||
t_test,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"describe",
|
||||
"zscore",
|
||||
"coefficient_of_variation",
|
||||
"correlation_matrix",
|
||||
"confidence_interval",
|
||||
"t_test",
|
||||
"anova",
|
||||
"chi_square_test",
|
||||
"fit_distribution",
|
||||
"goodness_of_fit",
|
||||
"bootstrap_statistic",
|
||||
"bootstrap_ci",
|
||||
]
|
||||
@@ -1,71 +0,0 @@
|
||||
"""Bootstrap 重采样。
|
||||
|
||||
★ S 档模块:建议自己完整实现一遍。
|
||||
它是"没有解析解时怎么给区间"的通用答案,赛题里出现频率极高,
|
||||
而且实现只有几十行——投入产出比最高的一个算法。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["bootstrap_ci", "bootstrap_statistic"]
|
||||
|
||||
|
||||
def bootstrap_statistic(
|
||||
x: Any,
|
||||
statistic: Callable[..., float],
|
||||
n_resamples: int = 10000,
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""返回 `n_resamples` 个重采样统计量组成的数组。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> vals = bootstrap_statistic([1, 2, 3, 4, 5], np.mean, n_resamples=200, random_state=0)
|
||||
"""
|
||||
# TODO(L1) ★ S 档
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def bootstrap_ci(
|
||||
x: Any,
|
||||
statistic: Callable[..., float] | None = None,
|
||||
confidence: float = 0.95,
|
||||
n_resamples: int = 10000,
|
||||
method: str = "percentile",
|
||||
random_state: int | None = None,
|
||||
) -> Any:
|
||||
"""用重采样给出任意统计量的置信区间。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
statistic:
|
||||
任意把样本映射成标量的函数,默认 `np.mean`。
|
||||
换成 `np.median` / `np.std` / 自定义的 `lambda s: s.max() - s.min()` 都能用,
|
||||
这是 bootstrap 相对于 `mean ± z·s/√n` 的核心优势。
|
||||
method:
|
||||
`"percentile"`:直接取经验分布的 α/2 与 1-α/2 分位(最常用,默认);
|
||||
`"bca"`:偏差校正 + 加速,区间更准但慢,样本有偏时用。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`params={"estimate", "low", "high", "confidence", "n_resamples"}`。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 重采样时必须**按块处理**,不要一次生成 `(10000, n)` 的整矩阵(n 大时会爆内存)。
|
||||
* 样本量 n < 10 时 bootstrap 不可靠,先补数据。
|
||||
* 比赛写论文时要注明:n_resamples、seed、用的是 percentile 还是 BCa。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> r = bootstrap_ci([1, 2, 3, 4, 5], np.median, random_state=0)
|
||||
>>> r.params["low"] < 3 < r.params["high"]
|
||||
True
|
||||
"""
|
||||
# TODO(L0) ★ S 档:配完整函数卡片
|
||||
raise NotImplementedError
|
||||
@@ -1,40 +0,0 @@
|
||||
"""描述统计。
|
||||
|
||||
每个函数都只是"一句话数学含义 + 一句话什么时候用"(A 档),
|
||||
不单独写文档卡片。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["coefficient_of_variation", "describe", "zscore"]
|
||||
|
||||
|
||||
def describe(x: Any, weights: Any | None = None) -> Any:
|
||||
"""一张表看完集中趋势、离散程度、偏度峰度、缺失情况。
|
||||
|
||||
`weights` 非空时给出加权均值与加权标准差(权重是"频数"或"重要性"时用)。
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame,行是指标,列是被描述的变量。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> describe(df).loc["mean"] # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def zscore(x: Any, ddof: int = 0) -> Any:
|
||||
"""标准化到 z 分数,顺带回答了"这个值离均值几个标准差"。"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def coefficient_of_variation(x: Any) -> float:
|
||||
"""变异系数 `std / mean` —— 跨量纲比较"谁更波动"时的唯一正确做法。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,114 +0,0 @@
|
||||
"""统计推断:区间估计、假设检验、分布拟合。
|
||||
|
||||
原建议里的 `mean_test`/`proportion_test`/`distribution_pdf`/`distribution_cdf`/
|
||||
`distribution_ppf`/`sample_distribution` 都是 scipy 的一行转发,已删。
|
||||
需要 pdf/cdf/ppf 时,用 `fit_distribution(...).dist.pdf(...)` 即可。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"anova",
|
||||
"chi_square_test",
|
||||
"confidence_interval",
|
||||
"correlation_matrix",
|
||||
"fit_distribution",
|
||||
"goodness_of_fit",
|
||||
"t_test",
|
||||
]
|
||||
|
||||
|
||||
def correlation_matrix(data: Any, method: str = "pearson", min_periods: int = 1) -> Any:
|
||||
"""相关矩阵 + 显著性星标。
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataFrame,值 = 相关系数;`min_periods` 控制缺失值容差。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> correlation_matrix(df).round(3) # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def confidence_interval(
|
||||
x: Any,
|
||||
confidence: float = 0.95,
|
||||
method: str = "normal",
|
||||
) -> tuple[float, float]:
|
||||
"""均值或比例的置信区间。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method:
|
||||
`"normal"`:`mean ± z·s/√n`(n 大时用);
|
||||
`"t"`:小样本用 t 分布(更保守,n < 30 建议用这个);
|
||||
`"proportion"`:比例的标准误 `√(p(1-p)/n)`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> lo, hi = confidence_interval([1, 2, 3, 4, 5], confidence=0.95)
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def t_test(a: Any, b: Any | None = None, value: float = 0.0, equal_var: bool = False, paired: bool = False, alpha: float = 0.05) -> Any:
|
||||
"""t 检验。`b=None` 时是单样本检验。
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:`{"statistic", "pvalue", "df", "significant", "conclusion"}`。
|
||||
`conclusion` 是一句能直接抄进论文的中文结论。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def anova(*groups: Any, alpha: float = 0.05) -> Any:
|
||||
"""单因素方差分析:多组均值是否相同。
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict:`{"F", "pvalue", "conclusion"}`。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def chi_square_test(observed: Any, expected: Any | None = None, alpha: float = 0.05) -> Any:
|
||||
"""卡方检验:观察频数与期望频数是否一致(分类变量的独立性 / 拟合优度)。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def fit_distribution(x: Any, candidates: tuple[str, ...] = ("norm", "lognorm", "gamma", "expon", "uniform")) -> Any:
|
||||
"""对候选分布逐个拟合,按 AIC(或 KS 统计量)排序。
|
||||
|
||||
Returns
|
||||
-------
|
||||
`Result`:`data` 是排好序的候选表;`params["best"]` 是最优分布名,
|
||||
`extras["dist"]` 是可直接调 `.pdf / .cdf / .ppf / `.rvs` 的 scipy 分布对象。
|
||||
|
||||
Notes
|
||||
-----
|
||||
这是"反正态题"的标准做法:先看图 → 候选分布拟合 → 比较 AIC → 做 KS 检验。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> r = fit_distribution(data) # doctest: +SKIP
|
||||
>>> r.params["best"] # doctest: +SKIP
|
||||
'lognorm'
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def goodness_of_fit(x: Any, dist: str = "norm", method: str = "ks") -> Any:
|
||||
"""拟合优度检验:`"ks"`(连续)/ `"chi2"`(分箱)。"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,21 +0,0 @@
|
||||
"""viz —— 出图与出表(论文产出的最后一公里)。
|
||||
|
||||
原建议把 `visualization/` 拆成 5 个文件、13 个 `plot_*` 函数。
|
||||
但每个 `plot_*` 本质上都是"在 ax 上加两行",拆开后
|
||||
**改一次全局样式要改 13 处**,而且 13 个名字谁也记不住。
|
||||
合并为 `plot(kind=...)` + 3 个有真实逻辑的专用图。
|
||||
"""
|
||||
|
||||
from .export import save_figure, save_table
|
||||
from .plot import plot, plot_convergence, plot_correlation_matrix, plot_residuals
|
||||
from .style import mcm_style
|
||||
|
||||
__all__ = [
|
||||
"mcm_style",
|
||||
"plot",
|
||||
"plot_correlation_matrix",
|
||||
"plot_residuals",
|
||||
"plot_convergence",
|
||||
"save_figure",
|
||||
"save_table",
|
||||
]
|
||||
@@ -1,74 +0,0 @@
|
||||
"""图表导出。
|
||||
|
||||
工具箱的最终消费者是**论文**,所以这一层不是"附赠功能",
|
||||
而是原建议里最被低估的部分(它只给了一句 `save_figure`)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["save_figure", "save_table"]
|
||||
|
||||
|
||||
def save_figure(
|
||||
fig: Any,
|
||||
path: str | Path,
|
||||
dpi: int = 300,
|
||||
formats: tuple[str, ...] = ("png", "pdf"),
|
||||
bbox_inches: str = "tight",
|
||||
transparent: bool = False,
|
||||
) -> list[Path]:
|
||||
"""保存图,默认同时出 PNG(预览用)和 PDF(LaTeX 插图用)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
formats:
|
||||
美赛论文如果用 LaTeX 排版,**矢量图(PDF/EPS)比 PNG 清晰得多**,
|
||||
所以默认两种都出。
|
||||
|
||||
Returns
|
||||
-------
|
||||
实际写出的文件路径列表。
|
||||
|
||||
Notes
|
||||
-----
|
||||
建议配合项目的 figure 目录约定:`figures/fig{序号}_{短名}.pdf`,
|
||||
图号与正文引用一一对应,最后核对时不容易错位。
|
||||
"""
|
||||
# TODO(L0)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def save_table(
|
||||
df: Any,
|
||||
path: str | Path,
|
||||
fmt: tuple[str, ...] = ("csv", "latex", "markdown"),
|
||||
caption: str | None = None,
|
||||
label: str | None = None,
|
||||
float_format: str = "%.4f",
|
||||
index: bool = True,
|
||||
) -> list[Path]:
|
||||
r"""导出结果表,默认同时出 CSV + LaTeX + Markdown。
|
||||
|
||||
这是**节省时间最多的一个函数**:`compare_models`、`parameter_sweep`、
|
||||
`topsis` 的结果都能直接 `save_table(r.data, "tab3_model_compare")`,
|
||||
然后把 LaTeX 片段粘进论文,不用手工敲表格。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
caption / label:
|
||||
写进 LaTeX 的 `\caption{}` 与 `\label{}`,
|
||||
表号与正文 `\ref{}` 的对应关系就不会断。
|
||||
|
||||
Returns
|
||||
-------
|
||||
实际写出的文件路径列表。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> paths = save_table(df, "tables/tab2", caption="模型对比") # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,79 +0,0 @@
|
||||
"""绘图。
|
||||
|
||||
`plot` 是一个通用入口,按 `kind` 分派;三个专用图有真实的额外逻辑
|
||||
(显著性星标、残差诊断、收敛曲线),所以单独保留。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["plot", "plot_convergence", "plot_correlation_matrix", "plot_residuals"]
|
||||
|
||||
|
||||
def plot(
|
||||
x: Any = None,
|
||||
y: Any = None,
|
||||
kind: str = "line",
|
||||
ax: Any = None,
|
||||
labels: Any = None,
|
||||
title: str | None = None,
|
||||
xlabel: str | None = None,
|
||||
ylabel: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""通用绘图入口。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kind:
|
||||
`"line"` / `"scatter"` / `"hist"` / `"box"` / `"bar"` / `"heatmap"` / `"pie"`。
|
||||
ax:
|
||||
传入已有 axes 就在上面画(多子图场景);`None` 时新建 figure。
|
||||
|
||||
Returns
|
||||
-------
|
||||
符合"创建—绘制—保存"三段的 `(fig, ax)`,交给 :func:`save_figure`。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> fig, ax = plot([1, 2, 3], [1, 4, 9], kind="line") # doctest: +SKIP
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def plot_correlation_matrix(data: Any, method: str = "pearson", annot: bool = True, significance: bool = True) -> Any:
|
||||
"""相关矩阵热图。
|
||||
|
||||
`significance=True` 时在不显著的格子(`p > 0.05`)上打叉。
|
||||
这比只画颜色有意义得多——**不显著的相关性画出来是误导**。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def plot_residuals(y_true: Any, y_pred: Any, n_features: int | None = None) -> Any:
|
||||
"""残差诊断四联图:残差-拟合值、Q-Q 图、直方图、残差-序。
|
||||
|
||||
拟合完模型**第一件事就是看这个**:
|
||||
* 残差-拟合值有漏斗形 → 异方差,考虑加权最小二乘或对数变换;
|
||||
* Q-Q 图偏离直线 → 正态性假设不成立,区间估计要改用 bootstrap;
|
||||
* 残差有周期性 → 漏了时间/结构项。
|
||||
|
||||
这三个诊断结论是论文"模型检验"一节的素材,比只报一个 R² 有说服力。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def plot_convergence(history: Any, ax: Any = None, labels: Any = None, log: bool = False, reference: float | None = None) -> Any:
|
||||
"""收敛曲线。
|
||||
|
||||
`history` 可以是迭代轨迹数组、也接受 `Result.extras["history"]`
|
||||
(`simulated_annealing` 与 `monte_carlo` 都会填这个字段)。
|
||||
`reference` 传入解析解时会把真值画成虚线,
|
||||
用于"MC 积分误差随 n 收敛"这类图——这是证明 `n` 选得合理的标准做法。
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
@@ -1,40 +0,0 @@
|
||||
"""统一图样式。
|
||||
|
||||
美赛送稿要求:**双栏排版、字号不能太小、图必须能在黑白打印下区分**。
|
||||
把这些要求固化成一次调用,比每次手调 rcParams 靠谱。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["mcm_style"]
|
||||
|
||||
|
||||
def mcm_style(palette: str = "colorblind", dpi: int = 300, font_size: float = 9.0, **overrides: Any) -> Any:
|
||||
"""设置全局绘图样式,返回 matplotlib rcParams(便于 `with` 临时切换)。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
palette:
|
||||
`"colorblind"`(默认,色盲友好且黑白打印可辨)/ `"muted"` / `"deep"`。
|
||||
dpi:
|
||||
默认 300 —— 美赛送稿的 DPI 底线,低于这个值的图放大后会糊。
|
||||
font_size:
|
||||
默认 9pt。**双栏排版下字号小于 8pt 就会被评审认为是"塞图"**。
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 中文标签需要设置中文字体(`SimHei` / `Microsoft YaHei`),
|
||||
否则 matplotlib 会画一堆方块;本函数会尝试自动选择可用中文字体。
|
||||
* 论文用图建议**只用线型 + 标记区分**,不要只靠颜色。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import matplotlib
|
||||
>>> mcm_style()
|
||||
>>> matplotlib.rcParams["figure.dpi"]
|
||||
300.0
|
||||
"""
|
||||
# TODO(L1)
|
||||
raise NotImplementedError
|
||||
Whitespace-only changes.
@@ -1,32 +0,0 @@
|
||||
"""pytest 公共夹具。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rng():
|
||||
"""固定种子的随机数生成器——所有含随机的测试都必须用它,保证可复现。"""
|
||||
return np.random.default_rng(20260101)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linear_data(rng):
|
||||
"""带噪声的线性数据,用于回归类测试(真值 y = 2x + 1)。"""
|
||||
x = rng.uniform(-5, 5, size=200)
|
||||
y = 2.0 * x + 1.0 + rng.normal(0, 0.1, size=200)
|
||||
return x, y
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def decision_matrix():
|
||||
"""3 个方案 × 4 个指标的决策矩阵,用于评价类测试。"""
|
||||
return np.array(
|
||||
[
|
||||
[9.0, 0.3, 120.0, 8.5],
|
||||
[7.5, 0.5, 90.0, 9.0],
|
||||
[8.8, 0.2, 150.0, 7.0],
|
||||
]
|
||||
)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""公开 API 契约测试。
|
||||
|
||||
这组测试**不测算法本身**,只守两件事:
|
||||
1. `__all__` 里承诺的名字,必须真的存在且可调用;
|
||||
2. 子包的导出必须与顶层一致。
|
||||
|
||||
它现在就能通过,并且在后续实现过程中会立刻抓到"改了导出忘了改 `__all__`"这类问题。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import mcm_toolkit as mcm
|
||||
|
||||
SUBPACKAGES = ["core", "data", "stats", "models", "solve", "decision", "viz"]
|
||||
|
||||
|
||||
def test_version_is_string():
|
||||
assert isinstance(mcm.__version__, str)
|
||||
assert mcm.__version__.count(".") == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", SUBPACKAGES)
|
||||
def test_subpackage_importable(name):
|
||||
mod = importlib.import_module(f"mcm_toolkit.{name}")
|
||||
assert hasattr(mod, "__all__"), f"mcm_toolkit.{name} 缺 __all__"
|
||||
assert hasattr(mcm, name), f"顶层未导出子包 {name}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", SUBPACKAGES)
|
||||
def test_subpackage_all_resolves(name):
|
||||
mod = importlib.import_module(f"mcm_toolkit.{name}")
|
||||
missing = [n for n in mod.__all__ if not hasattr(mod, n)]
|
||||
assert not missing, f"mcm_toolkit.{name}.__all__ 里有不存在的名字:{missing}"
|
||||
|
||||
|
||||
def test_toplevel_all_resolves():
|
||||
missing = [n for n in mcm.__all__ if not hasattr(mcm, n)]
|
||||
assert not missing, f"顶层 __all__ 里有不存在的名字:{missing}"
|
||||
|
||||
|
||||
def test_toplevel_shortcuts_are_callable():
|
||||
for name in ["monte_carlo", "bootstrap_ci", "parameter_sweep", "topsis", "compare_models"]:
|
||||
assert callable(getattr(mcm, name)), f"{name} 应当是可调用的快捷函数"
|
||||
|
||||
|
||||
def test_no_star_import_pollution():
|
||||
"""顶层不应把子包内部的函数全部摊平——只允许 __all__ 里那批。"""
|
||||
allowed = set(mcm.__all__) | {"annotations"} # annotations 来自 __future__ 导入
|
||||
leaked = [n for n in dir(mcm) if not n.startswith("_") and n not in allowed]
|
||||
assert not leaked, f"顶层出现了 __all__ 之外的公开名字:{leaked}"
|
||||
|
||||
|
||||
def test_result_dataclass_defaults():
|
||||
r = mcm.Result("demo")
|
||||
assert r.success is True
|
||||
assert r.params is None
|
||||
assert r.metrics is None
|
||||
assert r.message == ""
|
||||
|
||||
|
||||
def test_random_helpers_exist():
|
||||
assert callable(mcm.set_seed)
|
||||
assert callable(mcm.get_rng)
|
||||
@@ -1,69 +0,0 @@
|
||||
"""core 包测试。
|
||||
|
||||
TODO(L0):实现后去掉 skip 标记。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0")
|
||||
class TestRandom:
|
||||
def test_get_rng_is_reproducible(self):
|
||||
from mcm_toolkit.core import get_rng
|
||||
|
||||
assert get_rng(42).random() == get_rng(42).random()
|
||||
|
||||
def test_get_rng_passes_through_generator(self):
|
||||
from mcm_toolkit.core import get_rng
|
||||
|
||||
g = np.random.default_rng(0)
|
||||
assert get_rng(g) is g
|
||||
|
||||
def test_set_seed_makes_global_reproducible(self):
|
||||
from mcm_toolkit.core import set_seed
|
||||
|
||||
set_seed(7)
|
||||
a = np.random.default_rng(7).random()
|
||||
set_seed(7)
|
||||
b = np.random.default_rng(7).random()
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0")
|
||||
class TestValidate:
|
||||
def test_check_same_length_raises_with_detail(self):
|
||||
from mcm_toolkit.core import check_same_length
|
||||
|
||||
with pytest.raises(ValueError, match=r"2.*3|长度"):
|
||||
check_same_length([1, 2], [1, 2, 3])
|
||||
|
||||
def test_check_array_rejects_nan(self):
|
||||
from mcm_toolkit.core import check_array
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
check_array([1.0, np.nan, 2.0], finite=True)
|
||||
|
||||
def test_check_probability_bounds(self):
|
||||
from mcm_toolkit.core import check_probability
|
||||
|
||||
assert check_probability(0.5) == 0.5
|
||||
with pytest.raises(ValueError):
|
||||
check_probability(1.5)
|
||||
|
||||
def test_check_square_matrix(self):
|
||||
from mcm_toolkit.core import check_square_matrix
|
||||
|
||||
check_square_matrix([[1, 2], [3, 4]])
|
||||
with pytest.raises(ValueError):
|
||||
check_square_matrix([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0")
|
||||
def test_result_repr_is_readable():
|
||||
from mcm_toolkit.core import Result
|
||||
|
||||
text = repr(Result("demo", params={"a": 1}, metrics={"r2": 0.9}))
|
||||
assert "demo" in text and "r2" in text
|
||||
@@ -1,65 +0,0 @@
|
||||
"""data 包测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1")
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
"""国内赛题数据常见 GBK,编码探测必须能兜住。"""
|
||||
from mcm_toolkit.data import load_table
|
||||
|
||||
p = tmp_path / "gbk.csv"
|
||||
p.write_bytes("城市,人口\n北京,2189\n".encode("gbk"))
|
||||
df = load_table(p)
|
||||
assert list(df.columns) == ["城市", "人口"]
|
||||
@@ -1,79 +0,0 @@
|
||||
"""decision 包测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L1")
|
||||
|
||||
|
||||
def test_topsis_ranks_dominated_alternative_last():
|
||||
"""一个在所有指标上都最差的方案,TOPSIS 必须排最后。"""
|
||||
from mcm_toolkit.decision import topsis
|
||||
|
||||
X = np.array(
|
||||
[
|
||||
[9.0, 8.0, 7.0], # 全面最优
|
||||
[5.0, 5.0, 5.0], # 中等
|
||||
[1.0, 1.0, 1.0], # 全面最差
|
||||
]
|
||||
)
|
||||
r = topsis(X, benefit_columns=[0, 1, 2])
|
||||
ranks = dict(zip(range(3), r.data["rank"], strict=False))
|
||||
assert ranks[0] == 1
|
||||
assert ranks[2] == 3
|
||||
|
||||
|
||||
def test_topsis_direction_flips_result():
|
||||
"""把某列从效益型改成成本型,排名必须变——方向不能静默搞错。"""
|
||||
from mcm_toolkit.decision import topsis
|
||||
|
||||
X = np.array([[9.0, 1.0], [1.0, 9.0]])
|
||||
a = topsis(X, benefit_columns=[0, 1])
|
||||
b = topsis(X, benefit_columns=[0])
|
||||
assert list(a.data["rank"]) != list(b.data["rank"])
|
||||
|
||||
|
||||
def test_entropy_weights_sum_to_one_and_prefers_dispersed_column(rng):
|
||||
from mcm_toolkit.decision import entropy_weights
|
||||
|
||||
X = np.column_stack([rng.normal(size=50), np.ones(50)]) # 第二列无差异
|
||||
r = entropy_weights(X)
|
||||
assert np.isclose(r.params["weights"].sum(), 1.0)
|
||||
assert r.params["weights"][0] > r.params["weights"][1]
|
||||
|
||||
|
||||
def test_entropy_weights_handles_zero_and_negative():
|
||||
"""熵权法的三个坑之一:零值不能产生 NaN。"""
|
||||
from mcm_toolkit.decision import entropy_weights
|
||||
|
||||
r = entropy_weights(np.array([[0.0, 1.0], [1.0, 2.0], [2.0, 0.0]]))
|
||||
assert np.isfinite(r.params["weights"]).all()
|
||||
|
||||
|
||||
def test_ahp_consistency_for_consistent_matrix():
|
||||
"""完全一致的判断矩阵 CR 应接近 0。"""
|
||||
from mcm_toolkit.decision import ahp_weights
|
||||
|
||||
A = np.array([[1, 2, 4], [0.5, 1, 2], [0.25, 0.5, 1]], dtype=float)
|
||||
r = ahp_weights(A)
|
||||
assert r.metrics["CR"] < 0.01
|
||||
|
||||
|
||||
def test_ahp_weights_match_geometric_mean_for_consistent_matrix():
|
||||
from mcm_toolkit.decision import ahp_weights
|
||||
|
||||
A = np.array([[1, 2, 4], [0.5, 1, 2], [0.25, 0.5, 1]], dtype=float)
|
||||
r = ahp_weights(A)
|
||||
geo = (A.prod(axis=1)) ** (1 / 3)
|
||||
assert np.allclose(r.params["weights"], geo / geo.sum(), atol=1e-3)
|
||||
|
||||
|
||||
def test_gm11_fits_exponential_sequence():
|
||||
"""GM(1,1) 对纯指数序列应拟合得极好。"""
|
||||
from mcm_toolkit.decision import gm11_fit_predict
|
||||
|
||||
series = [2.0 * 1.1**k for k in range(6)]
|
||||
r = gm11_fit_predict(series, steps=2)
|
||||
assert r.metrics["c_ratio"] < 0.35 # 后验差一级
|
||||
@@ -1,58 +0,0 @@
|
||||
"""models 包测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L1")
|
||||
|
||||
|
||||
def test_fit_linear_recovers_known_coefficients(linear_data):
|
||||
"""真值 y = 2x + 1,拟合系数应在 1% 内。"""
|
||||
from mcm_toolkit.models import fit_linear
|
||||
|
||||
x, y = linear_data
|
||||
r = fit_linear(x.reshape(-1, 1), y)
|
||||
assert np.isclose(r.params["coef"][0], 2.0, rtol=0.01)
|
||||
assert np.isclose(r.params["intercept"], 1.0, atol=0.05)
|
||||
|
||||
|
||||
def test_fit_polynomial_matches_numpy(linear_data):
|
||||
from mcm_toolkit.models import fit_polynomial
|
||||
|
||||
x, y = linear_data
|
||||
r = fit_polynomial(x.reshape(-1, 1), y, degree=2)
|
||||
assert np.allclose(r.params["coef"], np.polyfit(x, y, 2), rtol=1e-6)
|
||||
|
||||
|
||||
def test_metrics_known_values():
|
||||
from mcm_toolkit.models import mae, r2_score, rmse
|
||||
|
||||
y_true = np.array([1.0, 2.0, 3.0])
|
||||
y_pred = np.array([1.0, 2.0, 5.0])
|
||||
assert np.isclose(rmse(y_true, y_pred), np.sqrt(4 / 3))
|
||||
assert np.isclose(mae(y_true, y_pred), 2 / 3)
|
||||
assert r2_score(y_true, y_pred) < 1.0
|
||||
|
||||
|
||||
def test_r2_is_one_for_perfect_fit():
|
||||
from mcm_toolkit.models import r2_score
|
||||
|
||||
y = np.array([1.0, 2.0, 3.0, 4.0])
|
||||
assert np.isclose(r2_score(y, y), 1.0)
|
||||
|
||||
|
||||
def test_compare_models_returns_table_sorted_by_first_metric(linear_data):
|
||||
from mcm_toolkit.models import compare_models, fit_linear, fit_polynomial
|
||||
|
||||
x, y = linear_data
|
||||
X = x.reshape(-1, 1)
|
||||
tbl = compare_models(
|
||||
{"linear": fit_linear(X, y), "poly2": fit_polynomial(X, y, degree=2)},
|
||||
X,
|
||||
y,
|
||||
cv=0,
|
||||
)
|
||||
assert set(tbl.index) >= {"linear", "poly2"}
|
||||
assert "rmse" in tbl.columns
|
||||
@@ -1,85 +0,0 @@
|
||||
"""solve 包测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1")
|
||||
|
||||
|
||||
def test_monte_carlo_integral_converges_to_analytic_value():
|
||||
"""∫₀¹ x² dx = 1/3。n=2e5 时误差应 < 0.01。"""
|
||||
from mcm_toolkit.solve import monte_carlo_integral
|
||||
|
||||
r = monte_carlo_integral(lambda X: X[:, 0] ** 2, bounds=[(0, 1)], n=200_000, random_state=0)
|
||||
assert abs(r.data - 1 / 3) < 0.01
|
||||
|
||||
|
||||
def test_monte_carlo_probability_of_standard_normal():
|
||||
"""P(Z > 0) = 0.5,n=5000 时误差应 < 0.05。"""
|
||||
from mcm_toolkit.solve import monte_carlo_probability
|
||||
|
||||
r = monte_carlo_probability(
|
||||
lambda z: z > 0,
|
||||
lambda rng, **kw: rng.standard_normal(),
|
||||
n=5000,
|
||||
random_state=0,
|
||||
)
|
||||
assert abs(r.metrics["probability"] - 0.5) < 0.05
|
||||
|
||||
|
||||
def test_monte_carlo_is_reproducible():
|
||||
from mcm_toolkit.solve import monte_carlo_probability
|
||||
|
||||
args = (lambda z: z > 0, lambda rng, **kw: rng.standard_normal())
|
||||
a = monte_carlo_probability(*args, n=1000, random_state=9)
|
||||
b = monte_carlo_probability(*args, n=1000, random_state=9)
|
||||
assert a.metrics["probability"] == b.metrics["probability"]
|
||||
|
||||
|
||||
def test_markov_steady_state_two_state_chain():
|
||||
"""P=[[0.9,0.1],[0.2,0.8]] 的平稳分布是 [2/3, 1/3]。"""
|
||||
from mcm_toolkit.solve import markov_steady_state
|
||||
|
||||
r = markov_steady_state(np.array([[0.9, 0.1], [0.2, 0.8]]))
|
||||
assert np.allclose(r.data, [2 / 3, 1 / 3], atol=1e-6)
|
||||
|
||||
|
||||
def test_knapsack_dp_known_optimum():
|
||||
from mcm_toolkit.solve import knapsack_dp
|
||||
|
||||
r = knapsack_dp(weights=[1, 3, 4, 5], values=[1, 4, 5, 7], capacity=7)
|
||||
assert np.isclose(r.metrics["total_value"], 9.0)
|
||||
assert np.isclose(r.metrics["total_weight"], 7.0)
|
||||
|
||||
|
||||
def test_linear_program_basic():
|
||||
from mcm_toolkit.solve import linear_program
|
||||
|
||||
r = linear_program(c=[1, 2], A_ub=[[1, 1]], b_ub=[4], bounds=[(0, None)] * 2)
|
||||
assert r.success
|
||||
assert np.isclose(r.metrics["objective"], 4.0)
|
||||
|
||||
|
||||
def test_mm1_metrics_closed_form():
|
||||
from mcm_toolkit.solve import mm1_metrics
|
||||
|
||||
r = mm1_metrics(arrival_rate=2, service_rate=3)
|
||||
assert np.isclose(r.metrics["rho"], 2 / 3, rtol=1e-6)
|
||||
assert np.isclose(r.metrics["L"], 2.0, rtol=1e-6)
|
||||
|
||||
|
||||
def test_mm1_rejects_unstable_system():
|
||||
"""λ >= μ 时系统不稳定,应 success=False 而不是返回 inf。"""
|
||||
from mcm_toolkit.solve import mm1_metrics
|
||||
|
||||
assert mm1_metrics(arrival_rate=3, service_rate=3).success is False
|
||||
|
||||
|
||||
def test_simulated_annealing_finds_quadratic_minimum():
|
||||
from mcm_toolkit.solve import simulated_annealing
|
||||
|
||||
f = lambda v: (v[0] - 3.0) ** 2 + (v[1] + 1.0) ** 2 # noqa: E731
|
||||
r = simulated_annealing(f, x0=[0.0, 0.0], n_iter=5000, random_state=0)
|
||||
assert np.allclose(r.params["x"], [3.0, -1.0], atol=0.1)
|
||||
@@ -1,67 +0,0 @@
|
||||
"""stats 包测试。
|
||||
|
||||
原则(见 docs/02-architecture.md §5):**测已知解析解,不测实现细节**。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L1")
|
||||
|
||||
|
||||
def test_bootstrap_ci_matches_normal_theory_on_large_n(rng):
|
||||
"""大样本下 bootstrap 百分位区间应逼近正态解析区间(±10% 容差)。"""
|
||||
from mcm_toolkit.stats import bootstrap_ci
|
||||
|
||||
x = rng.normal(10, 2, size=5000)
|
||||
r = bootstrap_ci(x, statistic=np.mean, confidence=0.95, n_resamples=5000, random_state=0)
|
||||
se = x.std(ddof=1) / np.sqrt(x.size)
|
||||
assert abs((r.params["high"] - r.params["low"]) - 2 * 1.96 * se) < 0.1 * 2 * 1.96 * se
|
||||
|
||||
|
||||
def test_bootstrap_ci_works_for_arbitrary_statistic(rng):
|
||||
"""bootstrap 的核心价值:任意统计量都能给区间(这里用中位数)。"""
|
||||
from mcm_toolkit.stats import bootstrap_ci
|
||||
|
||||
x = rng.lognormal(0, 1, size=2000)
|
||||
r = bootstrap_ci(x, statistic=np.median, random_state=0)
|
||||
assert r.params["low"] < r.params["high"]
|
||||
assert r.params["low"] < np.median(x) < r.params["high"]
|
||||
|
||||
|
||||
def test_bootstrap_ci_is_reproducible(rng):
|
||||
from mcm_toolkit.stats import bootstrap_ci
|
||||
|
||||
x = rng.normal(size=500)
|
||||
a = bootstrap_ci(x, statistic=np.mean, random_state=123)
|
||||
b = bootstrap_ci(x, statistic=np.mean, random_state=123)
|
||||
assert a.params["low"] == b.params["low"]
|
||||
|
||||
|
||||
def test_confidence_interval_matches_scipy(rng):
|
||||
from scipy import stats
|
||||
|
||||
from mcm_toolkit.stats import confidence_interval
|
||||
|
||||
x = rng.normal(5, 1, size=50)
|
||||
lo, hi = confidence_interval(x, confidence=0.95, method="t")
|
||||
ref = stats.t.interval(0.95, len(x) - 1, loc=x.mean(), scale=stats.sem(x))
|
||||
assert np.allclose([lo, hi], ref, rtol=1e-6)
|
||||
|
||||
|
||||
def test_correlation_matrix_diagonal_is_one(rng):
|
||||
from mcm_toolkit.stats import correlation_matrix
|
||||
|
||||
x = rng.normal(size=(200, 4))
|
||||
c = correlation_matrix(x)
|
||||
assert np.allclose(np.diag(np.asarray(c)), 1.0)
|
||||
|
||||
|
||||
def test_describe_reports_expected_keys(rng):
|
||||
from mcm_toolkit.stats import describe
|
||||
|
||||
table = describe(rng.normal(size=(100, 3)))
|
||||
for key in ["mean", "std", "min", "max", "median"]:
|
||||
assert key in table.index
|
||||
@@ -1,52 +0,0 @@
|
||||
"""viz 包测试。
|
||||
|
||||
绘图测试不比对像素——只验证"能画出来、能存成文件、尺寸符合美赛要求"。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["line", "scatter", "hist", "box", "bar"])
|
||||
def test_plot_kinds_produce_figure(kind, rng):
|
||||
from mcm_toolkit.viz import plot
|
||||
|
||||
x = rng.normal(size=100)
|
||||
fig, ax = plot(x, x, kind=kind)
|
||||
assert fig is not None and ax is not None
|
||||
|
||||
|
||||
def test_mcm_style_sets_300dpi():
|
||||
from mcm_toolkit.viz import mcm_style
|
||||
|
||||
mcm_style()
|
||||
assert matplotlib.rcParams["figure.dpi"] == 300.0
|
||||
assert matplotlib.rcParams["font.size"] >= 8.0
|
||||
|
||||
|
||||
def test_save_figure_writes_png_and_pdf(tmp_path, rng):
|
||||
from mcm_toolkit.viz import plot, save_figure
|
||||
|
||||
fig, _ = plot(rng.normal(size=50), rng.normal(size=50), kind="scatter")
|
||||
paths = save_figure(fig, tmp_path / "fig1", formats=("png", "pdf"))
|
||||
assert {p.suffix for p in paths} == {".png", ".pdf"}
|
||||
assert all(p.exists() and p.stat().st_size > 0 for p in paths)
|
||||
|
||||
|
||||
def test_save_table_writes_latex_with_caption(tmp_path):
|
||||
import pandas as pd
|
||||
|
||||
from mcm_toolkit.viz import save_table
|
||||
|
||||
df = pd.DataFrame({"rmse": [0.31, 0.28]}, index=["linear", "rf"])
|
||||
paths = save_table(df, tmp_path / "tab1", caption="模型对比", label="tab:compare")
|
||||
tex = next(p for p in paths if p.suffix == ".tex")
|
||||
content = tex.read_text(encoding="utf-8")
|
||||
assert "模型对比" in content and "tab:compare" in content
|
||||
Reference in new issue
Block a user