审查并精简 ChatGPT 的建库建议后落地的第一版骨架,只定接口不写实现。 - 建议审查:14 个包 → 7 个;约 250 个函数 → 约 40 个;5 个 Result 类 → 1 个 (docs/01-suggestion-review.md) - docs/:架构、路线图(含永久不做清单)、环境、Git 工作流、函数卡片模板 - src/mcm_toolkit/:core / data / stats / models / solve / decision / viz 共 23 个模块,均为签名 + docstring + NotImplementedError - tests/:9 个文件,其中 test_api_surface.py 已通过(20 passed) - examples/ problems/ notebooks/ figures/ tables/ 骨架 - pyproject.toml(核心依赖 4 个 + 6 组 extras)、environment.yml - scripts/:setup-venv.ps1(ASCII + LF)、setup-conda.ps1、doctor.ps1 - 环境:.venv(system-site-packages)与 conda 环境 mcm-toolkit 均已建好, 数值栈版本与 base 完全对齐(numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6,MKL 后端)
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""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 # 后验差一级
|