From 3cdbba4cfb3fa16b2243cd4994fc7d9f894a5464 Mon Sep 17 00:00:00 2001 From: guzhujushi Date: Thu, 1 Oct 2026 23:11:47 +0800 Subject: [PATCH] =?UTF-8?q?test(data):=20=E6=8B=86=E6=8E=89=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=BA=A7=20skip=EF=BC=8C=E7=BB=99=20load=5Ftable/save?= =?UTF-8?q?=5Ftable=20=E8=A1=A5=208=20=E6=9D=A1=E8=A1=8C=E4=B8=BA=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来 tests/test_data.py 顶上一条 pytestmark 把整个文件跳过了, 连已经写好的 load_table 也一起跳——等于零覆盖。 - 改成逐条 @pytest.mark.skip,只标 5 个未实现的算法 - load_table 补:UTF-8 不被误读成 GBK、NA/-/#N/A 归 NaN、 显式 encoding 优先、未知后缀抛 ValueError、缺文件抛带路径的报错 - save_table 补:往返读写返回 Path、非 DataFrame 抛 TypeError - 测试数 31 passed / 45 skipped -> 39 passed / 44 skipped --- tests/test_data.py | 189 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 142 insertions(+), 47 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index f3eda1c..09384b8 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,58 +1,22 @@ -"""data 包测试。""" +"""data 包测试。 + +L0 阶段的约定:**只测已实现的行为**。未实现的算法用逐条 skip 标出, +不要用模块级 pytestmark —— 那会把同文件里已实现的函数一起跳过, +让 load_table 这种已经写好的函数变成零覆盖。 +""" from __future__ import annotations +from pathlib import Path + import numpy as np +import pandas as pd import pytest -pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1") +_NOT_IMPLEMENTED = "未实现:见 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 +# ---------------------------------------------------------------- io: load_table def test_load_table_detects_gbk(tmp_path): @@ -63,3 +27,134 @@ def test_load_table_detects_gbk(tmp_path): p.write_bytes("城市,人口\n北京,2189\n".encode("gbk")) df = load_table(p) assert list(df.columns) == ["城市", "人口"] + + +def test_load_table_keeps_utf8_readable(tmp_path): + """UTF-8 文件必须按 UTF-8 读出,不能先试 GBK 变成乱码。""" + from mcm_toolkit.data import load_table + + p = tmp_path / "utf8.csv" + p.write_text("省份,GDP增速\n广东,5.8\n", encoding="utf-8") + df = load_table(p) + assert list(df.columns) == ["省份", "GDP增速"] + assert df.loc[0, "省份"] == "广东" + + +def test_load_table_maps_na_like_tokens_to_nan(tmp_path): + """赛题里的缺失值长得五花八门,默认 na_values 要全部吃成 NaN。""" + from mcm_toolkit.data import load_table + + p = tmp_path / "na.csv" + p.write_text("a,b,c\n1,NA,\n2,-,#N/A\n", encoding="utf-8") + df = load_table(p) + assert df["b"].isna().all() + assert df["c"].isna().all() + assert df["a"].notna().all() + + +def test_load_table_respects_explicit_encoding(tmp_path): + """显式给了 encoding 就照办,不再做探测。""" + from mcm_toolkit.data import load_table + + p = tmp_path / "gbk.csv" + p.write_bytes("城市,人口\n上海,2487\n".encode("gbk")) + df = load_table(p, encoding="gbk") + assert list(df.columns) == ["城市", "人口"] + + +def test_load_table_rejects_unknown_suffix(tmp_path): + """不支持的扩展名要抛 ValueError,而且要带上后缀名。""" + from mcm_toolkit.data import load_table + + p = tmp_path / "data.txt" + p.write_text("a,b\n1,2\n", encoding="utf-8") + with pytest.raises(ValueError, match=r"\.txt"): + load_table(p) + + +def test_load_table_missing_file_raises(tmp_path): + """文件不存在时要把路径原样带出来,方便定位。""" + from mcm_toolkit.data import load_table + + missing = tmp_path / "nope.csv" + with pytest.raises(FileNotFoundError, match=r"nope\.csv"): + load_table(missing) + + +# ---------------------------------------------------------------- io: save_table + + +def test_save_table_round_trip(tmp_path): + """存进去再读出来,内容要一致,并且返回实际写入的 Path。""" + from mcm_toolkit.data import load_table, save_table + + df = pd.DataFrame({"城市": ["北京", "上海"], "人口": [2189.0, 2487.0]}) + out = save_table(df, tmp_path / "out.csv") + assert isinstance(out, Path) + assert out.exists() + back = load_table(out) + assert list(back.columns) == ["城市", "人口"] + assert back["人口"].tolist() == [2189.0, 2487.0] + + +def test_save_table_rejects_non_dataframe(tmp_path): + """传错类型要当场报错,不要写出一个奇怪的文件。""" + from mcm_toolkit.data import save_table + + with pytest.raises(TypeError): + save_table([1, 2, 3], tmp_path / "bad.csv") + + +# ------------------------------------------------- 以下为未实现算法(L0/L1 待写) + + +@pytest.mark.skip(reason=_NOT_IMPLEMENTED) +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() + + +@pytest.mark.skip(reason=_NOT_IMPLEMENTED) +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 + + +@pytest.mark.skip(reason=_NOT_IMPLEMENTED) +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) + + +@pytest.mark.skip(reason=_NOT_IMPLEMENTED) +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 + + +@pytest.mark.skip(reason=_NOT_IMPLEMENTED) +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