Compare commits

..
1 Commits
Author SHA1 Message Date
guzhujushi 1c683cfb4d Initial commit 2026-10-01 17:49:54 +08:00
76 changed files with 15 additions and 6845 deletions

No files matched your search

-30
View File
@@ -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
-51
View File
@@ -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
View File
@@ -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%/
-31
View File
@@ -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
- 项目初始化,包结构落盘(实现为空)。
+13 -16
View File
@@ -1,21 +1,18 @@
MIT License MIT License
Copyright (c) 2026 孤竹居士 Copyright (c) 2026 guzhujushi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
of this software and associated documentation files (the "Software"), to deal associated documentation files (the "Software"), to deal in the Software without restriction, including
in the Software without restriction, including without limitation the rights without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
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
copies of the Software, and to permit persons to whom the Software is following conditions:
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all copies or substantial
copies or substantial portions of the Software. portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, USE OR OTHER DEALINGS IN THE SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -71
View File
@@ -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)。
-118
View File
@@ -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
-207
View File
@@ -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 个包建齐"相反。
-178
View File
@@ -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`。
- 每条断言都要能说清"错了说明什么"。
-112
View File
@@ -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 | 仓库自然长大 |
**核心原则:先跑通一条完整链路,再补宽度。**
-238
View File
@@ -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"
```
-156
View File
@@ -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>
```
-20
View File
@@ -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`;本目录只放**不会频繁变**的内容。
-26
View File
@@ -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 档,应该降档删卡片。
-18
View File
@@ -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 放进依赖**——它是可选的、和比赛无关的工具。
-137
View File
@@ -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 结果不同时要说明选了哪个及理由。
```
-18
View File
@@ -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`;这里放的是**建模思路**。
-53
View File
@@ -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 . # 本项目(可编辑安装)
-42
View File
@@ -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()
-48
View File
@@ -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()
-45
View File
@@ -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()
-21
View File
@@ -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
```
View File
Whitespace-only changes.
View File
Whitespace-only changes.
-25
View File
@@ -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. 论文里被评审/队友质疑的地方是什么?→ 对应的函数要补文档或补鲁棒性检验。
-64
View File
@@ -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"
-103
View File
@@ -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
-136
View File
@@ -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"
-71
View File
@@ -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'
-54
View File
@@ -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",
]
-3
View File
@@ -1,3 +0,0 @@
"""版本号唯一来源。"""
__version__ = "0.0.1"
-23
View File
@@ -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",
]
-45
View File
@@ -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
-51
View File
@@ -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
-54
View File
@@ -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
-26
View File
@@ -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",
]
-81
View File
@@ -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
-37
View File
@@ -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
-46
View File
@@ -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
-58
View File
@@ -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
-26
View File
@@ -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",
]
-57
View File
@@ -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
-71
View File
@@ -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
-59
View File
@@ -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
-71
View File
@@ -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
-41
View File
@@ -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",
]
-31
View File
@@ -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),便于直接拼成对比表。"""
...
-94
View File
@@ -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
-88
View File
@@ -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
-70
View File
@@ -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
-67
View File
@@ -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
-58
View File
@@ -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",
]
-79
View File
@@ -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
-86
View File
@@ -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
-234
View File
@@ -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
-214
View File
@@ -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
-218
View File
@@ -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
-32
View File
@@ -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",
]
-71
View File
@@ -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
-40
View File
@@ -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
-114
View File
@@ -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
-21
View File
@@ -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",
]
-74
View File
@@ -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
-79
View File
@@ -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
-40
View File
@@ -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
View File
Whitespace-only changes.
-32
View File
@@ -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],
]
)
-67
View File
@@ -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)
-69
View File
@@ -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
-65
View File
@@ -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) == ["城市", "人口"]
-79
View File
@@ -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 # 后验差一级
-58
View File
@@ -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
-85
View File
@@ -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)
-67
View File
@@ -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
-52
View File
@@ -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