把"数据类型必须明确"从口头要求变成仓库规则,每个函数都适用。 - 判断顺序表:整数用 (int, np.integer)、浮点用 (float, np.floating)、 bool 要放在 int 判断之前、数组先 np.asarray 归一化 - 硬规则:非法类型必须 raise TypeError,不许静默兜底 (静默的后果是"种子被忽略、结果不可复现且无提示",最难查) - 要求每个公开函数写全 Parameters(含 None 走哪条分支)与 Raises - docs/03-roadmap.md §3.2 开发纪律加一行指针
9.7 KiB
9.7 KiB
架构设计
上游依据:
01-suggestion-review.md。本文件定义"代码往哪写、边界在哪、接口长什么样"。
一、设计原则
- 薄封装优先。 能用 SciPy / sklearn / statsmodels / NetworkX / SALib 一行解决的,就只做一行转发 + 参数校验 +
Result包装。本仓库不替代这些库。 - 自己实现是有目的的。 只有"库里没有、或者自己写才学得会"的算法才手写(bootstrap、Monte Carlo、DP、SA、熵权、TOPSIS、灰色……)。手写实现要能被读懂、能被改。
- 统一出口。 凡是"拟合 / 求解 / 模拟"类函数,一律返回
Result,字段固定。 - 可复现。 所有含随机的公开函数都接受
random_state,默认None(用全局 RNG)。 - 默认值面向比赛。 默认参数应当在 4 天赛程里"直接能跑完",不用调。
二、包结构(7 个包)
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(唯一结果对象)
@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 随机数
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 模型协议
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 裸抛。
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。
五、测试策略
- 目录
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。 - 每条断言都要能说清"错了说明什么"。