2026-10-01 17:43:48 +08:00
|
|
|
|
# 架构设计
|
|
|
|
|
|
|
|
|
|
|
|
> 上游依据:`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
|
2026-10-03 19:17:22 +08:00
|
|
|
|
│ └── validate.py as_array(统一入口)/ check_array / check_same_length / check_probability / check_square
|
2026-10-01 17:43:48 +08:00
|
|
|
|
│
|
|
|
|
|
|
├── 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` 裸抛。
|
|
|
|
|
|
|
2026-10-01 20:04:32 +08:00
|
|
|
|
|
|
|
|
|
|
### 4.7 类型约定(每个函数都适用)
|
|
|
|
|
|
|
|
|
|
|
|
公开函数的**参数类型注解必须写全**,并且与 docstring 说的是同一件事:注解给 IDE / pyright 看,
|
|
|
|
|
|
docstring 给人看。两处打架时,合法用法会被标红,真正的错误反而看不见。
|
|
|
|
|
|
|
|
|
|
|
|
判断顺序照抄下表,**不要凭直觉**:
|
|
|
|
|
|
|
|
|
|
|
|
| 值可能来自 | 怎么写 | 为什么 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| 整数 | `isinstance(x, (int, np.integer))`,取值统一 `int(x)` | `np.int64` **不是** `int` 的子类,只写 `int` 会漏 |
|
|
|
|
|
|
| 浮点 | `isinstance(x, (float, np.floating))` | `np.float64` 是 `float` 子类,但 `np.float32` 不是——不统一 |
|
|
|
|
|
|
| 布尔 | `isinstance(x, bool)` 要放在 `int` 判断**之前** | `True` 也是 `int`,顺序反了会当成 1 |
|
|
|
|
|
|
| 数组 / 表 | `np.asarray(x, dtype=float)` 后再校验 | 别假设调用方已经转好类型 |
|
|
|
|
|
|
|
|
|
|
|
|
**非法类型必须 `raise TypeError`,不许静默兜底。** 静默的典型后果是
|
|
|
|
|
|
"种子被忽略、结果不可复现,但没有任何提示"——比直接报错难查一百倍。
|
|
|
|
|
|
|
|
|
|
|
|
配套要求,每个公开函数都要有:
|
|
|
|
|
|
|
|
|
|
|
|
- `Parameters` 写清类型与含义;允许 `None` 时,写明 `None` 会走哪条分支
|
|
|
|
|
|
- `Raises` 写清会抛什么异常
|
|
|
|
|
|
|
|
|
|
|
|
参考实现:`core/random.py` 的 `get_rng`。
|
|
|
|
|
|
|
2026-10-03 19:17:22 +08:00
|
|
|
|
### 4.8 入口只有一个:`core.validate.as_array`
|
|
|
|
|
|
|
|
|
|
|
|
**凡是要数值数组的函数,第一行都从 `as_array` 进**,不要各自抄
|
|
|
|
|
|
`isinstance(x, pd.DataFrame)` / `select_dtypes` / `np.asarray`:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
arr, kind = as_array(x, numeric_only=False) # 入口:白名单 + 转 ndarray + 报告原类型
|
|
|
|
|
|
... # 中间只写算法
|
|
|
|
|
|
if kind == "frame": # 出口:自己手写,各函数形状不同,没法一刀切
|
|
|
|
|
|
return pd.DataFrame(result, index=x.index, columns=x.columns)
|
|
|
|
|
|
if kind == "series":
|
|
|
|
|
|
return pd.Series(result, index=x.index, name=x.name)
|
|
|
|
|
|
return result
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
三条硬规矩:
|
|
|
|
|
|
|
|
|
|
|
|
1. **`numeric_only` 按"输出契约"选,不按喜好选**:输出要与输入**同形**的函数
|
|
|
|
|
|
(`normalize` / `standardize` / `log_transform`)必须 `False` —— 跳过非数值列会让返回的表少列、
|
|
|
|
|
|
对不上原表;输出是"一张数值结果表"的(`zscore` / `confidence_interval` / `t_test`)用默认的 `True`。
|
|
|
|
|
|
2. **默认不摊平**。要摊平必须显式 `ndim=1`(`t_test` 曾因静默摊平把 `gdp` 和 `pop` 混成一个样本)。
|
|
|
|
|
|
3. **`as_array` 不删缺失值**。`df.corr` 成对删、`describe` 整列统计、`paired=True` 的 t 检验必须**成对**删 ——
|
|
|
|
|
|
在这一层删掉,行与行的对应关系就永久丢了。删缺失是**语义**,归各函数。
|
|
|
|
|
|
|
|
|
|
|
|
只走入口、不走出口的(输出与输入不同形):`describe`、`correlation_*`、`confidence_interval`、`t_test`。
|
|
|
|
|
|
|
2026-10-01 17:43:48 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 五、测试策略
|
|
|
|
|
|
|
|
|
|
|
|
- 目录 `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`。
|
|
|
|
|
|
- 每条断言都要能说清"错了说明什么"。
|